Skip to content

Commit 81b20dc

Browse files
fix(web): avoid missing design-system preview assets (#5381)
Co-authored-by: lefarcen <935902669@qq.com>
1 parent 6469a3e commit 81b20dc

6 files changed

Lines changed: 218 additions & 9 deletions

File tree

apps/daemon/src/design-systems/index.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export type DesignSystemStaticFileDetail = {
9090

9191
export type DesignSystemPackageInfo = {
9292
manifest?: DesignSystemProjectManifest;
93+
availableFiles?: string[];
9394
sourceEvidence?: {
9495
scannedFileCount?: number;
9596
tokenCount?: number;
@@ -404,12 +405,49 @@ export async function readDesignSystemPackageInfo(
404405
if (manifest === null) return null;
405406

406407
const sourceEvidence = await readDesignSystemSourceEvidence(brandRoot, manifest);
408+
const availableFiles = await listAvailableDesignSystemPackageFiles(brandRoot, manifest);
407409
return {
408410
manifest,
411+
...(availableFiles.length > 0 ? { availableFiles } : {}),
409412
...(sourceEvidence ? { sourceEvidence } : {}),
410413
};
411414
}
412415

416+
async function listAvailableDesignSystemPackageFiles(
417+
brandRoot: string,
418+
manifest: DesignSystemProjectManifest,
419+
): Promise<string[]> {
420+
const candidates = new Set<string>(DESIGN_SYSTEM_STATIC_SYSTEM_FILES);
421+
const add = (filePath: string | undefined): void => {
422+
const cleanPath = typeof filePath === 'string' ? sanitizeRelativeFilePath(filePath) : null;
423+
if (cleanPath) candidates.add(cleanPath);
424+
};
425+
426+
add(manifest.files.design);
427+
add(manifest.files.tokens);
428+
add(manifest.files.components);
429+
add(manifest.files.designTokens);
430+
add(manifest.files.tailwind);
431+
add(manifest.usage);
432+
add(manifest.componentsManifest);
433+
for (const page of manifest.preview?.pages ?? []) add(page.path);
434+
for (const font of manifest.fonts ?? []) add(font.file);
435+
436+
const out: string[] = [];
437+
const resolvedRoot = path.resolve(brandRoot);
438+
for (const relativePath of Array.from(candidates).sort()) {
439+
const filePath = path.resolve(brandRoot, relativePath);
440+
if (filePath !== resolvedRoot && !filePath.startsWith(`${resolvedRoot}${path.sep}`)) continue;
441+
try {
442+
const stats = await stat(filePath);
443+
if (stats.isFile()) out.push(relativePath);
444+
} catch (err) {
445+
if (!isAbsenceError(err)) throw err;
446+
}
447+
}
448+
return out;
449+
}
450+
413451
/**
414452
* Structured (compiled) form of a brand's design system. Optional sibling
415453
* files alongside DESIGN.md that, when present, give agents a

apps/daemon/tests/design-systems/import.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,14 @@ describe('importLocalDesignSystemProject', () => {
232232

233233
const packageInfo = await readDesignSystemPackageInfo(userDesignSystemsRoot, 'kami-app');
234234
expect(packageInfo?.manifest?.sourceFiles?.report).toBe('source/token-contract.report.json');
235+
expect(packageInfo?.availableFiles).toEqual(
236+
expect.arrayContaining([
237+
'components.html',
238+
'preview/app.html',
239+
'preview/colors.html',
240+
]),
241+
);
242+
expect(packageInfo?.availableFiles).not.toContain('system/kit.html');
235243
expect(packageInfo?.sourceEvidence?.tokenContract).toMatchObject({
236244
contract: 'TOKEN_SCHEMA',
237245
selfCheckOk: true,

apps/web/src/components/DesignKitView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1583,7 +1583,7 @@ function DesignKitViewInner({
15831583
</button>
15841584
) : null}
15851585
</div>
1586-
<span className={styles.dsCap}>system/kit.html</span>
1586+
<span className={styles.dsCap}>{kit.system.kitLabel ?? 'system/kit.html'}</span>
15871587
</div>
15881588
<iframe
15891589
key={dsKitUrl}

apps/web/src/runtime/design-kit.ts

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface KitSystem {
8888
kitDarkUrl?: string;
8989
tokensUrl?: string;
9090
indexUrl?: string;
91+
kitLabel?: string;
9192
}
9293

9394
export interface KitAsset {
@@ -134,6 +135,72 @@ const ASSET_TILES: { kind: string; label: string; file: string }[] = [
134135
{ kind: 'form', label: 'Form page', file: 'system/artifacts/form.html' },
135136
];
136137

138+
function hasAvailablePackageFile(
139+
packageInfo: DesignSystemPackageInfo | undefined,
140+
filePath: string | undefined,
141+
): filePath is string {
142+
if (!filePath) return false;
143+
const availableFiles = packageInfo?.availableFiles;
144+
return Array.isArray(availableFiles) ? availableFiles.includes(filePath) : true;
145+
}
146+
147+
function firstAvailablePackageFile(
148+
packageInfo: DesignSystemPackageInfo | undefined,
149+
files: Array<string | undefined>,
150+
): string | null {
151+
return files.find((filePath) => hasAvailablePackageFile(packageInfo, filePath)) ?? null;
152+
}
153+
154+
function previewPageLabel(pathName: string, title: string | undefined, role: string | undefined): string {
155+
const explicit = title?.trim() || role?.trim();
156+
if (explicit) return explicit;
157+
const base = pathName.split('/').pop()?.replace(/\.[^.]+$/, '') ?? pathName;
158+
return base
159+
.split(/[-_]+/)
160+
.filter(Boolean)
161+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
162+
.join(' ') || 'Preview';
163+
}
164+
165+
type PackagePreviewPageInput = {
166+
path?: string;
167+
role?: string;
168+
title?: string;
169+
};
170+
type PackagePreviewPage = PackagePreviewPageInput & { path: string };
171+
172+
function isAvailablePreviewPage(
173+
packageInfo: DesignSystemPackageInfo,
174+
page: PackagePreviewPageInput,
175+
): page is PackagePreviewPage {
176+
return typeof page.path === 'string' && hasAvailablePackageFile(packageInfo, page.path);
177+
}
178+
179+
function packageAssetTiles(
180+
packageInfo: DesignSystemPackageInfo | undefined,
181+
staticUrl: (rel: string) => string,
182+
): KitAsset[] | undefined {
183+
if (!packageInfo) return undefined;
184+
const artifactTiles = ASSET_TILES
185+
.filter((a) => hasAvailablePackageFile(packageInfo, a.file))
186+
.map((a) => ({
187+
kind: a.kind,
188+
label: a.label,
189+
url: staticUrl(a.file),
190+
}));
191+
if (artifactTiles.length > 0) return artifactTiles;
192+
193+
const previewPages = packageInfo.manifest?.preview?.pages ?? [];
194+
const previewTiles = previewPages
195+
.filter((page): page is PackagePreviewPage => isAvailablePreviewPage(packageInfo, page))
196+
.map((page) => ({
197+
kind: page.role?.trim() || page.path,
198+
label: previewPageLabel(page.path, page.title, page.role),
199+
url: staticUrl(page.path),
200+
}));
201+
return previewTiles.length > 0 ? previewTiles : undefined;
202+
}
203+
137204
function fontList(typography: DesignKit['typography']): KitFont[] {
138205
return [typography.display, typography.body, typography.mono].filter(
139206
(f): f is KitFont => Boolean(f),
@@ -453,6 +520,25 @@ export function parsedToKit(parsed: ParsedDesignMd, opts: ParsedKitOptions): Des
453520
const staticUrl = !opts.editable && opts.designSystemId && opts.packageInfo?.manifest
454521
? (rel: string): string => designSystemStaticUrl(opts.designSystemId!, rel)
455522
: null;
523+
const manifestFiles = opts.packageInfo?.manifest?.files;
524+
const kitPath = staticUrl
525+
? firstAvailablePackageFile(opts.packageInfo, [
526+
'system/kit.html',
527+
manifestFiles?.components ?? 'components.html',
528+
])
529+
: null;
530+
const kitDarkPath = staticUrl
531+
? firstAvailablePackageFile(opts.packageInfo, ['system/kit.dark.html'])
532+
: null;
533+
const tokensPath = staticUrl
534+
? firstAvailablePackageFile(opts.packageInfo, [
535+
'system/tokens.default.json',
536+
manifestFiles?.designTokens,
537+
])
538+
: null;
539+
const indexPath = staticUrl
540+
? firstAvailablePackageFile(opts.packageInfo, ['system/index.html'])
541+
: null;
456542

457543
return {
458544
designSystemId: opts.designSystemId,
@@ -471,17 +557,16 @@ export function parsedToKit(parsed: ParsedDesignMd, opts: ParsedKitOptions): Des
471557
voice: hasVoice(parsed.voice) ? parsed.voice : undefined,
472558
imagery: hasImagery(imagery) ? imagery : undefined,
473559
layout: hasLayout(layout) ? layout : undefined,
474-
system: staticUrl
560+
system: staticUrl && kitPath
475561
? {
476-
kitUrl: staticUrl('system/kit.html'),
477-
kitDarkUrl: staticUrl('system/kit.dark.html'),
478-
tokensUrl: staticUrl('system/tokens.default.json'),
479-
indexUrl: staticUrl('system/index.html'),
562+
kitUrl: staticUrl(kitPath),
563+
...(kitDarkPath ? { kitDarkUrl: staticUrl(kitDarkPath) } : {}),
564+
...(tokensPath ? { tokensUrl: staticUrl(tokensPath) } : {}),
565+
...(indexPath ? { indexUrl: staticUrl(indexPath) } : {}),
566+
kitLabel: kitPath,
480567
}
481568
: undefined,
482-
assets: staticUrl
483-
? ASSET_TILES.map((a) => ({ kind: a.kind, label: a.label, url: staticUrl(a.file) }))
484-
: undefined,
569+
assets: staticUrl ? packageAssetTiles(opts.packageInfo, staticUrl) : undefined,
485570
showcaseHtml: opts.showcaseHtml ?? null,
486571
};
487572
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { parseDesignMd } from '../../src/runtime/design-md-parse';
4+
import { parsedToKit } from '../../src/runtime/design-kit';
5+
6+
describe('parsedToKit package static assets', () => {
7+
it('falls back to declared components and omits missing artifacts when packaged system files are absent', () => {
8+
const kit = parsedToKit(parseDesignMd('# Tom Modern Design'), {
9+
designSystemId: 'tom-modern',
10+
editable: false,
11+
packageInfo: {
12+
availableFiles: [
13+
'DESIGN.md',
14+
'tokens.css',
15+
'components.html',
16+
],
17+
manifest: {
18+
schemaVersion: 'od-design-system-project/v1',
19+
id: 'tom-modern',
20+
name: 'Tom Modern Design',
21+
category: 'Starter',
22+
files: {
23+
design: 'DESIGN.md',
24+
tokens: 'tokens.css',
25+
components: 'components.html',
26+
},
27+
},
28+
},
29+
});
30+
31+
expect(kit.system).toMatchObject({
32+
kitUrl: '/api/design-systems/tom-modern/static?path=components.html',
33+
kitLabel: 'components.html',
34+
});
35+
expect(kit.system?.kitDarkUrl).toBeUndefined();
36+
expect(kit.assets).toBeUndefined();
37+
});
38+
39+
it('keeps generated system kit and artifact URLs when the package includes them', () => {
40+
const kit = parsedToKit(parseDesignMd('# Bento'), {
41+
designSystemId: 'bento',
42+
editable: false,
43+
packageInfo: {
44+
availableFiles: [
45+
'system/kit.html',
46+
'system/kit.dark.html',
47+
'system/artifacts/landing.html',
48+
],
49+
manifest: {
50+
schemaVersion: 'od-design-system-project/v1',
51+
id: 'bento',
52+
name: 'Bento',
53+
category: 'Layout',
54+
files: {
55+
design: 'DESIGN.md',
56+
tokens: 'tokens.css',
57+
components: 'components.html',
58+
},
59+
},
60+
},
61+
});
62+
63+
expect(kit.system).toMatchObject({
64+
kitUrl: '/api/design-systems/bento/static?path=system%2Fkit.html',
65+
kitDarkUrl: '/api/design-systems/bento/static?path=system%2Fkit.dark.html',
66+
kitLabel: 'system/kit.html',
67+
});
68+
expect(kit.assets).toEqual([
69+
{
70+
kind: 'landing',
71+
label: 'Landing page',
72+
url: '/api/design-systems/bento/static?path=system%2Fartifacts%2Flanding.html',
73+
},
74+
]);
75+
});
76+
});

packages/contracts/src/api/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ export interface DesignSystemPackageInfo {
332332
};
333333
assetsDir?: string;
334334
};
335+
/** Package-relative files the daemon confirmed exist and can be served via /static. */
336+
availableFiles?: string[];
335337
sourceEvidence?: {
336338
scannedFileCount?: number;
337339
tokenCount?: number;

0 commit comments

Comments
 (0)