From 368922f8d7e8a6d10aab8059a78be20c17dfe0b0 Mon Sep 17 00:00:00 2001 From: jorenbroekema Date: Tue, 10 Oct 2023 12:11:56 +0200 Subject: [PATCH] fix: resolve multi-value shadow references for expansion --- CHANGELOG.md | 6 +++ package-lock.json | 4 +- package.json | 2 +- src/parsers/add-font-styles.ts | 37 +++---------- src/parsers/expand-composites.ts | 35 +++--------- src/parsers/resolveReference.ts | 52 ++++++++++++++++++ test/integration/expand-composition.test.ts | 27 +++++++++- .../object-value-references.test.ts | 4 +- .../tokens/expand-composition.tokens.json | 4 ++ .../object-value-references.tokens.json | 32 +++++++---- test/spec/parsers/expand.spec.ts | 54 +++++++++++++++++++ 11 files changed, 184 insertions(+), 73 deletions(-) create mode 100644 src/parsers/resolveReference.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b052d..f0b5aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 23f4cc8..c57eeae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tokens-studio/sd-transforms", - "version": "0.11.1", + "version": "0.11.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tokens-studio/sd-transforms", - "version": "0.11.1", + "version": "0.11.5", "license": "MIT", "dependencies": { "@tokens-studio/types": "^0.2.4", diff --git a/package.json b/package.json index cdd19d8..073b4eb 100644 --- a/package.json +++ b/package.json @@ -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 ", diff --git a/src/parsers/add-font-styles.ts b/src/parsers/add-font-styles.ts index 0d0484d..431bc02 100644 --- a/src/parsers/add-font-styles.ts +++ b/src/parsers/add-font-styles.ts @@ -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, - boundGetRef: (ref: string) => Array>, + copy: DeepKeyTokenMap, alwaysAddFontStyle = false, ) { for (const key in slice) { @@ -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; - 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; - } 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) { @@ -61,7 +41,7 @@ function recurse( tokenValue.fontStyle = 'normal'; } } else if (typeof token === 'object') { - recurse(token as unknown as DeepKeyTokenMap, boundGetRef, alwaysAddFontStyle); + recurse(token as unknown as DeepKeyTokenMap, copy, alwaysAddFontStyle); } } } @@ -71,7 +51,6 @@ export function addFontStyles( transformOpts?: TransformOptions, ): DeepKeyTokenMap { const copy = { ...dictionary }; - const boundGetRef = getReferences.bind({ properties: copy }); - recurse(copy, boundGetRef, transformOpts?.alwaysAddFontStyle); + recurse(copy, copy, transformOpts?.alwaysAddFontStyle); return copy; } diff --git a/src/parsers/expand-composites.ts b/src/parsers/expand-composites.ts index 2d1cf6e..754347b 100644 --- a/src/parsers/expand-composites.ts +++ b/src/parsers/expand-composites.ts @@ -1,8 +1,4 @@ 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, @@ -10,6 +6,7 @@ import { ExpandablesAsStrings, expandablesAsStringsArr, } from '../TransformOptions.js'; +import { resolveReference } from './resolveReference.js'; const typeMaps = { boxShadow: { @@ -34,6 +31,9 @@ const typeMaps = { }; export function expandToken(compToken: SingleToken, isShadow = false): SingleToken { + if (typeof compToken.value !== 'object') { + return compToken; + } const expandedObj = {} as SingleToken; const getType = (key: string) => typeMaps[compToken.type][key] ?? key; @@ -74,9 +74,9 @@ function shouldExpand( function recurse( slice: DeepKeyTokenMap, + copy: DeepKeyTokenMap, filePath: string, transformOpts: TransformOptions = {}, - boundGetRef: (ref: string) => Array>, ) { const opts = { ...transformOpts, @@ -102,25 +102,7 @@ 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; - 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; - } catch (e) { - console.warn(`Warning: could not resolve reference ${ref.value}`); - return; - } - } - token.value = ref as SingleToken['value']; - } + token.value = resolveReference(token.value, copy); slice[key] = expandToken(token, expandType === 'shadow'); } } @@ -128,7 +110,7 @@ function recurse( // 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, filePath, transformOpts, boundGetRef); + recurse(token as unknown as DeepKeyTokenMap, copy, filePath, transformOpts); } } } @@ -139,7 +121,6 @@ export function expandComposites( transformOpts?: TransformOptions, ): DeepKeyTokenMap { const copy = { ...dictionary }; - const boundGetRef = getReferences.bind({ properties: copy }); - recurse(copy, filePath, transformOpts, boundGetRef); + recurse(copy, copy, filePath, transformOpts); return copy; } diff --git a/src/parsers/resolveReference.ts b/src/parsers/resolveReference.ts new file mode 100644 index 0000000..b707912 --- /dev/null +++ b/src/parsers/resolveReference.ts @@ -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['value'] | { value: SingleToken['value'] }, +): obj is { value: SingleToken['value'] } { + return Object.prototype.hasOwnProperty.call(obj, 'value'); +} + +function flattenValues['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['value']> | Array<{ value: SingleToken['value'] }>; + +export function resolveReference['value']>( + tokenValue: T, + copy: DeepKeyTokenMap, +): 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; +} diff --git a/test/integration/expand-composition.test.ts b/test/integration/expand-composition.test.ts index 2c3b3d0..29aa479 100644 --- a/test/integration/expand-composition.test.ts +++ b/test/integration/expand-composition.test.ts @@ -130,8 +130,6 @@ describe('expand composition tokens', () => { transformOpts = { expand: { typography: true, - border: true, - shadow: true, }, }; before(); @@ -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)`, + ); + }); }); diff --git a/test/integration/object-value-references.test.ts b/test/integration/object-value-references.test.ts index 048f8fa..2a301e8 100644 --- a/test/integration/object-value-references.test.ts +++ b/test/integration/object-value-references.test.ts @@ -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);`, ); }); diff --git a/test/integration/tokens/expand-composition.tokens.json b/test/integration/tokens/expand-composition.tokens.json index f7bdb65..fd1d13b 100644 --- a/test/integration/tokens/expand-composition.tokens.json +++ b/test/integration/tokens/expand-composition.tokens.json @@ -95,5 +95,9 @@ "deepRef": { "value": "{ref}", "type": "typography" + }, + "deepRefShadowMulti": { + "value": "{shadow.double}", + "type": "boxShadow" } } diff --git a/test/integration/tokens/object-value-references.tokens.json b/test/integration/tokens/object-value-references.tokens.json index 69acc16..50d2a3d 100644 --- a/test/integration/tokens/object-value-references.tokens.json +++ b/test/integration/tokens/object-value-references.tokens.json @@ -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", @@ -52,6 +63,7 @@ "type": "border" }, "borderRef": { - "value": "{border}" + "value": "{border}", + "type": "border" } } diff --git a/test/spec/parsers/expand.spec.ts b/test/spec/parsers/expand.spec.ts index 021d7a8..66f36a3 100644 --- a/test/spec/parsers/expand.spec.ts +++ b/test/spec/parsers/expand.spec.ts @@ -71,6 +71,10 @@ const tokensInput = { ], type: 'boxShadow', }, + ref: { + value: '{shadow.double}', + type: 'boxShadow', + }, }, }; @@ -222,6 +226,56 @@ const tokensOutput = { }, }, }, + ref: { + 1: { + x: { + value: '0', + type: 'dimension', + }, + y: { + value: '4', + type: 'dimension', + }, + blur: { + value: '10', + type: 'dimension', + }, + spread: { + value: '0', + type: 'dimension', + }, + color: { + value: 'rgba(0,0,0,0.4)', + type: 'color', + }, + type: { + value: 'innerShadow', + type: 'other', + }, + }, + 2: { + x: { + value: '0', + type: 'dimension', + }, + y: { + value: '8', + type: 'dimension', + }, + blur: { + value: '12', + type: 'dimension', + }, + spread: { + value: '5', + type: 'dimension', + }, + color: { + value: 'rgba(0,0,0,0.4)', + type: 'color', + }, + }, + }, }, };