import { isRecord } from '@ankhorage/utility/object'; import type { WebComponentArtifactFileSystemPort } from '../ports/outbound/WebComponentArtifactFileSystemPort'; export interface SynchronizeWebComponentArtifactsInput { readonly components: readonly string[]; readonly outputDirectory: string; readonly packageRoot: string; readonly packageVersion: string; } export interface SynchronizeWebComponentArtifactsResult { readonly components: readonly string[]; readonly files: readonly string[]; readonly outputDirectory: string; } interface ArtifactDefinition { readonly component: string; readonly files: readonly string[]; } interface WebArtifactCatalog { readonly runtime: { readonly files: readonly string[] }; readonly artifacts: readonly ArtifactDefinition[]; } /*** Reconcile one desired web set from a single published ZORA generation. */ export async function synchronizeWebComponentArtifactsAsync( input: SynchronizeWebComponentArtifactsInput, fileSystem: WebComponentArtifactFileSystemPort, ): Promise { const catalog = await readArtifactCatalogAsync(input.packageRoot, fileSystem); const components = [...new Set(input.components)].sort(); const selected = components.map((component) => { const artifact = catalog.artifacts.find((candidate) => candidate.component === component); if (artifact === undefined) throw new Error(`Unsupported ZORA web component: ${component}`); return artifact; }); const files = [ ...new Set([...catalog.runtime.files, ...selected.flatMap(({ files }) => files)]), ].sort(); const stage = await fileSystem.createStageDirectoryAsync(input.outputDirectory); try { await Promise.all( files.map(async (file) => { const source = await fileSystem.readTextFileAsync( fileSystem.joinPath(input.packageRoot, 'web-dist', file), ); const destination = fileSystem.joinPath(stage, file); await fileSystem.ensureDirectoryAsync(fileSystem.joinPath(stage, parentDirectory(file))); await fileSystem.writeTextFileAsync( destination, `// Generated by @ankhorage/zora v${input.packageVersion}. Do not edit manually.\n${source}`, ); }), ); await fileSystem.writeTextFileAsync( fileSystem.joinPath(stage, 'materialization.json'), `${JSON.stringify( { schemaVersion: 1, owner: '@ankhorage/zora', ownerVersion: input.packageVersion, platform: 'web', components, files: [...files, 'materialization.json'].sort(), }, null, 2, )}\n`, ); await fileSystem.commitStageDirectoryAsync(stage, input.outputDirectory); } catch (error) { await fileSystem.removeStageDirectoryAsync(stage); throw error; } return { components, files: [...files, 'materialization.json'].sort(), outputDirectory: input.outputDirectory, }; } /*** Read and validate the packaged catalog before changing consumer output. */ async function readArtifactCatalogAsync( packageRoot: string, fileSystem: WebComponentArtifactFileSystemPort, ): Promise { const source = await fileSystem.readTextFileAsync( fileSystem.joinPath(packageRoot, 'web-dist', 'manifest.json'), ); const parsed: unknown = JSON.parse(source); if ( !isRecord(parsed) || parsed.schemaVersion !== 2 || !isRecord(parsed.runtime) || !isFileList(parsed.runtime.files) || !Array.isArray(parsed.artifacts) ) { throw new Error('Invalid ZORA web artifact manifest.'); } const artifacts = parsed.artifacts.map((artifact: unknown) => { if ( !isRecord(artifact) || typeof artifact.component !== 'string' || !isFileList(artifact.files) ) { throw new Error('Invalid ZORA web artifact manifest entry.'); } return { component: artifact.component, files: artifact.files }; }); return { runtime: { files: parsed.runtime.files }, artifacts }; } /*** Accept only known, relative generated file paths. */ function isFileList(value: unknown): value is readonly string[] { return ( Array.isArray(value) && value.length > 0 && value.every( (entry) => typeof entry === 'string' && /^(?:runtime|components|chunks|assets)\/[A-Za-z0-9_./-]+$/u.test(entry) && !entry.split('/').includes('..'), ) ); } /*** Resolve the parent path inside the generated directory. */ function parentDirectory(file: string): string { return file.slice(0, file.lastIndexOf('/')); }