fix(daemon): honor export manifest primary refs

This commit is contained in:
Denis Redozubov 2026-05-28 21:33:00 +04:00
parent 871a393917
commit 34ecd800ae
2 changed files with 65 additions and 11 deletions

View file

@ -713,9 +713,19 @@ function buildProjectExportManifestResponse({
note(file.name, 'artifact-manifest'); note(file.name, 'artifact-manifest');
const artifactSupporting = new Set<string>(); const artifactSupporting = new Set<string>();
const addManifestRef = (ref: unknown, reason: string) => { const addManifestRef = (
const normalized = normalizeManifestProjectRef(ref, file.name); ref: unknown,
if (!normalized || !filesByName.has(normalized)) return; reason: string,
options: { preferProjectRoot?: boolean } = {},
) => {
const candidates = options.preferProjectRoot
? [
normalizeManifestProjectRootRef(ref),
normalizeManifestProjectRef(ref, file.name),
]
: [normalizeManifestProjectRef(ref, file.name)];
const normalized = candidates.find((candidate) => candidate && filesByName.has(candidate));
if (!normalized) return;
if (normalized === file.name) return; if (normalized === file.name) return;
supportingNames.add(normalized); supportingNames.add(normalized);
artifactSupporting.add(normalized); artifactSupporting.add(normalized);
@ -723,7 +733,7 @@ function buildProjectExportManifestResponse({
}; };
addManifestRef(manifest.entry, 'artifact-entry'); addManifestRef(manifest.entry, 'artifact-entry');
if (typeof manifest.primary === 'string') { if (typeof manifest.primary === 'string') {
addManifestRef(manifest.primary, 'artifact-primary'); addManifestRef(manifest.primary, 'artifact-primary', { preferProjectRoot: true });
} }
if (Array.isArray(manifest.supportingFiles)) { if (Array.isArray(manifest.supportingFiles)) {
for (const ref of manifest.supportingFiles) { for (const ref of manifest.supportingFiles) {
@ -785,18 +795,26 @@ function chooseExportManifestEntryFile(
? project.metadata.entryFile ? project.metadata.entryFile
: null; : null;
if (metadataEntry && filesByName.has(metadataEntry)) return metadataEntry; if (metadataEntry && filesByName.has(metadataEntry)) return metadataEntry;
const primary = files.find((file) => { for (const file of files) {
const manifest = file.artifactManifest; const manifest = file.artifactManifest;
if (!manifest || typeof manifest !== 'object') return false; if (!manifest || typeof manifest !== 'object') continue;
return manifest.primary === true || manifest.primary === file.name; if (manifest.primary === true) return file.name;
}); if (typeof manifest.primary !== 'string') continue;
if (primary?.name) return primary.name; const rootPrimary = normalizeManifestProjectRootRef(manifest.primary);
if (rootPrimary && filesByName.has(rootPrimary)) return rootPrimary;
const ownerRelativePrimary = normalizeManifestProjectRef(manifest.primary, file.name);
if (ownerRelativePrimary && filesByName.has(ownerRelativePrimary)) return ownerRelativePrimary;
}
return files.find((file) => /(^|\/)index\.html?$/i.test(file.name))?.name return files.find((file) => /(^|\/)index\.html?$/i.test(file.name))?.name
?? files.find((file) => file.kind === 'html')?.name ?? files.find((file) => file.kind === 'html')?.name
?? files[0]?.name ?? files[0]?.name
?? null; ?? null;
} }
function normalizeManifestProjectRootRef(ref: unknown): string | null {
return normalizeManifestProjectRef(ref, '');
}
function normalizeManifestProjectRef(ref: unknown, ownerFile: string): string | null { function normalizeManifestProjectRef(ref: unknown, ownerFile: string): string | null {
if (typeof ref !== 'string' || !ref.trim()) return null; if (typeof ref !== 'string' || !ref.trim()) return null;
const value = ref.trim(); const value = ref.trim();

View file

@ -25,7 +25,9 @@ describe('project export manifest route', () => {
await new Promise<void>((resolve) => server.close(() => resolve())); await new Promise<void>((resolve) => server.close(() => resolve()));
}); });
async function createProject(): Promise<string> { async function createProject(
metadata: Record<string, unknown> = { kind: 'prototype', entryFile: 'index.html' },
): Promise<string> {
const id = `export-manifest-${randomUUID()}`; const id = `export-manifest-${randomUUID()}`;
const response = await fetch(`${baseUrl}/api/projects`, { const response = await fetch(`${baseUrl}/api/projects`, {
method: 'POST', method: 'POST',
@ -33,7 +35,7 @@ describe('project export manifest route', () => {
body: JSON.stringify({ body: JSON.stringify({
id, id,
name: 'Export manifest project', name: 'Export manifest project',
metadata: { kind: 'prototype', entryFile: 'index.html' }, metadata,
}), }),
}); });
expect(response.ok).toBe(true); expect(response.ok).toBe(true);
@ -115,6 +117,40 @@ describe('project export manifest route', () => {
expect(body.files.some((file) => file.name.endsWith('.artifact.json'))).toBe(false); expect(body.files.some((file) => file.name.endsWith('.artifact.json'))).toBe(false);
}); });
it('uses artifact primary strings as project-relative entry refs', async () => {
const projectId = await createProject({ kind: 'prototype' });
await writeFile(projectId, {
name: 'reviewed.html',
content: '<!doctype html><main>reviewed</main>',
});
await writeFile(projectId, {
name: 'preview/wrapper.html',
content: '<!doctype html><iframe src="../reviewed.html"></iframe>',
artifactManifest: {
version: 1,
kind: 'html',
title: 'Review wrapper',
renderer: 'html',
status: 'complete',
exports: ['html'],
primary: 'reviewed.html',
},
});
const response = await fetch(`${baseUrl}/api/projects/${projectId}/export/manifest`);
expect(response.ok).toBe(true);
const body = await response.json() as {
entryFile: string;
files: Array<{ name: string; role: string; reasons: string[] }>;
};
expect(body.entryFile).toBe('reviewed.html');
expect(body.files.find((file) => file.name === 'reviewed.html')).toMatchObject({
role: 'entry',
reasons: expect.arrayContaining(['artifact-primary', 'project-entry-file']),
});
});
it('rejects invalid project ids before listing files', async () => { it('rejects invalid project ids before listing files', async () => {
const response = await fetch(`${baseUrl}/api/projects/bad:id/export/manifest`); const response = await fetch(`${baseUrl}/api/projects/bad:id/export/manifest`);
expect(response.status).toBe(400); expect(response.status).toBe(400);