Files
openclaw/scripts/package-docs-map.mjs
Peter Steinberger 385f1dee16 docs: generate docs map at publish time (#117398)
* docs: generate docs map at publish time

* fix: canonicalize Docker pack entry path

* fix: preserve package lifecycle ownership
2026-08-01 07:01:43 -07:00

128 lines
4.1 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Materializes the generated docs map only while npm assembles a package tarball.
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { renderDocsHeadingMap } from "./docs-list.js";
const DOCS_MAP_PATH = path.join("docs", "docs_map.md");
const RECEIPT_PATH = path.join(".artifacts", "package-docs-map", "receipt.json");
function activePreparationError() {
return Object.assign(
new Error(
`Another package preparation owns ${DOCS_MAP_PATH}; wait for it to finish or run \`node scripts/openclaw-postpack.mjs\` after an interrupted pack.`,
),
{ code: "PACKAGE_DOCS_MAP_ACTIVE" },
);
}
function preparationRestoreError(error, restoreError) {
return new AggregateError(
[error, restoreError],
`Writing ${DOCS_MAP_PATH} failed and its source state could not be restored.`,
{ cause: error },
);
}
function sha256(content) {
return createHash("sha256").update(content).digest("hex");
}
/** Restore the source map stub after prepack replaced it with generated content. */
export async function restorePackageDocsMap(cwd = process.cwd()) {
const receiptPath = path.join(cwd, RECEIPT_PATH);
if (!existsSync(receiptPath)) {
return false;
}
const receipt = JSON.parse(await readFile(receiptPath, "utf8"));
if (
!receipt ||
typeof receipt.generatedSha256 !== "string" ||
!/^[0-9a-f]{64}$/u.test(receipt.generatedSha256) ||
(receipt.original !== null && typeof receipt.original !== "string")
) {
throw new Error(`Invalid package docs-map receipt at ${RECEIPT_PATH}.`);
}
const mapPath = path.join(cwd, DOCS_MAP_PATH);
const current = existsSync(mapPath) ? await readFile(mapPath, "utf8") : null;
if (current === receipt.original) {
await rm(receiptPath, { force: true });
return true;
}
if (current === null || sha256(current) !== receipt.generatedSha256) {
throw new Error(
`Refusing to restore ${DOCS_MAP_PATH} because it changed after prepack generated it.`,
);
}
if (receipt.original === null) {
await rm(mapPath);
} else {
await writeFile(mapPath, receipt.original, "utf8");
}
await rm(receiptPath, { force: true });
return true;
}
/** Generate the shipped docs map for npm while recording the source state for cleanup. */
export async function preparePackageDocsMap(cwd = process.cwd()) {
const receiptPath = path.join(cwd, RECEIPT_PATH);
if (existsSync(receiptPath)) {
throw activePreparationError();
}
const mapPath = path.join(cwd, DOCS_MAP_PATH);
const content = renderDocsHeadingMap(path.join(cwd, "docs"));
const original = existsSync(mapPath) ? await readFile(mapPath, "utf8") : null;
if (original === content) {
return false;
}
await mkdir(path.dirname(receiptPath), { recursive: true });
try {
await writeFile(
receiptPath,
`${JSON.stringify({ generatedSha256: sha256(content), original })}\n`,
{ encoding: "utf8", flag: "wx" },
);
} catch (error) {
if (error?.code === "EEXIST") {
throw activePreparationError();
}
throw error;
}
try {
await writeFile(mapPath, content, "utf8");
} catch (error) {
try {
await restorePackageDocsMap(cwd);
} catch (restoreError) {
throw preparationRestoreError(error, restoreError);
}
throw error;
}
return true;
}
async function main(argv = process.argv.slice(2)) {
if (argv.length !== 1 || (argv[0] !== "prepare" && argv[0] !== "restore")) {
console.error("Usage: node scripts/package-docs-map.mjs <prepare|restore>");
process.exitCode = 1;
return;
}
const changed =
argv[0] === "prepare" ? await preparePackageDocsMap() : await restorePackageDocsMap();
console.error(
changed
? `package-docs-map: ${argv[0] === "prepare" ? "generated" : "removed"} transient docs map.`
: `package-docs-map: no ${argv[0] === "prepare" ? "generation" : "cleanup"} needed.`,
);
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main();
}