139 lines
4.0 KiB
JavaScript
139 lines
4.0 KiB
JavaScript
import { z } from "zod";
|
|
|
|
const version = z.string().regex(/^\d+(?:\.\d+){0,2}$/);
|
|
export const releaseManifestSchema = z
|
|
.object({
|
|
schemaVersion: z.literal(1),
|
|
appVersion: z.string().min(1),
|
|
buildId: z.string().min(1),
|
|
commitSha: z.string().min(1),
|
|
configSchemaVersion: version,
|
|
apiContractVersion: version,
|
|
assetManifestHash: z.string().min(1),
|
|
releaseId: z.string().min(1),
|
|
builtAt: z.string().min(1),
|
|
routeChunks: z.record(z.string().min(1), z.string().min(1)),
|
|
})
|
|
.strict();
|
|
|
|
export class ReleaseManifestError extends Error {
|
|
/** @param {string} code @param {{buildId: string, releaseId?: string}} safe */
|
|
constructor(code, safe) {
|
|
super("Release manifest could not be loaded");
|
|
this.name = "ReleaseManifestError";
|
|
this.kind =
|
|
{
|
|
MANIFEST_BUILD_MISMATCH: "BUILD_MISMATCH",
|
|
MANIFEST_CONFIG_SCHEMA_MISMATCH: "CONFIG_MISMATCH",
|
|
MANIFEST_API_CONTRACT_MISMATCH: "API_CONTRACT_MISMATCH",
|
|
MANIFEST_RELEASE_MISMATCH: "RELEASE_MISMATCH",
|
|
MANIFEST_ASSET_MISMATCH: "ASSET_MISMATCH",
|
|
}[code] ?? "RELEASE_MANIFEST_FAILURE";
|
|
this.code = code;
|
|
this.safe = Object.freeze({
|
|
kind: this.kind,
|
|
code,
|
|
buildId: safe.buildId,
|
|
releaseId: safe.releaseId,
|
|
supportReference: `${safe.buildId}:${code}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches and validates the active manifest without imposing the current
|
|
* build tuple. Chunk recovery uses this no-store view to detect a new release.
|
|
*
|
|
* @param {string} url
|
|
* @param {{
|
|
* fetcher?: typeof fetch,
|
|
* buildId: string,
|
|
* releaseId?: string
|
|
* }} options
|
|
*/
|
|
export async function fetchReleaseManifest(url, options) {
|
|
const fetcher = options.fetcher ?? fetch;
|
|
let response;
|
|
try {
|
|
response = await fetcher(url, {
|
|
cache: "no-store",
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
} catch {
|
|
throw new ReleaseManifestError("MANIFEST_FETCH_FAILED", options);
|
|
}
|
|
if (!response.ok) {
|
|
throw new ReleaseManifestError("MANIFEST_HTTP_FAILED", options);
|
|
}
|
|
let raw;
|
|
try {
|
|
raw = await response.json();
|
|
} catch {
|
|
throw new ReleaseManifestError("MANIFEST_JSON_INVALID", options);
|
|
}
|
|
const parsed = releaseManifestSchema.safeParse(raw);
|
|
if (!parsed.success) {
|
|
throw new ReleaseManifestError("MANIFEST_SCHEMA_INVALID", options);
|
|
}
|
|
return Object.freeze(structuredClone(parsed.data));
|
|
}
|
|
|
|
/**
|
|
* @param {Awaited<ReturnType<typeof import("./load-runtime-config.js").loadRuntimeConfig>>} runtime
|
|
* @param {{fetcher?: typeof fetch, expectedAssetManifestHash?: string}} [options]
|
|
*/
|
|
export async function loadReleaseManifest(runtime, options = {}) {
|
|
const manifest = await fetchReleaseManifest(
|
|
runtime.config.RELEASE_MANIFEST_URL,
|
|
{
|
|
fetcher: options.fetcher,
|
|
buildId: runtime.build.buildId,
|
|
releaseId: runtime.config.RELEASE_ID,
|
|
},
|
|
);
|
|
let mismatchCode = null;
|
|
if (manifest.buildId !== runtime.build.buildId) {
|
|
mismatchCode = "MANIFEST_BUILD_MISMATCH";
|
|
}
|
|
if (
|
|
!mismatchCode &&
|
|
runtime.config.BUILD_ID &&
|
|
manifest.buildId !== runtime.config.BUILD_ID
|
|
) {
|
|
mismatchCode = "MANIFEST_BUILD_MISMATCH";
|
|
}
|
|
if (
|
|
!mismatchCode &&
|
|
manifest.configSchemaVersion !== runtime.config.CONFIG_SCHEMA_VERSION
|
|
) {
|
|
mismatchCode = "MANIFEST_CONFIG_SCHEMA_MISMATCH";
|
|
}
|
|
if (
|
|
!mismatchCode &&
|
|
manifest.apiContractVersion !== runtime.config.API_CONTRACT_VERSION
|
|
) {
|
|
mismatchCode = "MANIFEST_API_CONTRACT_MISMATCH";
|
|
}
|
|
if (
|
|
!mismatchCode &&
|
|
runtime.config.RELEASE_ID &&
|
|
manifest.releaseId !== runtime.config.RELEASE_ID
|
|
) {
|
|
mismatchCode = "MANIFEST_RELEASE_MISMATCH";
|
|
}
|
|
if (
|
|
!mismatchCode &&
|
|
options.expectedAssetManifestHash &&
|
|
manifest.assetManifestHash !== options.expectedAssetManifestHash
|
|
) {
|
|
mismatchCode = "MANIFEST_ASSET_MISMATCH";
|
|
}
|
|
if (mismatchCode) {
|
|
throw new ReleaseManifestError(mismatchCode, {
|
|
buildId: runtime.build.buildId,
|
|
releaseId: runtime.config.RELEASE_ID,
|
|
});
|
|
}
|
|
return manifest;
|
|
}
|