Skip to content
Open
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
39 changes: 39 additions & 0 deletions packages/cli/src/linter/linter/rules/missing-sections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@ describe('missingSections', () => {
expect(missingSections(state)).toEqual([]);
});

it('returns empty for missing sections listed as omitted', () => {
const state = buildState({
omitted: ['spacing', 'rounded'],
colors: { primary: '#ff0000' },
});

expect(missingSections(state)).toEqual([]);
});

it('still reports sections not listed as omitted', () => {
const state = buildState({
omitted: ['spacing'],
colors: { primary: '#ff0000' },
});

const findings = missingSections(state);
expect(findings.map(d => d.path)).toEqual(['rounded']);
});

it('matches omitted section names case-insensitively', () => {
const state = buildState({
omitted: ['Spacing'],
colors: { primary: '#ff0000' },
rounded: { regular: '4px' },
});

expect(missingSections(state)).toEqual([]);
});

it('treats empty omitted lists like no omitted key', () => {
const state = buildState({
omitted: [],
colors: { primary: '#ff0000' },
rounded: { regular: '4px' },
});

expect(missingSections(state).map(d => d.path)).toEqual(['spacing']);
});

it('returns empty when no colors exist (nothing to compare against)', () => {
const state = buildState({});
expect(missingSections(state)).toEqual([]);
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/linter/linter/rules/missing-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ import type { RuleDescriptor, RuleFinding } from './types.js';
*/
export function missingSections(state: DesignSystemState): RuleFinding[] {
const findings: RuleFinding[] = [];
const omitted = new Set((state.omitted ?? []).map(section => section.toLowerCase()));
const sections = [
{ map: state.spacing, name: 'spacing', fallback: 'Layout spacing will fall back to agent defaults.' },
{ map: state.rounded, name: 'rounded', fallback: 'Corner rounding will fall back to agent defaults.' },
];

for (const { map, name, fallback } of sections) {
if (omitted.has(name)) continue;

if (map.size === 0 && state.colors.size > 0) {
findings.push({
path: name,
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/src/linter/linter/rules/missing-typography.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,33 @@ describe('missingTypography', () => {
expect(missingTypography(state)).toEqual([]);
});

it('returns empty when typography is listed as omitted', () => {
const state = buildState({
omitted: ['typography'],
colors: { primary: '#ff0000' },
});

expect(missingTypography(state)).toEqual([]);
});

it('matches omitted typography case-insensitively', () => {
const state = buildState({
omitted: ['Typography'],
colors: { primary: '#ff0000' },
});

expect(missingTypography(state)).toEqual([]);
});

it('treats empty omitted lists like no omitted key', () => {
const state = buildState({
omitted: [],
colors: { primary: '#ff0000' },
});

expect(missingTypography(state).map(d => d.path)).toEqual(['typography']);
});

it('returns empty when no colors defined (nothing to compare against)', () => {
const state = buildState({});
expect(missingTypography(state)).toEqual([]);
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/linter/linter/rules/missing-typography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import type { RuleDescriptor, RuleFinding } from './types.js';
* reducing the author's control over the design system's typographic identity.
*/
export function missingTypography(state: DesignSystemState): RuleFinding[] {
const omitted = new Set((state.omitted ?? []).map(section => section.toLowerCase()));
if (omitted.has('typography')) {
return [];
}

if (state.typography.size === 0 && state.colors.size > 0) {
return [{
path: 'typography',
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/linter/model/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export class ModelHandler implements ModelSpec {
designSystem: {
name: input.name,
description: input.description,
omitted: input.omitted,
colors,
typography,
rounded,
Expand Down Expand Up @@ -438,4 +439,4 @@ function forEachLeaf(
fn(fullPath, value);
}
}
}
}
32 changes: 32 additions & 0 deletions packages/cli/src/linter/model/spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@

import { describe, it, expect } from 'bun:test';
import { isValidColor, isStandardDimension, isParseableDimension, parseDimensionParts, isTokenReference } from './spec.js';
import { ModelHandler } from './handler.js';
import type { ParsedDesignSystem } from '../parser/spec.js';

const handler = new ModelHandler();

function makeParsed(overrides: Partial<ParsedDesignSystem> = {}): ParsedDesignSystem {
return {
sourceMap: new Map(),
...overrides,
};
}

describe('isValidColor', () => {
const validColors = ['#ff0000', '#FF0000', '#abc', '#ABC', '#647D66', '#000', '#fff', 'red', 'blue'];
Expand Down Expand Up @@ -95,3 +106,24 @@ describe('isTokenReference', () => {
expect(isTokenReference('{ colors.primary }')).toBe(false);
});
});

describe('omitted model metadata', () => {
it('passes omitted through to the design system state', () => {
const result = handler.execute(makeParsed({
omitted: ['spacing', 'rounded'],
}));

expect(result.designSystem.omitted).toEqual(['spacing', 'rounded']);
});

it('treats omitted as a known top-level key', () => {
const result = handler.execute(makeParsed({
omitted: ['typography'],
sourceMap: new Map([
['omitted', { line: 1, column: 0, block: 'frontmatter' as const }],
]),
}));

expect(result.designSystem.unknownKeys).toEqual([]);
});
});
1 change: 1 addition & 0 deletions packages/cli/src/linter/model/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const VALID_COMPONENT_SUB_TOKENS = _VALID_COMPONENT_SUB_TOKENS;
export interface DesignSystemState {
name?: string | undefined;
description?: string | undefined;
omitted?: string[] | undefined;
colors: Map<string, ResolvedColor>;
typography: Map<string, ResolvedTypography>;
rounded: Map<string, ResolvedDimension>;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/linter/parser/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class ParserHandler implements ParserSpec {
version: typeof raw['version'] === 'string' ? raw['version'] : undefined,
name: typeof raw['name'] === 'string' ? raw['name'] : undefined,
description: typeof raw['description'] === 'string' ? raw['description'] : undefined,
omitted: Array.isArray(raw['omitted']) ? raw['omitted'].filter(v => typeof v === 'string') : undefined,
colors: raw['colors'] as Record<string, string> | undefined,
typography: raw['typography'] as Record<string, Record<string, string | number>> | undefined,
rounded: raw['rounded'] as Record<string, string> | undefined,
Expand Down
62 changes: 61 additions & 1 deletion packages/cli/src/linter/parser/spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
// limitations under the License.

import { describe, it, expect } from 'bun:test';
import { ParserInputSchema } from './spec.js';
import { ParserHandler } from './handler.js';
import { ParserInputSchema, SCHEMA_KEYS } from './spec.js';

describe('ParserInputSchema', () => {
it('rejects empty content', () => {
Expand All @@ -31,3 +32,62 @@ describe('ParserInputSchema', () => {
expect(result.success).toBe(false);
});
});

describe('SCHEMA_KEYS', () => {
it('includes omitted as a known top-level key', () => {
expect(SCHEMA_KEYS).toContain('omitted');
});
});

describe('omitted frontmatter parsing', () => {
const handler = new ParserHandler();

it('extracts omitted string arrays', () => {
const result = handler.execute({
content: `---
omitted:
- spacing
- rounded
colors:
primary: "#ff0000"
---`,
});

expect(result.success).toBe(true);
if (result.success) {
expect(result.data.omitted).toEqual(['spacing', 'rounded']);
}
});

it('ignores non-array omitted values', () => {
const result = handler.execute({
content: `---
omitted: typography
colors:
primary: "#ff0000"
---`,
});

expect(result.success).toBe(true);
if (result.success) {
expect(result.data.omitted).toBeUndefined();
}
});

it('filters non-string omitted entries', () => {
const result = handler.execute({
content: `---
omitted:
- spacing
- 42
- true
- rounded
---`,
});

expect(result.success).toBe(true);
if (result.success) {
expect(result.data.omitted).toEqual(['spacing', 'rounded']);
}
});
});
2 changes: 2 additions & 0 deletions packages/cli/src/linter/parser/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface ParsedDesignSystem {
version?: string | undefined;
name?: string | undefined;
description?: string | undefined;
omitted?: string[] | undefined;
colors?: Record<string, any> | undefined;
typography?: Record<string, Record<string, any>> | undefined;
rounded?: Record<string, any> | undefined;
Expand All @@ -61,6 +62,7 @@ export const SCHEMA_KEYS = [
'version',
'name',
'description',
'omitted',
'colors',
'typography',
'rounded',
Expand Down