Skip to content

Commit

Permalink
Merge pull request #206 from tokens-studio/fix-ref-shadow
Browse files Browse the repository at this point in the history
fix: resolve multi-value shadow references for expansion
  • Loading branch information
jorenbroekema authored Oct 10, 2023
2 parents d91d222 + 368922f commit c69d9cd
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 73 deletions.
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 <joren.broekema@gmail.com>",
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

0 comments on commit c69d9cd

Please sign in to comment.