Skip to content
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
54 changes: 54 additions & 0 deletions packages/cli/src/linter/model/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,4 +506,58 @@ describe('ModelHandler', () => {
expect(btn?.properties.get('fontWeight')).toBe(600);
});
});

// ── Fix #75: non-string YAML scalars crash model builder ────────────
describe('non-string component property values (Issue #75)', () => {
it('does not crash when a component property is a float (opacity: 0.9)', () => {
const result = handler.execute(makeParsed({
colors: { primary: '#FF0000', 'on-primary': '#FFFFFF' },
components: {
button: {
backgroundColor: '{colors.primary}',
textColor: '{colors.on-primary}',
opacity: 0.9 as unknown as string,
},
},
}));
expect(result.findings.filter(f => f.severity === 'error')).toHaveLength(0);
const btn = result.designSystem.components.get('button');
expect(btn?.properties.get('opacity')).toBe(0.9);
});

it('does not crash when a component property is a boolean (visible: true)', () => {
const result = handler.execute(makeParsed({
components: {
banner: {
visible: true as unknown as string,
},
},
}));
expect(result.findings.filter(f => f.severity === 'error')).toHaveLength(0);
const banner = result.designSystem.components.get('banner');
expect(banner?.properties.get('visible')).toBe(true);
});

it('handles mixed number, boolean, and string props without crashing', () => {
const result = handler.execute(makeParsed({
colors: { primary: '#ff0000' },
components: {
card: {
backgroundColor: '{colors.primary}',
borderRadius: '8px',
fontWeight: 500 as unknown as string,
opacity: 0.85 as unknown as string,
visible: true as unknown as string,
disabled: false 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('fontWeight')).toBe(500);
expect(card?.properties.get('opacity')).toBe(0.85);
expect(card?.properties.get('visible')).toBe(true);
expect(card?.properties.get('disabled')).toBe(false);
});
});
});
12 changes: 6 additions & 6 deletions packages/cli/src/linter/model/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,12 @@ export class ModelHandler implements ModelSpec {
const unresolvedRefs: string[] = [];

for (const [propName, rawValue] of Object.entries(props)) {
// Numeric values (e.g. fontWeight: 600, borderWidth: 1) are valid
// per spec and must be stored as-is. We check for 'number' here,
// but isTokenReference, isValidColor, and isParseableDimension
// are also guarded against other non-string types (like booleans)
// to prevent crashes like "raw.match is not a function".
if (typeof rawValue === 'number') {
// Non-string scalars (numbers, booleans) are valid YAML values
// that can appear in component properties (e.g. fontWeight: 600,
// visible: true, opacity: 0.9). Store them as-is rather than
// passing them to string-only helpers like isTokenReference or
// isValidColor, which would either silently coerce or crash.
if (typeof rawValue === 'number' || typeof rawValue === 'boolean') {
properties.set(propName, rawValue);
} else if (isTokenReference(rawValue)) {
const refPath = rawValue.slice(1, -1);
Expand Down
Loading