Open jorenbroekema opened 11 months ago
https://github.com/tokens-studio/sd-transforms/issues/211 another related issue, which essentially boils down to that:
{
"color": {
"red": { "value": "#f00" },
"danger": { "value": "{color.red}", "darken": 0.75 },
"error": { "value": "{color.danger}", "darken": 0.5 }
}
}
stops working when the "darken" value also is or contains a reference 😅 and it's really quite tricky to fix that as far as I can see
https://github.com/tokens-studio/sd-transforms/issues/198 related topic for outputReferences, which begs the question:
If we allow not resolving references to begin with, will we allow transforms that apply on values with references, useful for token values:
"{dimension.lg} * {dimension.scale}"
-> calc(var(--dimension-lg) * var(--dimension-scale))
{ value: "{colors.red}", "lighten": "0.5" }
-> color-mix(in srgb, var(--colors.red), #fff 50%)
For the second case, you could imagine that there is a reference in the lighten prop:
{ value: "{colors.red}", "lighten": "{lighten.half}" }
-> color-mix(in srgb, var(--colors.red), #fff 50%)
Which could then be tackled by allowing resolve to be a callback function resolve: ({ propName }) => propName !== 'value'
which excludes references inside value props to be resolved, but resolves it in other props.
This issue is a proposal for changing some of Style-Dictionary's core architecture with regards to resolving references and transforms.
It goes over two topics, but they are quite interconnected hence why I include both in this issue.
Lifecycle
As I understood, the lifecycle of style-dictionary is roughly as follows:
However, a crucial thing to understand is that step 2 till 4 runs iteratively for each token, in sequence, rather than phased for the whole dictionary, and the way that tokens with references are deferred continuously for transitive transforms to deal with, makes this quite complicated.
It can easily lead to transforms misbehaving and stacking up on one another, depending on the order of tokens that use references. This is explained in the docs, but I would like to challenge this architecture.
Problematic example configurator link
Now imagine we have a transitive transform that puts !default after each value. Due to the references and the fact that transforms run iteratively per token, the !default suffix is stacking up once for every reference layer, resulting in pretty awkward output:
There are also use cases where this stacking of transforms is intended:
error.value should be darken(darken('#f00', 0.75), 0.5), so stacked.
One solution, I think, would be to run every step as a phase over the whole dictionary. This simplifies the lifecycle conceptually as well and ensures that the order of tokens doesn't change the output:
But this would break use cases where the stacking of transitive transforms is intended, so that's important to note.
Output References
Right now, outputReferences is an option on format-level, only enabled for CSS-like languages:
css
,sass
(akascss
),stylus
andless
. Output references has had its challenges when combining with value transforms (transitive ones specifically), some of which have been fixed by me in version 3.9.0 and will be forward-ported to 4.0.0-prerelease.1, but I think outputReferences struggles more fundamentally, because it only applies at the format lifecycle, which is actually a bit too late to work reliably.I would like to propose that outputting references should be split into separate parts.
API proposal:
"references" prop is valid on global dictionary config level, as well as on platform specific level.
Putting resolve on
false
will mean that any values that contain references, will just keep those references as is, rather than resolving them.The other part of this proposal is that in the future all transforms will happen after reference resolution. If reference resolution is turned off, transforms will not apply to tokens with references.
The math expression transform cannot do much with this value pre-reference-resolution. It needs to happen afterwards, where both foo and bar are resolved to numbers.
Finally, when references are resolved (or not!), transforms are ran (or not, if value is/contains a reference), we get to the format stage, where any format can ensure that it understands how to deal with references, e.g. for ES6 variables:
This simplifies the format logic a bit, and makes the reference replacement logic more agnostic to what the format is, because all it does is identify references (
'{...}'
) and run a callback for each reference to replace it with something format-specific.In summary I think this restructure of transforms and reference resolution could simplify the developer experience and reduce the amount of bugs/issues we have related to the current structure, but we have to find a way to still allow stacked transitive transforms to work.
Configuring reference resolution on a global and platform level makes sense I think, opening up the outputting of references in the final format to more platforms than just CSS.
Disclaimer: I don't have full context on the history of the token transform/resolve lifecycle or the addition of transitive transforms, so please do let me know if there's something I'm missing here.