chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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";
|
||||
import { CANONICAL_VITE_MANIFEST_PATH } from "./lib/build-manifest-outputs.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(CANONICAL_VITE_MANIFEST_PATH, "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<string, string> = {};
|
||||
const runtimeContracts: Readonly<Record<string, { moduleId: string }>> =
|
||||
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: CANONICAL_VITE_MANIFEST_PATH,
|
||||
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<Record<string, ViteManifestEntry>> {
|
||||
if (!isRecord(value)) throw new TypeError("Vite manifest must be an object");
|
||||
const entries: Record<string, ViteManifestEntry> = {};
|
||||
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<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
Reference in New Issue
Block a user