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

need non flat es6 format #990

Closed isimmons closed 1 year ago

isimmons commented 1 year ago

The javascript/module format and javascript/module-flat gives options if I want to flatten objects or not but with javascript/es6 the only option seems to be flat.

Storybook 7 docs complains that the file has no export named 'default' due to it being a cjs module but if I build it as javascript/es6 I get a bunch of single line exports where the objects are flattened like

export const AnimationsDefault = "all 400ms cubic-bezier(0.420, 0.000, 0.580, 1.000)";
export const ColorsPrimary100 = "#EBEFFF";
export const ColorsPrimary200 = "#B9C4FF";
//...

But in my react app my components need to access them like this which works in react but not when storybook loads my component, due to the above mentioned problem

// import the tokens as an object or accept as a prop and then
color = tokens.colors[props.color][800].value}

This is the correct format from using javascript/module but the wrong export for what I need

module.exports = {
    "animations": {
        "default": {
            "value": "all 400ms cubic-bezier(0.420, 0.000, 0.580, 1.000)",
            "filePath": "src/tokens/animations.json",
            "isSource": true,
            "original": {
                "value": "all 400ms cubic-bezier(0.420, 0.000, 0.580, 1.000)"
            },
            "name": "AnimationsDefault",
            "attributes": {
                "category": "animations",
                "type": "default"
            },
            "path": [
                "animations",
                "default"
            ]
        }
    },
    "colors": {
        "primary": {
            "100": {
                "value": "#EBEFFF",
                "filePath": "src/tokens/colors.json",
                "isSource": true,
                "original": {
                    "value": "#EBEFFF"
                },
                "name": "ColorsPrimary100",
                "attributes": {
                    "category": "colors",
                    "type": "primary",
                    "item": "100"
                },
   //... and so on

If I change the first line to

export default {

This makes storybook and react happy but of course it would get overwritten any time I re-build tokens.

Is there a way to make javascript/es6 not flat?

Thanks

pascalduez commented 1 year ago

Hello,

you'll need to build a custom format to achieve that.

Here is how I implemented it. There are some specific stuff in here like prettier, camelCase, that are not mandatory, but this should give you a sense of it.

import { camelCase } from 'change-case';
import { format } from 'prettier';
import StyleDictionary from 'style-dictionary';

import { isPlainObject } from '../utils/isPlainObject.js';

const {
  formatHelpers: { fileHeader },
} = StyleDictionary;

export const nestedEs = {
  name: 'nested/es',
  formatter: ({ dictionary, file, platform }) => {
    let { prefix } = platform;
    let tokens = prefix ? { [prefix]: dictionary.tokens } : dictionary.tokens;

    let ouput = [
      fileHeader({ file }),
      'export default ',
      JSON.stringify(minifyDictionary(tokens), null, 2),
    ].join('');

    return format(ouput, { parser: 'typescript', printWidth: 200 });
  },
};

/**
 * Output a nested object.
 * Strip out everything except values.
 */
function minifyDictionary(node) {
  if (!isPlainObject(node)) return node;

  if (node.hasOwnProperty('value')) return node.value;

  let nextNode = {};
  for (let [key, value] of Object.entries(node)) {
    nextNode[camelCase(key)] = minifyDictionary(value);
  }

  return nextNode;
}
isimmons commented 1 year ago

Thanks @pascalduez

I just copied the function from style-dictionary for javascript/module and prefixed it with 'export default' instead of 'module.exports' and this made react, storybook, and TS all happy :-)

const StyleDictionary = require('style-dictionary');
const { fileHeader } = StyleDictionary.formatHelpers;

StyleDictionary.registerFormat({
  name: 'myCustomFormat',
  formatter: function ({ dictionary, file }) {
    return (
      fileHeader({ file }) +
      'export default ' +
      JSON.stringify(dictionary.tokens, null, 2) +
      ';\n'
    );
  },
});

But same as with the javascript/module format, there is a lot of un-needed info in there for my needs so I'll try out your example and see what I need to do to shrink this

"colors": {
    "primary": {
      "100": {
        "value": "#EBEFFF",
        "filePath": "src/tokens/colors.json",
        "isSource": true,
        "original": {
          "value": "#EBEFFF"
        },
        "name": "ColorsPrimary100",
        "attributes": {
          "category": "colors",
          "type": "primary",
          "item": "100"
        },
        "path": [
          "colors",
          "primary",
          "100"
        ]
      },

down to this

"colors": {
    "primary": {
      "100":  "#EBEFFF"
   }
},
isimmons commented 1 year ago

Well here is the solution. style-dictionary-utils

const StyleDictionary = require('style-dictionary-utils');

const myStyleDictionary = StyleDictionary.extend({
  source: ['src/tokens/**/*.json'],
  platforms: {
    scss: {
      transformGroup: 'scss',
      buildPath: 'lib/tokens/scss/',
      files: [
        {
          destination: 'tokens.scss',
          format: 'scss/variables',
        },
      ],
    },
    css: {
      transformGroup: 'css',
      buildPath: 'lib/tokens/css/',
      files: [
        {
          destination: 'tokens.css',
          format: 'css/variables',
        },
      ],
    },
    'js-src': {
      transformGroup: 'js',
      buildPath: 'src/tokens/js/',
      files: [
        {
          destination: 'tokens.js',
          format: 'javascript/esm',
        },
      ],
    },
    js: {
      transformGroup: 'js',
      buildPath: 'lib/tokens/js/',
      files: [
        {
          destination: 'tokens.js',
          format: 'javascript/esm',
        },
      ],
    },
  },
});

myStyleDictionary.buildAllPlatforms();

Produces this beautiful output for the js files

export default {
  animations: {
    default: "all 400ms cubic-bezier(0.420, 0.000, 0.580, 1.000)",
  },
  colors: {
    primary: {
      "100": "#EBEFFF",
      "200": "#B9C4FF",
      "300": "#8FA0FF",
      "400": "#6E82FE",
      "500": "#556AEB",
      "600": "#1D2F99",
      "700": "#1D2F99",
      "800": "#0C1A66",
      "900": "#020A33",
    },
    neutral: {
      "100": "#F8F9FA",
      "200": "#E9ECEF",
      "300": "#DEE2E6",
      "400": "#CED4DA",
      "500": "#ADB5BD",
      "600": "#6C757D",
      "700": "#495057",
      "800": "#343A40",
      "900": "#212529",
      black: "#000000",
      white: "#FFFFFF",
    },
    warning: {
      "100": "#FFF8EB",
      "200": "#FFE3B0",
      "300": "#FFCC75",
      "400": "#FFB23B",
      "500": "#FF9500",
      "600": "#CC7C00",
      "700": "#996000",
      "800": "#664200",
      "900": "#332200",
    },
    danger: {
      "100": "#FF9680",
      "200": "#FF8166",
      "300": "#FF6C4C",
      "400": "#FF4B24",
      "500": "#FF380D",
      "600": "#EB2A00",
      "700": "#CC2400",
      "800": "#B22000",
      "900": "#991B00",
    },
  },
  radius: {
    small: "8px",
    large: "20px",
  },
  shadows: {
    "level-1": "0px 4px 6px rgba(33, 37, 41, 0.2)",
    "level-2": "0px 2px 10px rgba(33, 37, 41, 0.15)",
  },
  spacings: {
    "4": "4px",
    "8": "8px",
    "16": "16px",
    "32": "32px",
    "40": "40px",
    "48": "48px",
    "56": "56px",
    "64": "64px",
    "80": "80px",
    "100": "100px",
  },
};

I'm gonna go ahead and close. I wonder though if something like this will get added into the next version.