import { createHash } from "node:crypto"; import { mkdir, readFile, rm } 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, jsonSchemaDocumentArtifactSchema, moduleInventoryArtifactSchema, releaseManifestV2ArtifactSchema, runtimeConfigV2ArtifactSchema, } from "./contracts/release-artifacts.ts"; import { buildContractSet } from "./generate-contract-set.ts"; import { assertCiBuildEnvironment, buildDate, } from "./lib/build-environment.ts"; import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.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", ); const moduleInventoryDocument = moduleInventoryArtifactSchema.parse( JSON.parse(moduleInventory), ); const moduleInventoryBytes = `${JSON.stringify(moduleInventoryDocument, null, 2)}\n`; const assetManifestHash = createHash("sha256") .update(viteManifest) .digest("hex"); const moduleInventoryHash = createHash("sha256") .update(moduleInventoryBytes) .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 writeValidatedJsonArtifact({ path: "artifacts/quality/vite-module-inventory.json", schema: moduleInventoryArtifactSchema, value: moduleInventoryDocument, }); await rm("dist/.vite/module-inventory.json"); await writeValidatedJsonArtifact({ path: "dist/config.json", schema: runtimeConfigV2ArtifactSchema, value: runtimeConfig, }); await writeValidatedJsonArtifact({ path: "dist/release-manifest.json", schema: releaseManifestV2ArtifactSchema, value: releaseManifest, }); await writeValidatedJsonArtifact({ path: "dist/runtime-config.schema.json", schema: jsonSchemaDocumentArtifactSchema, value: runtimeConfigJsonSchema, }); await writeValidatedJsonArtifact({ path: "artifacts/release/runtime-config.schema.json", schema: jsonSchemaDocumentArtifactSchema, value: runtimeConfigJsonSchema, }); await writeValidatedJsonArtifact({ path: "artifacts/release/build-manifest.json", schema: buildManifestArtifactSchema, value: manifest, }); 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 isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); }