From d701c433e604eef127bf00a551a919f268fbdaad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 08:05:30 +0000 Subject: [PATCH] Fix token parsing for Studio edge cases Co-authored-by: Jan Six --- .../src/app/components/utils.test.ts | 16 ++++++ .../src/utils/convertTokens.test.ts | 50 +++++++++++++++++++ .../src/utils/convertTokens.tsx | 27 +++++++--- .../src/utils/is/isSingleToken.ts | 1 + 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/packages/tokens-studio-for-figma/src/app/components/utils.test.ts b/packages/tokens-studio-for-figma/src/app/components/utils.test.ts index 3243f17d8d..3e7055c555 100644 --- a/packages/tokens-studio-for-figma/src/app/components/utils.test.ts +++ b/packages/tokens-studio-for-figma/src/app/components/utils.test.ts @@ -49,6 +49,22 @@ describe('isSingleToken', () => { expect(isSingleToken(wrongToken)).toBe(false); }); + it('rejects token groups with name and value child tokens', () => { + const tokenGroup = { + name: { + type: 'typography', + name: 'included-link.contract.chip.text.label.name', + value: { fontFamily: 'Inter' }, + }, + value: { + type: 'typography', + name: 'included-link.contract.chip.text.label.value', + value: { fontFamily: 'Inter' }, + }, + }; + + expect(isSingleToken(tokenGroup)).toBe(false); + }); }); describe('isSingleTypographyToken', () => { diff --git a/packages/tokens-studio-for-figma/src/utils/convertTokens.test.ts b/packages/tokens-studio-for-figma/src/utils/convertTokens.test.ts index 50e9f5a0c6..97542a680d 100644 --- a/packages/tokens-studio-for-figma/src/utils/convertTokens.test.ts +++ b/packages/tokens-studio-for-figma/src/utils/convertTokens.test.ts @@ -141,4 +141,54 @@ describe('convertToTokenArray', () => { { name: 'global.nestGroupWithType.font.big', value: '24px', type: 'dimension' }, ]); }); + + it('ignores non-string name properties in token JSON', () => { + expect(convertToTokenArray({ + tokens: { + global: { + color: { + primary: { + $value: '#ff0000', + $type: 'color', + name: 123, + }, + }, + }, + }, + })).toEqual([ + { + name: 'global.color.primary', + value: '#ff0000', + type: 'color', + }, + ]); + }); + + it('normalizes new Studio type aliases to plugin token types', () => { + expect(convertToTokenArray({ + tokens: { + global: { + gap: { + $value: '8px', + $type: 'space', + }, + width: { + $value: '16px', + $type: 'size', + }, + }, + }, + })).toEqual([ + { + name: 'global.gap', + value: '8px', + type: 'spacing', + }, + { + name: 'global.width', + value: '16px', + type: 'sizing', + }, + ]); + }); }); diff --git a/packages/tokens-studio-for-figma/src/utils/convertTokens.tsx b/packages/tokens-studio-for-figma/src/utils/convertTokens.tsx index 61354dfbe7..4819b49a2a 100644 --- a/packages/tokens-studio-for-figma/src/utils/convertTokens.tsx +++ b/packages/tokens-studio-for-figma/src/utils/convertTokens.tsx @@ -45,6 +45,17 @@ export type Tokens = $description?: string; }; +function normalizeTokenType(type: unknown): TokenTypes | undefined { + switch (type) { + case 'space': + return TokenTypes.SPACING; + case 'size': + return TokenTypes.SIZING; + default: + return typeof type === 'string' ? type as TokenTypes : undefined; + } +} + // @TODO fix typings function checkForTokens({ obj, @@ -83,16 +94,17 @@ function checkForTokens({ if (isSingleTokenInJSON(token)) { returnValue = token as SingleToken; returnValue.value = token[TokenFormat.tokenValueKey]; + const tokenType = normalizeTokenType(token[TokenFormat.tokenTypeKey]); if (token[TokenFormat.tokenDescriptionKey] && typeof token[TokenFormat.tokenDescriptionKey] === 'string') { returnValue.description = token[TokenFormat.tokenDescriptionKey] as string; } - if (!token[TokenFormat.tokenTypeKey] && inheritType) { + if (!tokenType && inheritType) { returnValue.type = inheritType as TokenTypes; returnValue.inheritTypeLevel = currentTypeLevel as number; } else { - returnValue.type = token[TokenFormat.tokenTypeKey]; - if (inheritType === token[TokenFormat.tokenTypeKey] && currentTypeLevel > 0) { + returnValue.type = tokenType as TokenTypes; + if (inheritType === tokenType && currentTypeLevel > 0) { returnValue.inheritTypeLevel = currentTypeLevel as number; } } @@ -107,14 +119,15 @@ function checkForTokens({ acc[key] = isSingleTokenValueObject(val) && returnValuesOnly ? val[TokenFormat.tokenValueKey] : val; return acc; }, {}); + const tokenType = normalizeTokenType(token[TokenFormat.tokenTypeKey]); if (token[TokenFormat.tokenDescriptionKey] && typeof token[TokenFormat.tokenDescriptionKey] === 'string') { returnValue.description = token[TokenFormat.tokenDescriptionKey] as string; } - if (!token[TokenFormat.tokenTypeKey] && inheritType) { + if (!tokenType && inheritType) { returnValue.type = inheritType as TokenTypes; returnValue.inheritTypeLevel = currentTypeLevel as number; } else { - returnValue.type = token[TokenFormat.tokenTypeKey] as TokenTypes; + returnValue.type = tokenType as TokenTypes; } } else if (typeof token === 'object') { // We dont have a single token value key yet, so it's likely a group which we need to iterate over @@ -124,7 +137,7 @@ function checkForTokens({ // When token groups are typed, we need to inherit the type to their children if (isTokenGroupWithType(token)) { const { [TokenFormat.tokenTypeKey]: groupType, ...tokenValues } = token; - inheritType = groupType as unknown as TokenTypes; + inheritType = normalizeTokenType(groupType); currentTypeLevel = groupLevel; tokenToCheck = tokenValues as Tokens; } @@ -159,7 +172,7 @@ function checkForTokens({ }; } - if (typeof returnValue === 'object' && 'name' in returnValue && returnValue?.name) { + if (typeof returnValue === 'object' && 'name' in returnValue && typeof returnValue?.name === 'string') { returnValue.name = returnValue.name.split('/').join('.'); } diff --git a/packages/tokens-studio-for-figma/src/utils/is/isSingleToken.ts b/packages/tokens-studio-for-figma/src/utils/is/isSingleToken.ts index 36528f0f60..140f989a6f 100644 --- a/packages/tokens-studio-for-figma/src/utils/is/isSingleToken.ts +++ b/packages/tokens-studio-for-figma/src/utils/is/isSingleToken.ts @@ -6,5 +6,6 @@ export function isSingleToken(token: SingleToken | any): token is SingleToken { && typeof token === 'object' && 'value' in token && 'name' in token + && typeof token.name === 'string' ); }