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

fix: resolve multi-value shadow references for expansion #206

Merged
merged 1 commit into from
Oct 10, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @tokens-studio/sd-transforms

## 0.11.5

### Patch Changes

- Fix resolve reference for multi-value shadow tokens when expanding shadow tokens.

## 0.11.4

### Patch Changes
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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tokens-studio/sd-transforms",
"version": "0.11.4",
"version": "0.11.5",
"description": "Custom transforms for Style-Dictionary, to work with Design Tokens that are exported from Tokens Studio",
"license": "MIT",
"author": "Joren Broekema <[email protected]>",
Expand Down
37 changes: 8 additions & 29 deletions src/parsers/add-font-styles.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { DeepKeyTokenMap, SingleToken, TokenTypographyValue } from '@tokens-studio/types';
// @ts-expect-error no type exported for this function
import getReferences from 'style-dictionary/lib/utils/references/getReferences.js';
// @ts-expect-error no type exported for this function
import usesReference from 'style-dictionary/lib/utils/references/usesReference.js';
import { DeepKeyTokenMap, TokenTypographyValue } from '@tokens-studio/types';
import { fontWeightReg, fontStyles } from '../transformFontWeights.js';
import { TransformOptions } from '../TransformOptions.js';
import { resolveReference } from './resolveReference.js';

function recurse(
slice: DeepKeyTokenMap<false>,
boundGetRef: (ref: string) => Array<SingleToken<false>>,
copy: DeepKeyTokenMap<false>,
alwaysAddFontStyle = false,
) {
for (const key in slice) {
Expand All @@ -18,28 +15,11 @@ function recurse(
}
const { type, value } = token;
if (type === 'typography') {
if (typeof value !== 'object') {
if (typeof value !== 'object' || value.fontWeight === undefined) {
continue;
}
let fontWeight = value.fontWeight;

if (usesReference(fontWeight)) {
let ref = { value: fontWeight } as SingleToken<false>;
while (ref && ref.value && typeof ref.value === 'string' && usesReference(ref.value)) {
try {
ref = Object.fromEntries(
Object.entries(boundGetRef(ref.value)[0]).map(([k, v]) => [k, v]),
) as SingleToken<false>;
} catch (e) {
console.warn(`Warning: could not resolve reference ${ref.value}`);
return;
}
}
fontWeight = ref.value as string;
}

// cast it to TokenTypographyValue now that we've resolved references all the way, we know it cannot be a string anymore.
// fontStyle is a prop we add ourselves
const fontWeight = resolveReference(value.fontWeight, copy);
// cast because fontStyle is a prop we will add ourselves
const tokenValue = value as TokenTypographyValue & { fontStyle: string };

if (fontWeight) {
Expand All @@ -61,7 +41,7 @@ function recurse(
tokenValue.fontStyle = 'normal';
}
} else if (typeof token === 'object') {
recurse(token as unknown as DeepKeyTokenMap<false>, boundGetRef, alwaysAddFontStyle);
recurse(token as unknown as DeepKeyTokenMap<false>, copy, alwaysAddFontStyle);
}
}
}
Expand All @@ -71,7 +51,6 @@ export function addFontStyles(
transformOpts?: TransformOptions,
): DeepKeyTokenMap<false> {
const copy = { ...dictionary };
const boundGetRef = getReferences.bind({ properties: copy });
recurse(copy, boundGetRef, transformOpts?.alwaysAddFontStyle);
recurse(copy, copy, transformOpts?.alwaysAddFontStyle);
return copy;
}
35 changes: 8 additions & 27 deletions src/parsers/expand-composites.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { DeepKeyTokenMap, SingleToken } from '@tokens-studio/types';
// @ts-expect-error no type exported for this function
import getReferences from 'style-dictionary/lib/utils/references/getReferences.js';
// @ts-expect-error no type exported for this function
import usesReference from 'style-dictionary/lib/utils/references/usesReference.js';
import {
ExpandFilter,
TransformOptions,
Expandables,
ExpandablesAsStrings,
expandablesAsStringsArr,
} from '../TransformOptions.js';
import { resolveReference } from './resolveReference.js';

const typeMaps = {
boxShadow: {
Expand All @@ -34,6 +31,9 @@ const typeMaps = {
};

export function expandToken(compToken: SingleToken<false>, isShadow = false): SingleToken<false> {
if (typeof compToken.value !== 'object') {
return compToken;
}
const expandedObj = {} as SingleToken<false>;

const getType = (key: string) => typeMaps[compToken.type][key] ?? key;
Expand Down Expand Up @@ -74,9 +74,9 @@ function shouldExpand<T extends SingleToken>(

function recurse(
slice: DeepKeyTokenMap<false>,
copy: DeepKeyTokenMap<false>,
filePath: string,
transformOpts: TransformOptions = {},
boundGetRef: (ref: string) => Array<SingleToken<false>>,
) {
const opts = {
...transformOpts,
Expand All @@ -102,33 +102,15 @@ function recurse(
);
if (expand) {
// if token uses a reference, resolve it
if (typeof token.value === 'string' && usesReference(token.value)) {
let ref = { value: token.value } as SingleToken<false>;
while (ref && ref.value && typeof ref.value === 'string' && usesReference(ref.value)) {
// boundGetRef = getReferences() but bound to this style-dictionary object during parsing
// this spits back either { value: '{deepRef}' } if it's a nested reference or
// the object value (typography/composition/border/shadow)
// However, when it's the final resolved value, the props are as { value, type }
// instead of just the value, so we use a map to grab only the value...
try {
ref = Object.fromEntries(
Object.entries(boundGetRef(ref.value)[0]).map(([k, v]) => [k, v.value]),
) as SingleToken<false>;
} catch (e) {
console.warn(`Warning: could not resolve reference ${ref.value}`);
return;
}
}
token.value = ref as SingleToken<false>['value'];
}
token.value = resolveReference(token.value, copy);
slice[key] = expandToken(token, expandType === 'shadow');
}
}
} else if (typeof token === 'object') {
// TODO: figure out why we have to hack this typecast, if a value doesn't have a value & type,
// it is definitely a nested DeepKeyTokenMap and not a SingleToken, but TS seems to think it must be
// a SingleToken after this if statement
recurse(token as unknown as DeepKeyTokenMap<false>, filePath, transformOpts, boundGetRef);
recurse(token as unknown as DeepKeyTokenMap<false>, copy, filePath, transformOpts);
}
}
}
Expand All @@ -139,7 +121,6 @@ export function expandComposites(
transformOpts?: TransformOptions,
): DeepKeyTokenMap<false> {
const copy = { ...dictionary };
const boundGetRef = getReferences.bind({ properties: copy });
recurse(copy, filePath, transformOpts, boundGetRef);
recurse(copy, copy, filePath, transformOpts);
return copy;
}
52 changes: 52 additions & 0 deletions src/parsers/resolveReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DeepKeyTokenMap, SingleToken, TokenBoxshadowValue } from '@tokens-studio/types';
// @ts-expect-error no type exported for this function
import usesReference from 'style-dictionary/lib/utils/references/usesReference.js';
// @ts-expect-error no type exported for this function
import getReferences from 'style-dictionary/lib/utils/references/getReferences.js';

// Type function to determine whether the obj is `tokenValue` or `{ value: tokenValue }`
function isReferenceValue(
obj: SingleToken<false>['value'] | { value: SingleToken<false>['value'] },
): obj is { value: SingleToken<false>['value'] } {
return Object.prototype.hasOwnProperty.call(obj, 'value');
}

function flattenValues<T extends SingleToken<false>['value']>(val: T): T {
return Object.fromEntries(Object.entries(val).map(([k, v]) => [k, v.value])) as T;
}

// first in normal situation, second if it's another nested reference
type boundGetRef = (
ref: string,
) => Array<SingleToken<false>['value']> | Array<{ value: SingleToken<false>['value'] }>;

export function resolveReference<T extends SingleToken<false>['value']>(
tokenValue: T,
copy: DeepKeyTokenMap<false>,
): T {
const boundGetRef = getReferences.bind({ properties: copy }) as boundGetRef;

let ref = tokenValue;
while (ref && typeof ref === 'string' && usesReference(ref)) {
try {
const getRefResult = boundGetRef(ref)[0];

// If every key of the result is a number, the ref value is a multi-value, which means TokenBoxshadowValue[]
if (Object.keys(getRefResult).every(key => !isNaN(Number(key)))) {
ref = Object.values(getRefResult).map((refPart: TokenBoxshadowValue) =>
flattenValues(refPart),
) as T;
} else if (isReferenceValue(getRefResult)) {
// this means it spit back a reference { value: '{deepRef}' }
// and we'll continue the while loop
ref = getRefResult.value as T;
} else {
ref = flattenValues(getRefResult) as T;
}
} catch (e) {
console.warn(`Warning: could not resolve reference ${ref}`);
return ref;
}
}
return ref;
}
27 changes: 25 additions & 2 deletions test/integration/expand-composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ describe('expand composition tokens', () => {
transformOpts = {
expand: {
typography: true,
border: true,
shadow: true,
},
};
before();
Expand Down Expand Up @@ -161,4 +159,29 @@ describe('expand composition tokens', () => {
--sdDeepRefFontStyle: italic;`,
);
});

it('handles references for multi-shadow value', async () => {
transformOpts = {
expand: {
shadow: true,
},
};
before();

const file = await promises.readFile(outputFilePath, 'utf-8');
expect(file).to.include(
`
--sdDeepRefShadowMulti1X: 0;
--sdDeepRefShadowMulti1Y: 4px;
--sdDeepRefShadowMulti1Blur: 10px;
--sdDeepRefShadowMulti1Spread: 0;
--sdDeepRefShadowMulti1Color: rgba(0,0,0,0.4);
--sdDeepRefShadowMulti1Type: innerShadow;
--sdDeepRefShadowMulti2X: 0;
--sdDeepRefShadowMulti2Y: 8px;
--sdDeepRefShadowMulti2Blur: 12px;
--sdDeepRefShadowMulti2Spread: 5px;
--sdDeepRefShadowMulti2Color: rgba(0,0,0,0.4)`,
);
});
});
4 changes: 2 additions & 2 deletions test/integration/object-value-references.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ describe('typography references', () => {
const file = await promises.readFile(outputFilePath, 'utf-8');
expect(file).to.include(
`
--sdShadow: inset 0 4px 10px 0 rgba(0,0,0,0.4);
--sdShadowRef: inset 0 4px 10px 0 rgba(0,0,0,0.4);`,
--sdShadow: 0 4px 10px 0 rgba(0,0,0,0.4), inset 0 8px 10px 4px rgba(0,0,0,0.6);
--sdShadowRef: 0 4px 10px 0 rgba(0,0,0,0.4), inset 0 8px 10px 4px rgba(0,0,0,0.6);`,
);
});

Expand Down
4 changes: 4 additions & 0 deletions test/integration/tokens/expand-composition.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,9 @@
"deepRef": {
"value": "{ref}",
"type": "typography"
},
"deepRefShadowMulti": {
"value": "{shadow.double}",
"type": "boxShadow"
}
}
32 changes: 22 additions & 10 deletions test/integration/tokens/object-value-references.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,29 @@
"type": "typography"
},
"shadow": {
"value": {
"x": "0",
"y": "4",
"blur": "10",
"spread": "0",
"color": "rgba(0,0,0,0.4)",
"type": "innerShadow"
},
"value": [
{
"x": "0",
"y": "4",
"blur": "10",
"spread": "0",
"color": "rgba(0,0,0,0.4)",
"type": "dropShadow"
},
{
"x": "0",
"y": "8",
"blur": "10",
"spread": "4",
"color": "rgba(0,0,0,0.6)",
"type": "innerShadow"
}
],
"type": "boxShadow"
},
"shadowRef": {
"value": "{shadow}"
"value": "{shadow}",
"type": "boxShadow"
},
"fontWeightRef": {
"value": "Regular Italic",
Expand All @@ -52,6 +63,7 @@
"type": "border"
},
"borderRef": {
"value": "{border}"
"value": "{border}",
"type": "border"
}
}
Loading
Loading