openclaw/scripts/lib/runtime-dependency-ownership-build-plugin.mts
Peter Steinberger 6d9e21892e
fix(nodes): bootstrap source builds with private QA tooling (#149926)
* fix(nodes): bootstrap source builds with private QA tooling

Keep private QA chunks out of node bootstrap archives only when their current
bytes match the shared ownership record and no retained runtime requires them.
Preserve root URL and bound createRequire imports, verify copied bytes, and
keep package inventory and updater selection unchanged.

Move the existing ownership reader, import parser and private plugin set to
runtime-safe owners with all tooling callers migrated. Cover archive creation,
root scripts, dependency scopes and package verification; retain the existing
source-build bootstrap contract.

* test(build): stage shared sources for declaration fixtures

Copy the relocated build configuration owners into the synthetic project and
resolve record-coerce through its existing local source. Keep the package
forwarder relative so cache relocation stays stable and generator imports do
not depend on the compiler configuration under test.

Preserve the original declaration assertions, including invalid-config
failures and cleanup ownership checks.

* fix(build): keep ownership metadata readable by cold tooling

Import the existing normalization source directly so native run-node,
updater, and packaging entrypoints do not need workspace package resolution
before their bootstrap logic can run.

Copy the shared policy source into the two remaining concrete packaging
fixtures and remove the declaration fixture package forwarder made obsolete
by the native-readable owner. Preserve existing assertions and timeouts.
2026-09-16 04:08:49 -07:00

59 lines
2.4 KiB
TypeScript

import { createHash } from "node:crypto";
import path from "node:path";
import type { TsdownPlugin } from "tsdown";
import {
RUNTIME_DEPENDENCY_OWNERSHIP_ASSET_NAME,
type RuntimeDependencyOwnership,
} from "../../src/infra/runtime-dependency-ownership.ts";
export function createRuntimeDependencyOwnershipBuildPlugin(rootDir = process.cwd()): TsdownPlugin {
return {
name: "openclaw:runtime-dependency-ownership",
generateBundle: {
order: "post",
handler(_options, bundle) {
const chunks = Object.values(bundle).filter((output) => output.type === "chunk");
const ownersByChunk = new Map<string, Set<string>>();
for (const entry of chunks.filter((chunk) => chunk.isEntry)) {
const source = entry.facadeModuleId?.split("?", 1)[0];
const relative = source ? path.relative(rootDir, source).split(path.sep).join("/") : "";
// An empty owner represents a root entry and prevents plugin authorization.
const owner = /^extensions\/([a-z0-9][a-z0-9-]*)\//u.exec(relative)?.[1] ?? "";
const pending = [entry.fileName];
const visited = new Set<string>();
while (pending.length > 0) {
const fileName = pending.pop()!;
if (visited.has(fileName)) {
continue;
}
visited.add(fileName);
const chunk = bundle[fileName];
if (chunk?.type !== "chunk") {
continue;
}
const owners = ownersByChunk.get(fileName) ?? new Set<string>();
owners.add(owner);
ownersByChunk.set(fileName, owners);
pending.push(...chunk.imports, ...chunk.dynamicImports);
}
}
const ownership: RuntimeDependencyOwnership = { chunks: {} };
for (const chunk of chunks.toSorted((a, b) => a.fileName.localeCompare(b.fileName))) {
const owners = ownersByChunk.get(chunk.fileName);
if (!owners?.size || owners.has("") || chunk.fileName.startsWith("extensions/")) {
continue;
}
ownership.chunks[chunk.fileName] = {
sha256: createHash("sha256").update(chunk.code).digest("hex"),
extensions: [...owners].toSorted(),
};
}
this.emitFile({
type: "asset",
fileName: RUNTIME_DEPENDENCY_OWNERSHIP_ASSET_NAME,
source: `${JSON.stringify(ownership)}\n`,
});
},
},
};
}