feat(design-system): add token schema and resolver
This commit is contained in:
parent
ec13fa8ca2
commit
129282d634
3172
package-lock.json
generated
3172
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,8 @@
|
||||
"workspaces": [
|
||||
"apps/backend",
|
||||
"apps/frontend",
|
||||
"apps/docs"
|
||||
"apps/docs",
|
||||
"packages/design-system"
|
||||
],
|
||||
"scripts": {
|
||||
"dev:backend": "npm run start:dev -w apps/backend",
|
||||
@ -18,6 +19,12 @@
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"dev:docs": "npm run dev -w apps/docs",
|
||||
"build:docs": "npm run build -w apps/docs",
|
||||
"build:design-system": "npm run build -w packages/design-system",
|
||||
"test:design-system": "npm run test -w packages/design-system",
|
||||
"lint:design-system": "npm run lint -w packages/design-system",
|
||||
"storybook": "npm run storybook -w packages/design-system",
|
||||
"build:storybook": "npm run build-storybook -w packages/design-system",
|
||||
"test:storybook": "npm run test:storybook -w packages/design-system",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
30
packages/design-system/src/tokens/resolveToken.test.ts
Normal file
30
packages/design-system/src/tokens/resolveToken.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { resolveToken } from './resolveToken';
|
||||
import type { TokenCollection } from './types';
|
||||
|
||||
describe('resolveToken', () => {
|
||||
it('resolves an alias chain and preserves the declared type', () => {
|
||||
const tokens = {
|
||||
base: { $type: 'color' as const, $value: '#176747' },
|
||||
semantic: { $type: 'color' as const, $value: '{base}' },
|
||||
component: { $type: 'color' as const, $value: '{semantic}' },
|
||||
} satisfies TokenCollection;
|
||||
expect(resolveToken(tokens, 'component')).toEqual({ type: 'color', value: '#176747' });
|
||||
});
|
||||
|
||||
it.each([
|
||||
['missing alias', { a: { $type: 'color', $value: '{missing}' } }, /Unknown token/],
|
||||
[
|
||||
'cycle',
|
||||
{ a: { $type: 'color', $value: '{b}' }, b: { $type: 'color', $value: '{a}' } },
|
||||
/cycle/i,
|
||||
],
|
||||
[
|
||||
'type mismatch',
|
||||
{ a: { $type: 'color', $value: '#fff' }, b: { $type: 'dimension', $value: '{a}' } },
|
||||
/type/i,
|
||||
],
|
||||
] as const)('rejects %s', (_name, tokens, error) => {
|
||||
expect(() => resolveToken(tokens, 'b')).toThrow(error);
|
||||
});
|
||||
});
|
||||
35
packages/design-system/src/tokens/resolveToken.ts
Normal file
35
packages/design-system/src/tokens/resolveToken.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { TokenCollection, ResolvedToken } from './types';
|
||||
|
||||
const aliasPattern = /^\{([^}]+)\}$/;
|
||||
|
||||
export function resolveToken(
|
||||
tokens: TokenCollection,
|
||||
name: string,
|
||||
visited = new Set<string>(),
|
||||
): ResolvedToken {
|
||||
if (visited.has(name)) {
|
||||
throw new Error(`Cycle detected: ${name} has already been visited in this chain`);
|
||||
}
|
||||
|
||||
const def = tokens[name];
|
||||
if (!def) {
|
||||
throw new Error(`Unknown token: ${name}`);
|
||||
}
|
||||
|
||||
const match = aliasPattern.exec(String(def.$value));
|
||||
if (!match) {
|
||||
return { type: def.$type, value: def.$value };
|
||||
}
|
||||
|
||||
const targetName = match[1];
|
||||
visited.add(name);
|
||||
const resolved = resolveToken(tokens, targetName, visited);
|
||||
|
||||
if (resolved.type !== def.$type) {
|
||||
throw new Error(
|
||||
`Type mismatch: token "${name}" has type "${def.$type}" but alias target "${targetName}" has type "${resolved.type}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
12
packages/design-system/src/tokens/types.ts
Normal file
12
packages/design-system/src/tokens/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export type TokenType =
|
||||
| 'color'
|
||||
| 'dimension'
|
||||
| 'fontFamily'
|
||||
| 'fontWeight'
|
||||
| 'duration'
|
||||
| 'cubicBezier'
|
||||
| 'shadow';
|
||||
export type TokenValue = string | number | readonly number[] | readonly string[];
|
||||
export type TokenDefinition = { readonly $type: TokenType; readonly $value: TokenValue };
|
||||
export type TokenCollection = Readonly<Record<string, TokenDefinition>>;
|
||||
export type ResolvedToken = { readonly type: TokenType; readonly value: TokenValue };
|
||||
Loading…
x
Reference in New Issue
Block a user