import { createHash } from "node:crypto"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import process from "node:process"; import { z } from "zod"; import { ROUTE_REGISTRY, ROUTE_RUNTIME_CONTRACT, } from "../src/features/installed-feature-contracts.ts"; import { buildManifestArtifactSchema, releaseManifestV2ArtifactSchema, runtimeConfigV2ArtifactSchema, } from "../src/contracts/release-artifacts.ts"; import { buildContractSet } from "./generate-contract-set.ts"; import { assertCiBuildEnvironment, buildDate, } from "./lib/build-environment.ts"; assertCiBuildEnvironment(process.env); type ViteManifestEntry = Readonly<{ file: string; name?: string; isDynamicEntry?: boolean; }>; const packageJson = parsePackageMetadata( JSON.parse(await readFile("package.json", "utf8")), ); const packageManagerVersion = packageJson.packageManager.split("@").at(-1); const buildId = process.env.VITE_BUILD_ID ?? "local-build"; const commitSha = process.env.VITE_COMMIT_SHA ?? "local"; const releaseId = process.env.RELEASE_ID ?? "local-release"; const runnerImage = process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`; const buildTime = buildDate(process.env); const builtAt = buildTime.toISOString(); const viteManifest = await readFile("dist/.vite/manifest.json", "utf8"); const viteManifestObject = parseViteManifest(JSON.parse(viteManifest)); const moduleInventory = await readFile( "dist/.vite/module-inventory.json", "utf8", ); parseModuleInventory(JSON.parse(moduleInventory)); const assetManifestHash = createHash("sha256") .update(viteManifest) .digest("hex"); const moduleInventoryHash = createHash("sha256") .update(moduleInventory) .digest("hex"); const runtimeConfig = runtimeConfigV2ArtifactSchema.parse( JSON.parse(await readFile("dist/config.json", "utf8")), ); const routeChunks: Record = {}; const runtimeContracts: Readonly> = ROUTE_RUNTIME_CONTRACT; for (const definition of Object.values(ROUTE_REGISTRY)) { const runtime = runtimeContracts[definition.routeId]; const asset = Object.values(viteManifestObject).find( (entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry, ); if (!runtime || !asset?.file) { throw new Error(`Missing built route chunk: ${definition.routeId}`); } routeChunks[definition.chunkId] = asset.file; } const runtimeConfigJsonSchema = z.toJSONSchema(runtimeConfigV2ArtifactSchema); runtimeConfig.BUILD_ID = buildId; runtimeConfig.RELEASE_ID = releaseId; const manifest = buildManifestArtifactSchema.parse({ schemaVersion: 1, buildId, commitSha, releaseId, moduleInventoryHash, generatedAt: builtAt, buildContext: { nodeVersion: process.version, packageManagerVersion, runnerImage, sourceDateEpoch: process.env.SOURCE_DATE_EPOCH ?? null, }, outputs: { directory: "dist", viteManifest: "dist/.vite/manifest.json", moduleInventory: "artifacts/quality/vite-module-inventory.json", routeChunks, runtimeConfigSchema: "dist/runtime-config.schema.json", }, }); // §5.2 / §24.6 RC-3. The writer emits V2 only; the scalar API contract // version has no writer source left. const releaseManifest = releaseManifestV2ArtifactSchema.parse({ schemaVersion: 2, appVersion: packageJson.version, buildId, commitSha, configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION, assetManifestHash, releaseId, builtAt, routeChunks, contractSet: buildContractSet(), }); await mkdir("artifacts/release", { recursive: true }); await mkdir("artifacts/quality", { recursive: true }); await writeFile( "artifacts/quality/vite-module-inventory.json", moduleInventory, ); await rm("dist/.vite/module-inventory.json"); await writeFile("dist/config.json", `${JSON.stringify(runtimeConfig, null, 2)}\n`); await writeFile( "dist/release-manifest.json", `${JSON.stringify(releaseManifest, null, 2)}\n`, ); await writeFile( "dist/runtime-config.schema.json", `${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`, ); await writeFile( "artifacts/release/runtime-config.schema.json", `${JSON.stringify(runtimeConfigJsonSchema, null, 2)}\n`, ); await writeFile( "artifacts/release/build-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`, ); function parsePackageMetadata(value: unknown): Readonly<{ version: string; packageManager: string; }> { if ( !isRecord(value) || typeof value.version !== "string" || typeof value.packageManager !== "string" ) { throw new TypeError("package.json release metadata is invalid"); } return { version: value.version, packageManager: value.packageManager }; } function parseViteManifest( value: unknown, ): Readonly> { if (!isRecord(value)) throw new TypeError("Vite manifest must be an object"); const entries: Record = {}; for (const [key, candidate] of Object.entries(value)) { if (!isRecord(candidate) || typeof candidate.file !== "string") { throw new TypeError(`Invalid Vite manifest entry: ${key}`); } entries[key] = { file: candidate.file, ...(typeof candidate.name === "string" ? { name: candidate.name } : {}), ...(typeof candidate.isDynamicEntry === "boolean" ? { isDynamicEntry: candidate.isDynamicEntry } : {}), }; } return entries; } function parseModuleInventory(value: unknown): void { if ( !isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.chunks) || value.chunks.some( (chunk) => !isRecord(chunk) || typeof chunk.fileName !== "string" || !Array.isArray(chunk.modules) || chunk.modules.some((moduleId) => typeof moduleId !== "string"), ) ) { throw new TypeError("Vite module inventory is invalid"); } } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }