Files
suaveplan/packages/api-stack/api/wasm-compiler-javy/src/manifest/manifest.ts
2026-05-27 23:29:53 +01:00

86 lines
3.1 KiB
TypeScript

import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import type { SerializedRoute } from "@suaveplan/api-types/sandbox-bridge";
import {
WASM_COMPILER_CODES,
WasmCompilerError,
} from "../error-codes/error-codes.js";
/** Inputs for {@link writeManifest}. */
export interface WriteManifestOptions {
/** Absolute path the manifest will be written to. */
readonly manifestPath: string;
/** Routes captured by the route-extract probe. */
readonly routes: readonly SerializedRoute[];
/** Pre-Javy bundled JS source — hashed into `sourceDigest`. */
readonly bundledJs: string;
/** Upstream Javy version pinned by the wrapper that compiled. */
readonly javyVersion: string;
/** This compiler's package version. */
readonly compilerVersion: string;
/** RFC 3339 UTC timestamp; defaults to `new Date().toISOString()`. */
readonly compiledAt?: string;
/**
* Caller-supplied filesystem writer — defaults to
* `node:fs/promises#writeFile`. Tests inject a recorder.
*/
readonly fsWrite?: (path: string, data: string) => Promise<void>;
}
/** The full manifest record shape. */
export interface CompiledPluginManifest {
readonly routes: readonly SerializedRoute[];
readonly abiVersion: string;
readonly abiVariant: "wasi-stdio";
readonly javyVersion: string;
readonly compilerVersion: string;
readonly compiledAt: string;
readonly sourceDigest: string;
}
/** The current compiler ABI version — semver string. */
export const COMPILER_ABI_VERSION = "1.0.0" as const;
/**
* Emit the `<basename>.wasm.json` sidecar. Refuses empty `routes`
* with `WASM_COMPILER.MANIFEST_MISSING_ROUTES` — a routeless plugin
* cannot route a single request through the bridge, so shipping the
* artifact would be pointless.
*
* Returns the manifest record so the orchestrator can include it in
* the compile result without re-reading from disk.
*/
export async function writeManifest(
options: WriteManifestOptions
): Promise<CompiledPluginManifest> {
if (options.routes.length === 0) {
throw new WasmCompilerError(
WASM_COMPILER_CODES.MANIFEST_MISSING_ROUTES,
"error.wasm_compiler.manifest_missing_routes",
{
context: { manifestPath: options.manifestPath },
}
);
}
const sourceDigest = createHash("sha256")
.update(options.bundledJs)
.digest("hex");
const compiledAt = options.compiledAt ?? new Date().toISOString();
const manifest: CompiledPluginManifest = {
routes: options.routes,
abiVersion: COMPILER_ABI_VERSION,
abiVariant: "wasi-stdio",
javyVersion: options.javyVersion,
compilerVersion: options.compilerVersion,
compiledAt,
sourceDigest,
};
const writer = options.fsWrite ?? defaultWriter;
await writer(options.manifestPath, JSON.stringify(manifest, null, 2));
return manifest;
}
async function defaultWriter(path: string, data: string): Promise<void> {
await writeFile(path, data, "utf8");
}