amzn / style-dictionary

A build system for creating cross-platform styles.
https://styledictionary.com
Apache License 2.0
3.87k stars 543 forks source link

Add `formatPlatform`/`formatAllPlatforms` methods #1211

Closed jorenbroekema closed 4 months ago

jorenbroekema commented 4 months ago

See https://github.com/amzn/style-dictionary/issues/1198 for context

The idea is a method called formatPlatform/formatAllPlatforms which doesn't write to the filesystem but just returns the outputs as strings, or as some other data structure, enabling users to do with that what they want, whether that's writing to the filesystem or something more custom.

The input for these methods is similar to buildPlatform / buildAllPlatforms, the "platform" string e.g. "css". It will consume the files array from that platform config, and the destination property on File will be optional now, because outputting to a destination file is not a requirement, only if you use buildPlatform/buildAllPlatforms.

Use cases

Combining the outputs of multiple SD instances (strings) into a combined file output

:root {
  --divider-color-default: rgba(240, 240, 240, 1);
  --divider-color-inverse: rgba(50, 50, 50, 1);
}

html[theme="dark"] {
  --divider-color-default: rgba(50, 50, 50, 1);
  --divider-color-inverse: rgba(240, 240, 240, 1);
}

In the above example, there are two SD instances, one that has light.json as input source and the other dark.json, both of which are defining the same design tokens but with different values.

Outputting an array of string outputs per token

So themed values can be combined into the property itself:

public enum DividerTokens: DividerDesignTokens {
    public static let dividerColorDefault = Color(light: Color(red: 0.910, green: 0.910, blue: 0.910, opacity: 1), dark: Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1))l
    public static let dividerColorInverse = Color(light: Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1), dark: Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1))
    public static let dividerSizeHeight = CGFloat(1)
}

In the above example, the input is the same as the CSS example but because we're formatting the different theme permutations into the property itself, we'll want the format step to output the following data structure or something similar:

// assuming this is base
const light = {
  dividerColorDefault: {
    type: 'color', 
    value: 'Color(red: 0.910, green: 0.910, blue: 0.910, opacity: 1)'
  },
  dividerColorInverse: { 
    type: 'color',
    value: 'Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1)'
  },
  dividerSizeHeight: {
    type: 'dimension',
    value: 'CGFloat(1)'
  },
};

const dark = {
  dividerColorDefault: {
    type: 'color', 
    value: 'Color(red: 0.357, green: 0.357, blue: 0.357, opacity: 1)'
  },
  dividerColorInverse: { 
    type: 'color',
    value: 'Color(red: 0.910, green: 0.910, blue: 0.910, opacity: 1)'
  },
};

/**
 * 1. Loop over base `Object.entries(light)`, then for each token,
 * 2. Check if the override theme (dark) has a token with the same name & a different value
 * 3. Create theme-specific output string for this token type e.g. for colors: `Color(light: ${lightTokenValue}, dark: ${darkTokenValue})`
 */
const output = `...`;

Besides these new methods being added as public API:

Important note: combining theme outputs in a single file especially from the perspective of the Web, is not a good idea for performance generally speaking: you'd be loading a lot of redundant styles for themes that aren't currently selected by the end user, and are therefore inactive, so outputting to individual files and switching/swapping those when the theme selection changes is still the better approach, usually.