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

feat: recursively expand nested composite tokens #1244

Merged
merged 3 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/lucky-buckets-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'style-dictionary': minor
---

Some fixes for Expand utility:

- Array values such as `dashArray` property of `strokeStyle` tokens no longer get expanded unintentionally, `typeof 'object'` check changed to `isPlainObject` check.
- Nested object-value tokens (such as `style` property inside `border` tokens) will now also be expanded.
- When references are involved during expansion, the resolved value is used when the property is an object, if not, then we keep the reference as is.
This is because if the reference is to an object value, the expansion might break the reference.
141 changes: 141 additions & 0 deletions __tests__/utils/expandObjectTokens.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,147 @@ describe('utils', () => {
});
});

it('should expand nested composite tokens', () => {
const refInput = {
black: {
value: '#000',
type: 'color',
},
stroke: {
value: {
dashArray: ['0.5rem', '0.25rem'],
lineCap: 'round',
},
type: 'strokeStyle',
},
border: {
value: {
color: '{black}',
width: '3px',
style: '{stroke}',
},
type: 'border',
},
};

const expanded = expandTokens(refInput, {
expand: true,
usesDtcg: false,
});

expect(expanded).to.eql({
black: {
value: '#000',
type: 'color',
},
stroke: {
dashArray: {
value: ['0.5rem', '0.25rem'],
type: 'dimension',
},
lineCap: {
value: 'round',
type: 'lineCap',
},
},
border: {
// color can remain unresolved ref because its resolved value is not an object
color: { value: '{black}', type: 'color' },
width: { value: '3px', type: 'dimension' },
// style must be its resolved value because it is an object and potentially gets expanded,
// breaking the original reference
style: {
dashArray: {
value: ['0.5rem', '0.25rem'],
type: 'dimension',
},
lineCap: {
value: 'round',
type: 'lineCap',
},
},
},
});
});

it('should expand shadow tokens', () => {
const refInput = {
shade: {
type: 'shadow',
value: [
{
offsetX: '2px',
offsetY: '4px',
blur: '2px',
spread: '0',
color: '#000',
},
{
offsetX: '10px',
offsetY: '12px',
blur: '4px',
spread: '3px',
color: '#ccc',
},
],
},
};

const expanded = expandTokens(refInput, {
expand: true,
usesDtcg: false,
});

expect(expanded).to.eql({
shade: {
1: {
offsetX: {
type: 'dimension',
value: '2px',
},
offsetY: {
type: 'dimension',
value: '4px',
},
blur: {
type: 'dimension',
value: '2px',
},
spread: {
type: 'dimension',
value: '0',
},
color: {
type: 'color',
value: '#000',
},
},
2: {
offsetX: {
type: 'dimension',
value: '10px',
},
offsetY: {
type: 'dimension',
value: '12px',
},
blur: {
type: 'dimension',
value: '4px',
},
spread: {
type: 'dimension',
value: '3px',
},
color: {
type: 'color',
value: '#ccc',
},
},
},
});
});

it('should support DTCG format', () => {
const input = {
border: {
Expand Down
34 changes: 25 additions & 9 deletions lib/utils/expandObjectTokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { resolveReferences } from './references/resolveReferences.js';
import usesReferences from './references/usesReferences.js';
import { deepmerge } from './deepmerge.js';
import isPlainObject from 'is-plain-obj';

/**
* @typedef {import('../../types/DesignToken.d.ts').DesignToken} DesignToken
Expand Down Expand Up @@ -53,6 +54,10 @@ export const DTCGTypesMap = {
letterSpacing: 'dimension',
lineHeight: 'number',
},
// https://design-tokens.github.io/community-group/format/#object-value
strokeStyle: {
dashArray: 'dimension',
},
};

/**
Expand Down Expand Up @@ -198,7 +203,7 @@ export function expandToken(token, opts, platform) {
function expandTokensRecurse(slice, original, opts, platform) {
for (const key in slice) {
const token = slice[key];
if (typeof token !== 'object' || token === null) {
if (!isPlainObject(token) || token === null) {
continue;
}
const uses$ = opts.usesDtcg;
Expand All @@ -207,17 +212,28 @@ function expandTokensRecurse(slice, original, opts, platform) {
// if our token is a ref, we have to resolve it first in order to expand its value
if (typeof value === 'string' && usesReferences(value)) {
value = resolveReferences(value, original, { usesDtcg: uses$ });
token[uses$ ? '$value' : 'value'] = value;
}
if (typeof value === 'object' && shouldExpand(token, opts, platform)) {
// TODO: Support nested objects, e.g. a border can have a style prop (strokeStyle) which itself
// can also be an object value with dashArray and lineCap props.
// More info: https://design-tokens.github.io/community-group/format/#example-border-composite-token-examples
slice[key] = expandToken(token, opts, platform);

if (
isPlainObject(value) ||
// support multi-value arrays where each item is an object, e.g. shadow tokens
(Array.isArray(value) && value.every((sub) => isPlainObject(sub)))
) {
// if the resolved value is an object, then we must assume it could get expanded and
// we must set the value to the resolved value, since the reference might be broken after expansion
slice[key][uses$ ? '$value' : 'value'] = value;

if (shouldExpand(token, opts, platform)) {
slice[key] = expandToken(token, opts, platform);
}
}
} else {
expandTokensRecurse(token, original, opts, platform);
}
// We might expect an else statement here on the line above, but we also want
// to recurse if a value is present so that we support expanding nested object values,
// e.g. a border can have a style prop (strokeStyle) which itself
// can also be an object value with dashArray and lineCap props.
// More info: https://design-tokens.github.io/community-group/format/#example-border-composite-token-examples
expandTokensRecurse(slice[key], original, opts, platform);
}
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.