Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull $extensions from closest ancestor #1376

Open
gavinbaradic opened this issue Oct 28, 2024 · 4 comments
Open

Pull $extensions from closest ancestor #1376

gavinbaradic opened this issue Oct 28, 2024 · 4 comments

Comments

@gavinbaradic
Copy link

I see the $type property is applied from the closest ancestor in my design tokens file. Is there a way to do this for other properties? Specifically looking at using it for $extensions.

I'm just getting into the DTCG format and this seems like it would be a really useful feature to keep token files clean. If not supported out of the box, it'd be nice to show the best approach for how to achieve something like this.

Any advice would be greatly appreciated!

{
  color: {
    light: {
      $type: 'color',

      content: {
        $extensions: {
          'org.figma': {
            scopes: ['TEXT_FILL'],
          },
        },

        default: {
          $value: '{base.color.neutral.black}',
          $description: 'Default color for text and icons.',
        },
        muted: { $value: '{base.color.neutral.600}' },
        // ...
      }
    }
  }
}
@jorenbroekema
Copy link
Collaborator

jorenbroekema commented Oct 29, 2024

Yeah you could look at this utility https://github.com/amzn/style-dictionary/blob/main/lib/utils/typeDtcgDelegate.js
which is responsible for delegating the group level $type to the token level. I could have probably done a better job at commenting what that recursive code is doing but essentially it recurses breadth-first and:

  • checks if there is a $type on the current level, if so, keep a memo of it
  • if there is a $type on the current level and we already have a memo, update the memo so the "deeper" $type overrules the one from the ancestors
  • once we stored a memo of the type we can delete the group level $type property, it will be delegated to the token level
  • if the current level has no $type, but we do have a memo, and we also have a $value key, it means we have arrived at the token level, so we adopt a $type property -> using the memo
  • recurse 1 layer deeper if we encounter an object value for one of the key-value pairs of the slice

Hope that makes sense 😅 and you can now copy + adjust it for $extensions. Then, you will have to register a preprocessor that uses your adapted $extensions delegate function:

StyleDictionary.registerPreprocessor({
  name: '$extensions-delegate',
  preprocessor: (dictionary) => extensionsDelegate(dictionary)
});

and use it by name in your config "preprocessors" prop, more info here


If the spec starts allowing more $props to be group-level dedupable just like $type, then I'm open to add these to Style Dictionary but until that's standardized / defined in the DTCG format, I think it's better if this is something users do themselves.

That said, if there's a lot of use cases where putting other properties on the group level and delegating them to the token level is useful, we might want to publish a utility that makes it easy for users to use this e.g. the public API would look something like:

import StyleDictionary from 'style-dictionary';
import { delegateGroupPropertiesFactory } from 'style-dictionary/utils';

StyleDictionary.registerPreprocessor({
  name: 'delegate-group-props',
  preprocessor: (dictionary) => delegateGroupPropertiesFactory(['$extensions', '$description'])(dictionary)
});

@gavinbaradic
Copy link
Author

This worked perfectly, thank you! I was looking around the codebase for the utility that handled $type and just couldn't find it.

Appreciate the help! I could definitely see value in adding this to the docs even with the DTCG format not being finalized... Being able to apply properties to all children tokens is a super useful feature.

@chris-dura
Copy link

If the spec starts allowing more $props to be group-level dedupable just like $type, then I'm open to add these to Style Dictionary but until that's standardized / defined in the DTCG format, I think it's better if this is something users do themselves.

FWIW -- the DTCG format module now allows for several additional group properties...

  • $type
  • $description
  • $deprecated
  • $extensions

@jorenbroekema
Copy link
Collaborator

Good point @chris-dura .

If I'm understanding correctly, it seems that $deprecated is similar to $type in that nested child nodes inherit it from the parent node, whereas with $extensions and $description, they belong to the token group OR the token level, but tokens within a group don't automatically inherit them.

Group level metadata tends to get lost when the dictionary structure is flattened, we need to figure out a way to store these group level metada props so that when we convert from a flat structure to a nested structure, we can reconstruct this metadata again. Perhaps our convertTokenData for array/map can accept an option which will adjust its return type:

// object to array, save the meta props as a flat list as well
const { output: flattened, meta } = convertTokenData(dictionary, { output: 'array', saveGroupMeta: true });

// array to object, pass the meta back in, to reconstruct it
const nested = convertTokenData(flattened, { output: 'object', meta });

Example:

{
  "color": {
    "$type": "color",
    "$description": "Red brand color range",
    "$extensions": {
      "com.styledictionary": {
        "foo": "bar"
      }
    },
    "red": {
      "$deprecated": "Use darkred instead, instead for 500, this is still valid",
      "500": {
        "$deprecated": false,
        "$value": "#ff0000"
      }
    }
  }
}

Would be flattened to:

[
  {
    "key": "{color.red.500}",
    "$type": "color", // inherited
    "$deprecated": false, // would be inherited, if it wasn't negated
    "value": "#ff0000"
  }
]

and meta:

[
  {
    "key": "{color}",
    "$type": "color",
    "$description": "Red brand color range",
    "$extensions": {
      "com.styledictionary": {
        "foo": "bar"
      }
    }
  },
  {
    "key": "{color.red}",
    "$deprecated": "Use darkred instead, instead for 500, this is still valid"
  }
]

And when converting back to object, we should also allow specifying which properties we want to push back to the farthest common ancestor e.g.:

// array to object, pass the meta back in, to reconstruct it
const nested = convertTokenData(flattened, { output: 'object', meta, applyToGroup: ['$type', '$description'] });

So to summarize:

  • $type -> no change needed, already works: https://styledictionary.com/reference/utils/dtcg/ , using the convertToDTCG utils can be used to put the $type back to the closest common ancestor, although we should probably use the convertTokenData to do it instead in the future once it can reconstruct meta.
  • $deprecated -> Works similar to $type but we have to abstract the util linked above in a way where we can apply it to other properties than just $type. Small difference: you can negate a parent's deprecated by setting it to false in a child. For that to work when reconstructing from flat back to object structure, we'll have to store the group meta props.
  • $extensions -> similar to $deprecated, we have to store the group meta props to be able to reconstruct because child nodes don't inherit this from the parent groups. That's also the difference to the previous 2, child nodes not inheriting it.
  • $description -> same as $extensions

It seems technically feasible to me.. definitely not trivial, definitely requires a fair amount of tests to be written, not sure when I'll be able to pick this up given there are a couple more pressing issues than the luxury of being able to author more metadata on token group level

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants