mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* Add design system import manifest schema * Generate hybrid design system imports * Read design system usage and cached manifests * Add design system pull-file tool * Show design system package evidence * Wire design system import semantics * Add design system package quality guard --------- Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
218 lines
8.2 KiB
TypeScript
218 lines
8.2 KiB
TypeScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import { importLocalDesignSystemProject } from '../src/design-system-import.js';
|
|
import { listDesignSystems, readDesignSystemAssets } from '../src/design-systems.js';
|
|
|
|
describe('importLocalDesignSystemProject', () => {
|
|
let tempRoot: string;
|
|
let sourceRoot: string;
|
|
let userDesignSystemsRoot: string;
|
|
|
|
beforeEach(() => {
|
|
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'od-ds-import-'));
|
|
sourceRoot = path.join(tempRoot, 'source-app');
|
|
userDesignSystemsRoot = path.join(tempRoot, 'user-design-systems');
|
|
fs.mkdirSync(path.join(sourceRoot, 'src', 'components'), { recursive: true });
|
|
fs.mkdirSync(path.join(sourceRoot, 'src', 'styles'), { recursive: true });
|
|
fs.mkdirSync(path.join(sourceRoot, 'src', 'assets', 'fonts'), { recursive: true });
|
|
fs.mkdirSync(path.join(sourceRoot, 'public'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(sourceRoot, 'package.json'),
|
|
JSON.stringify({
|
|
name: '@acme/kami-app',
|
|
description: 'A focused workspace for AI design reviews.',
|
|
dependencies: { react: '^18.0.0', tailwindcss: '^3.0.0' },
|
|
}),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(sourceRoot, 'README.md'),
|
|
'# Kami App\n\nA calm review surface with crisp cards and bright primary actions.\n',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(sourceRoot, 'src', 'styles', 'tokens.css'),
|
|
':root { --color-primary: #ff3366; --color-background: #101014; --radius-card: 12px; }',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(sourceRoot, 'tailwind.config.ts'),
|
|
'export default { theme: { extend: { colors: {}, fontFamily: {}, borderRadius: {} } } }',
|
|
);
|
|
fs.writeFileSync(path.join(sourceRoot, 'src', 'components', 'Button.tsx'), 'export function Button() {}');
|
|
fs.writeFileSync(path.join(sourceRoot, 'public', 'logo.svg'), '<svg xmlns="http://www.w3.org/2000/svg" />');
|
|
fs.writeFileSync(path.join(sourceRoot, 'src', 'assets', 'fonts', 'AcmeSans-Regular.woff2'), 'font');
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
it('generates a design-system project from a local app directory', async () => {
|
|
const result = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
|
|
now: new Date('2026-05-18T09:00:00.000Z'),
|
|
});
|
|
|
|
expect(result.id).toBe('kami-app');
|
|
expect(result.files).toEqual(
|
|
expect.arrayContaining([
|
|
'USAGE.md',
|
|
'DESIGN.md',
|
|
'tokens.css',
|
|
'components.html',
|
|
'components.manifest.json',
|
|
'manifest.json',
|
|
'assets/logo.svg',
|
|
'fonts/acmesans-regular.woff2',
|
|
'preview/colors.html',
|
|
'preview/typography.html',
|
|
'preview/spacing.html',
|
|
'preview/components-buttons.html',
|
|
'preview/components-inputs.html',
|
|
'preview/app.html',
|
|
'source/scanned-files.json',
|
|
'source/evidence.md',
|
|
'source/tokens.source.json',
|
|
'source/snippets/INDEX.json',
|
|
'source/snippets/button.tsx',
|
|
]),
|
|
);
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(result.dir, 'manifest.json'), 'utf8')) as Record<string, unknown>;
|
|
expect(manifest).toMatchObject({
|
|
schemaVersion: 'od-design-system-project/v1',
|
|
id: 'kami-app',
|
|
name: 'kami app',
|
|
category: 'Imported',
|
|
source: {
|
|
type: 'local',
|
|
path: fs.realpathSync.native(sourceRoot),
|
|
importedAt: '2026-05-18T09:00:00.000Z',
|
|
},
|
|
files: {
|
|
design: 'DESIGN.md',
|
|
tokens: 'tokens.css',
|
|
components: 'components.html',
|
|
},
|
|
usage: 'USAGE.md',
|
|
componentsManifest: 'components.manifest.json',
|
|
importMode: 'hybrid',
|
|
assetsDir: 'assets',
|
|
craft: {
|
|
applies: [],
|
|
suggested: ['color'],
|
|
exemptions: [],
|
|
},
|
|
preview: {
|
|
dir: 'preview',
|
|
},
|
|
sourceFiles: {
|
|
scanned: 'source/scanned-files.json',
|
|
evidence: 'source/evidence.md',
|
|
tokens: 'source/tokens.source.json',
|
|
snippets: 'source/snippets/INDEX.json',
|
|
},
|
|
});
|
|
expect((manifest.preview as { pages: unknown[] }).pages).toHaveLength(6);
|
|
expect(manifest.fonts).toMatchObject([{ family: 'AcmeSans Regular', file: 'fonts/acmesans-regular.woff2' }]);
|
|
|
|
const design = fs.readFileSync(path.join(result.dir, 'DESIGN.md'), 'utf8');
|
|
expect(design).toContain('A focused workspace for AI design reviews.');
|
|
expect(design).toContain('Button: `src/components/Button.tsx`');
|
|
expect(design).toContain('`--color-primary: #ff3366`');
|
|
|
|
const usage = fs.readFileSync(path.join(result.dir, 'USAGE.md'), 'utf8');
|
|
expect(usage).toContain('Auto-generated by Open Design importer');
|
|
expect(usage).toContain('## Read Order');
|
|
expect(usage).toContain('source/tokens.source.json');
|
|
|
|
const componentsManifest = JSON.parse(
|
|
fs.readFileSync(path.join(result.dir, 'components.manifest.json'), 'utf8'),
|
|
) as Record<string, unknown>;
|
|
expect(componentsManifest).toMatchObject({
|
|
schemaVersion: 1,
|
|
brandId: 'kami-app',
|
|
source: {
|
|
componentsHtml: 'components.html',
|
|
tokensCss: 'tokens.css',
|
|
},
|
|
});
|
|
|
|
const scannedFiles = JSON.parse(
|
|
fs.readFileSync(path.join(result.dir, 'source', 'scanned-files.json'), 'utf8'),
|
|
) as { files: Array<{ path: string; kind: string }> };
|
|
expect(scannedFiles.files).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ path: 'src/styles/tokens.css', kind: 'style' }),
|
|
expect.objectContaining({ path: 'src/components/Button.tsx', kind: 'component' }),
|
|
]),
|
|
);
|
|
|
|
const sourceTokens = JSON.parse(
|
|
fs.readFileSync(path.join(result.dir, 'source', 'tokens.source.json'), 'utf8'),
|
|
) as { tokenCount: number; tokens: Array<{ name: string; normalizedRole?: string }> };
|
|
expect(sourceTokens.tokenCount).toBe(3);
|
|
expect(sourceTokens.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ name: '--color-primary', normalizedRole: 'accent' }),
|
|
]),
|
|
);
|
|
|
|
const snippetsIndex = JSON.parse(
|
|
fs.readFileSync(path.join(result.dir, 'source', 'snippets', 'INDEX.json'), 'utf8'),
|
|
) as { snippets: Array<{ path: string; role: string; sourcePath: string }> };
|
|
expect(snippetsIndex.snippets).toEqual([
|
|
expect.objectContaining({
|
|
path: 'source/snippets/button.tsx',
|
|
role: 'button',
|
|
sourcePath: 'src/components/Button.tsx',
|
|
}),
|
|
]);
|
|
expect(fs.readFileSync(path.join(result.dir, 'preview', 'app.html'), 'utf8')).toContain('src/components/Button.tsx');
|
|
|
|
const assets = await readDesignSystemAssets(userDesignSystemsRoot, 'kami-app');
|
|
expect(assets.tokensCss).toContain('--accent: #ff3366;');
|
|
expect(assets.tokensCss).toContain('--bg: #101014;');
|
|
expect(assets.fixtureHtml).toContain('Component fixture');
|
|
|
|
const systems = await listDesignSystems(userDesignSystemsRoot);
|
|
expect(systems).toMatchObject([
|
|
{
|
|
id: 'kami-app',
|
|
title: 'kami app',
|
|
category: 'Imported',
|
|
summary: 'A focused workspace for AI design reviews.',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('allocates a new slug instead of colliding with existing systems', async () => {
|
|
const first = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
|
|
reservedIds: ['kami-app'],
|
|
});
|
|
const second = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
|
|
reservedIds: ['kami-app'],
|
|
});
|
|
|
|
expect(first.id).toBe('kami-app-2');
|
|
expect(second.id).toBe('kami-app-3');
|
|
});
|
|
|
|
it('writes selected importMode and applied craft semantics into manifest', async () => {
|
|
const result = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
|
|
now: new Date('2026-05-18T09:00:00.000Z'),
|
|
importMode: 'verbatim',
|
|
craftApplies: ['color', 'accessibility-baseline', 'color'],
|
|
});
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(result.dir, 'manifest.json'), 'utf8')) as Record<string, unknown>;
|
|
expect(manifest).toMatchObject({
|
|
importMode: 'verbatim',
|
|
craft: {
|
|
applies: ['color', 'accessibility-baseline'],
|
|
suggested: [],
|
|
exemptions: [],
|
|
},
|
|
});
|
|
});
|
|
});
|