import { portableRepoPathError } from "./config-paths.js"; import { REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION, hashRegistryFileContent, registryInstallReceiptSchema, type RegistryArtifact, type RegistryDependency, type RegistryFile, type RegistryInstallReceipt, } from "./registry.js"; export interface RegistryWritePlan { path: string; sourcePath: string; content: string; componentId?: string; } export interface BuildRegistryInstallPlanInput { artifact: RegistryArtifact; requestedComponents: readonly string[]; targetPath: string; all?: boolean; includeTests?: boolean; } export interface RegistryInstallPlan { componentNames: string[]; plans: RegistryWritePlan[]; dependencies: RegistryDependency[]; } export function normalizeRegistryRepoPath(path: string): string { return path .split("\\") .join("/") .replace(/^\.\/+/, ""); } export function assertPortableRegistryPath(path: string, label: string): string { const normalized = normalizeRegistryRepoPath(path); const error = portableRepoPathError(normalized); if (error) throw new Error(`${label}: ${error}`); return normalized; } function joinRegistryPath(...parts: string[]): string { return normalizeRegistryRepoPath(parts.filter(Boolean).join("/")); } export function sourceToRegistryTargetPath(targetRoot: string, sourcePath: string): string { const source = normalizeRegistryRepoPath(sourcePath); const withoutSrc = source.startsWith("src/") ? source.slice("src/".length) : source; return assertPortableRegistryPath( joinRegistryPath(targetRoot, withoutSrc), "Registry install target" ); } export function selectedRegistryComponentNames( artifact: RegistryArtifact, requested: readonly string[], all: boolean | undefined ): string[] { if (all || requested.includes("*")) { return Object.keys(artifact.manifest.components).sort((left, right) => left.localeCompare(right) ); } if (requested.length === 0) { throw new Error("Choose at least one registry component to install."); } for (const name of requested) { if (!artifact.manifest.components[name]) { throw new Error(`Unknown registry component ${name}`); } } return [...new Set(requested)].sort(); } function installsCompleteRegistry( artifact: RegistryArtifact, componentNames: readonly string[] ): boolean { const allComponents = Object.keys(artifact.manifest.components); return ( componentNames.length === allComponents.length && allComponents.every((name) => componentNames.includes(name)) ); } function shouldInstallFile(file: RegistryFile, includeTests: boolean | undefined): boolean { if (includeTests) return true; return file.role !== "test" && file.role !== "story"; } function dependencyKey(dependency: RegistryDependency): string { return `${dependency.type}:${dependency.name}:${dependency.versionRange}:${dependency.optional ? 1 : 0}`; } export function collectRegistryInstallDependencies( artifact: RegistryArtifact, componentNames: readonly string[] ): RegistryDependency[] { const dependencies = new Map(); for (const dependency of artifact.manifest.dependencies) { dependencies.set(dependencyKey(dependency), dependency); } for (const name of componentNames) { const component = artifact.manifest.components[name]; for (const dependency of [...component.runtimeDependencies, ...component.peerDependencies]) { dependencies.set(dependencyKey(dependency), dependency); } } return [...dependencies.values()].sort((left, right) => left.name.localeCompare(right.name)); } function collectComponentWritePlan(args: { artifact: RegistryArtifact; componentNames: readonly string[]; targetPath: string; includeTests?: boolean; }): RegistryWritePlan[] { const writes = new Map(); for (const componentName of args.componentNames) { const component = args.artifact.manifest.components[componentName]; for (const file of component.files) { if (!shouldInstallFile(file, args.includeTests)) continue; const content = args.artifact.files[file.path]; if (!content) throw new Error(`Artifact is missing file content for ${file.path}`); const target = sourceToRegistryTargetPath(args.targetPath, file.path); writes.set(target, { path: target, sourcePath: file.path, content: content.content, componentId: component.componentId, }); } } return [...writes.values()].sort((left, right) => left.path.localeCompare(right.path)); } function rewriteInstalledPackagePath(value: string): string { if (value.startsWith("./src/")) return `./${value.slice("./src/".length)}`; if (value.startsWith("src/")) return value.slice("src/".length); return value; } function rewriteInstalledPackageExport(value: unknown): unknown { if (typeof value === "string") return rewriteInstalledPackagePath(value); if (!value || typeof value !== "object" || Array.isArray(value)) return value; return Object.fromEntries( Object.entries(value).map(([key, nested]) => [key, rewriteInstalledPackageExport(nested)]) ); } function rewriteInstalledPackageJson(content: string): string { const parsed = JSON.parse(content) as { name?: string; version?: string; type?: string; sideEffects?: unknown; main?: string; types?: string; fragments?: string; exports?: unknown; dependencies?: unknown; peerDependencies?: unknown; peerDependenciesMeta?: unknown; }; const installed = { ...(parsed.name && { name: parsed.name }), ...(parsed.version && { version: parsed.version }), private: true, ...(parsed.type && { type: parsed.type }), ...(parsed.sideEffects !== undefined && { sideEffects: parsed.sideEffects }), ...(parsed.main && { main: rewriteInstalledPackagePath(parsed.main) }), ...(parsed.types && { types: rewriteInstalledPackagePath(parsed.types) }), fragments: rewriteInstalledPackagePath(parsed.fragments ?? "fragments.json"), ...(parsed.exports !== undefined && { exports: rewriteInstalledPackageExport(parsed.exports), }), ...(parsed.dependencies !== undefined && { dependencies: parsed.dependencies }), ...(parsed.peerDependencies !== undefined && { peerDependencies: parsed.peerDependencies }), ...(parsed.peerDependenciesMeta !== undefined && { peerDependenciesMeta: parsed.peerDependenciesMeta, }), }; return `${JSON.stringify(installed, null, 2)}\n`; } function collectPackageWritePlan(args: { artifact: RegistryArtifact; targetPath: string; includeTests?: boolean; }): RegistryWritePlan[] { const writes = new Map(); const packageFiles = new Map( args.artifact.manifest.packageFiles.map((file) => [file.path, file]) ); for (const entrypoint of args.artifact.manifest.entrypoints) { const file = packageFiles.get(entrypoint.sourcePath); if (file) continue; const content = args.artifact.files[entrypoint.sourcePath]; if (!content) throw new Error(`Artifact is missing file content for ${entrypoint.sourcePath}`); packageFiles.set(entrypoint.sourcePath, { path: entrypoint.sourcePath, role: "barrel", sha256: content.sha256, size: content.size, contentType: content.contentType, contentRef: `sha256:${content.sha256}`, }); } for (const file of packageFiles.values()) { if (!shouldInstallFile(file, args.includeTests)) continue; const content = args.artifact.files[file.path]; if (!content) throw new Error(`Artifact is missing file content for ${file.path}`); const target = sourceToRegistryTargetPath(args.targetPath, file.path); writes.set(target, { path: target, sourcePath: file.path, content: file.path === "package.json" ? rewriteInstalledPackageJson(content.content) : content.content, }); } return [...writes.values()].sort((left, right) => left.path.localeCompare(right.path)); } function generatedIndexPlan(args: { componentNames: readonly string[]; targetPath: string; artifact: RegistryArtifact; }): RegistryWritePlan { const lines = args.componentNames.map((name) => { const component = args.artifact.manifest.components[name]; const exportName = component.exports[0]?.name ?? component.name; return `export { ${exportName} } from "./components/${component.componentId}";`; }); return { path: assertPortableRegistryPath( joinRegistryPath(args.targetPath, "index.ts"), "Registry index target" ), sourcePath: "registry/generated-index.ts", content: `${lines.join("\n")}\n`, }; } function dedupePlans(plans: readonly RegistryWritePlan[]): RegistryWritePlan[] { const byPath = new Map(); for (const plan of plans) { const existing = byPath.get(plan.path); if (existing && existing.content !== plan.content) { throw new Error(`Conflicting registry write plans for ${plan.path}`); } byPath.set(plan.path, existing ?? plan); } return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path)); } export function buildRegistryInstallPlan( input: BuildRegistryInstallPlanInput ): RegistryInstallPlan { const targetPath = assertPortableRegistryPath(input.targetPath, "Registry target"); const componentNames = selectedRegistryComponentNames( input.artifact, input.requestedComponents, input.all ); const dependencies = collectRegistryInstallDependencies(input.artifact, componentNames); const isCompleteInstall = installsCompleteRegistry(input.artifact, componentNames); const hasPackageRoot = input.artifact.manifest.entrypoints.some( (entrypoint) => entrypoint.specifier === "." ); const plans = dedupePlans([ ...(isCompleteInstall ? collectPackageWritePlan({ artifact: input.artifact, targetPath, includeTests: input.includeTests, }) : []), ...collectComponentWritePlan({ artifact: input.artifact, componentNames, targetPath, includeTests: input.includeTests, }), ...(isCompleteInstall && hasPackageRoot ? [] : [generatedIndexPlan({ artifact: input.artifact, componentNames, targetPath })]), ]); return { componentNames, plans, dependencies }; } export function buildRegistryInstallReceipt(args: { artifact: RegistryArtifact; targetPath: string; importPath: string; componentNames: readonly string[]; plans: readonly RegistryWritePlan[]; dependencies: readonly RegistryDependency[]; installedAt?: string; }): RegistryInstallReceipt { const installedComponents = args.componentNames.map((name) => { const component = args.artifact.manifest.components[name]; return { componentId: component.componentId, publicRef: component.publicRef, name: component.name, files: args.plans .filter((plan) => plan.componentId === component.componentId) .map((plan) => plan.path), }; }); return registryInstallReceiptSchema.parse({ schemaVersion: REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION, registryId: args.artifact.manifest.registryId, registryVersion: args.artifact.manifest.version, registryHash: args.artifact.manifest.registryHash, fcid: args.artifact.manifest.fcid, channel: args.artifact.manifest.channel, targetPath: assertPortableRegistryPath(args.targetPath, "Registry target"), importPath: args.importPath, installedComponents, files: args.plans.map((plan) => ({ path: plan.path, sourcePath: plan.sourcePath, sha256: hashRegistryFileContent(plan.content), componentId: plan.componentId, })), dependencies: args.dependencies, installedAt: args.installedAt ?? new Date().toISOString(), }); }