mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(daemon): honor export manifest primary refs
This commit is contained in:
parent
871a393917
commit
34ecd800ae
2 changed files with 65 additions and 11 deletions
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue