From 231e0be6e08ca39fd8c6ce546051d5df082a6798 Mon Sep 17 00:00:00 2001 From: tejas100 Date: Thu, 23 Apr 2026 23:16:01 -0400 Subject: [PATCH] fix(model): handle numeric component prop values without crashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Numeric values such as fontWeight: 600 or borderWidth: 1 are valid per the DESIGN.md spec, which states bare numbers and quoted strings are equivalent. However, the component property loop in ModelHandler passed all rawValues directly to isTokenReference / isValidColor, both of which call .match() and crash when rawValue is a number. Fix: add a typeof rawValue === 'number' guard at the top of the loop, storing numeric values as-is — matching the pattern already used by parseTypography for the same property. Fixes #42 --- packages/cli/src/linter/model/handler.test.ts | 62 +++++++++++++++++++ packages/cli/src/linter/model/handler.ts | 8 ++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/linter/model/handler.test.ts b/packages/cli/src/linter/model/handler.test.ts index 2b645155..c9dddfa4 100644 --- a/packages/cli/src/linter/model/handler.test.ts +++ b/packages/cli/src/linter/model/handler.test.ts @@ -356,4 +356,66 @@ describe('ModelHandler', () => { } }); }); + + // ── Fix #42: numeric component props crash model builder ────────── + describe('numeric component property values', () => { + it('does not crash when fontWeight is a bare number', () => { + const result = handler.execute(makeParsed({ + colors: { primary: '#000000' }, + components: { + 'button-primary': { + backgroundColor: '{colors.primary}', + fontWeight: 600 as unknown as string, + }, + }, + })); + expect(result.findings.filter(f => f.severity === 'error')).toHaveLength(0); + const btn = result.designSystem.components.get('button-primary'); + expect(btn).toBeDefined(); + expect(btn?.properties.get('fontWeight')).toBe(600); + }); + + it('stores numeric fontWeight value as-is in component properties', () => { + const result = handler.execute(makeParsed({ + components: { + 'heading': { + fontWeight: 700 as unknown as string, + }, + }, + })); + const heading = result.designSystem.components.get('heading'); + expect(heading?.properties.get('fontWeight')).toBe(700); + }); + + it('does not crash when borderWidth is a bare number', () => { + const result = handler.execute(makeParsed({ + components: { + 'card': { + borderWidth: 1 as unknown as string, + }, + }, + })); + expect(result.findings.filter(f => f.severity === 'error')).toHaveLength(0); + const card = result.designSystem.components.get('card'); + expect(card?.properties.get('borderWidth')).toBe(1); + }); + + it('handles mixed numeric and string props in same component without crashing', () => { + const result = handler.execute(makeParsed({ + colors: { primary: '#ff0000' }, + spacing: { md: '16px' }, + components: { + 'button': { + fontWeight: 600 as unknown as string, + backgroundColor: '{colors.primary}', + padding: '{spacing.md}', + borderRadius: '4px', + }, + }, + })); + expect(result.findings.filter(f => f.severity === 'error')).toHaveLength(0); + const btn = result.designSystem.components.get('button'); + expect(btn?.properties.get('fontWeight')).toBe(600); + }); + }); }); \ No newline at end of file diff --git a/packages/cli/src/linter/model/handler.ts b/packages/cli/src/linter/model/handler.ts index f801a250..d75de278 100644 --- a/packages/cli/src/linter/model/handler.ts +++ b/packages/cli/src/linter/model/handler.ts @@ -176,7 +176,13 @@ export class ModelHandler implements ModelSpec { const unresolvedRefs: string[] = []; for (const [propName, rawValue] of Object.entries(props)) { - if (isTokenReference(rawValue)) { + // Numeric values (e.g. fontWeight: 600, borderWidth: 1) are valid + // per spec and must be stored as-is, coercing to string first + // would cause isTokenReference / isValidColor to call .match() on + // a number and crash with "raw.match is not a function". + if (typeof rawValue === 'number') { + properties.set(propName, rawValue); + } else if (isTokenReference(rawValue)) { const refPath = rawValue.slice(1, -1); const resolved = resolveReference(symbolTable, refPath, new Set()); if (resolved !== null) {