mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
239 lines
9.4 KiB
TypeScript
239 lines
9.4 KiB
TypeScript
// Canonical declaration groups share one private compiler and publication lifetime.
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import {
|
|
prepareTsdownBuildExecution,
|
|
resolveStagedDeclarationConcurrency,
|
|
TSDOWN_DECLARATION_EXTENSIONS,
|
|
TSDOWN_UNIFIED_CACHE_ENV,
|
|
} from "../tsdown-build.mts";
|
|
import {
|
|
finalizeBuildStepCache,
|
|
portableRelativePath,
|
|
restoreBuildStepCacheOutputs,
|
|
resolveBuildStepCacheState,
|
|
resolveTsdownCompilerFiles,
|
|
type BuildCacheStep,
|
|
} from "./build-artifact-cache.mts";
|
|
import { CompilerInputSnapshot } from "./compiler-input-snapshot.mts";
|
|
import { publishStagedDeclarations } from "./declaration-stage.mts";
|
|
import { withDistArtifactOwnership } from "./dist-artifact-ownership.mts";
|
|
import { hasUnjoinedWork } from "./managed-child-process.mts";
|
|
import { resolveTsdownDeclarationGeneratorInputs } from "./tsdown-declaration-generator-inputs.mts";
|
|
import {
|
|
createDeclarationStage,
|
|
readDeclarationInputs,
|
|
requestDeclarationInputs,
|
|
} from "./tsdown-declaration-inputs.mts";
|
|
|
|
export async function writeTsdownDeclarations(
|
|
groups: readonly string[],
|
|
label: string,
|
|
previousOutputs: (root: string) => string[],
|
|
generatorEntry: string,
|
|
) {
|
|
const root = fs.realpathSync.native(process.cwd());
|
|
const stages: string[] = [];
|
|
const createStage = () => {
|
|
const stage = createDeclarationStage(root);
|
|
stages.push(stage);
|
|
return stage;
|
|
};
|
|
const failures: unknown[] = [];
|
|
try {
|
|
// The private child retains declared cwd ownership; snapshot/output paths are physical.
|
|
await withDistArtifactOwnership(process.cwd(), async () => {
|
|
const { default: configs }: { default: typeof import("../../tsdown.config.ts").default } =
|
|
await import(pathToFileURL(path.join(root, "tsdown.config.ts")).href);
|
|
const staging = createStage();
|
|
const output = path.join(staging, "dist");
|
|
const environment = TSDOWN_UNIFIED_CACHE_ENV.map((name) =>
|
|
JSON.stringify([name, process.env[name] ?? ""]),
|
|
);
|
|
const generatorInputs = resolveTsdownDeclarationGeneratorInputs(root, generatorEntry);
|
|
const snapshot = () =>
|
|
new CompilerInputSnapshot(root, {
|
|
toolchainFiles: resolveTsdownCompilerFiles(),
|
|
generatorInputs,
|
|
isGeneratorInput: (file) => /(?:^|\/)(?:package|openclaw\.plugin)\.json$/u.test(file),
|
|
});
|
|
// All groups share the same before/after reads of configuration, topology,
|
|
// tools and overlapping sources. Only compiler membership differs.
|
|
const before = snapshot();
|
|
const liveDist = path.join(root, "dist");
|
|
const prepared = groups.map((name) => {
|
|
const config = configs.find((candidate: { name?: string }) => candidate.name === name);
|
|
if (
|
|
!config?.dts ||
|
|
typeof config.dts !== "object" ||
|
|
!Array.isArray(config.dts.entry) ||
|
|
!config.entry ||
|
|
typeof config.entry !== "object" ||
|
|
Array.isArray(config.entry)
|
|
) {
|
|
throw new Error(`Missing canonical declaration group ${name}`);
|
|
}
|
|
// Runtime worker entries are absolute; declaration partitions are checkout-relative.
|
|
const entries = Object.entries(config.entry).map(
|
|
([entry, inputs]) =>
|
|
[entry, [inputs].flat().map((input) => path.resolve(root, input))] as const,
|
|
);
|
|
const required = config.dts.entry.map((source) => {
|
|
const resolved = path.resolve(root, source);
|
|
const selected = entries.find(([, inputs]) => inputs.includes(resolved));
|
|
if (!selected) {
|
|
throw new Error(`Missing canonical declaration entry for ${source}`);
|
|
}
|
|
return `${selected[0]}.d.ts`;
|
|
});
|
|
const identity = [
|
|
...environment,
|
|
JSON.stringify({
|
|
name,
|
|
entry: Object.fromEntries(
|
|
entries.map(([entry, sources]) => [
|
|
entry,
|
|
sources.map((source) => portableRelativePath(root, source)),
|
|
]),
|
|
),
|
|
declarations: config.dts.entry,
|
|
sourcemap: config.sourcemap,
|
|
}),
|
|
];
|
|
const stage = createStage();
|
|
const groupOutput = path.join(stage, "dist");
|
|
requestDeclarationInputs(groupOutput, name, config.dts.entry);
|
|
const plan = prepareTsdownBuildExecution(
|
|
{
|
|
args: ["--config", "tsdown.config.ts", "--filter", name, "--out-dir", groupOutput],
|
|
},
|
|
{
|
|
// Every compiler owns a fresh stage; live runtime outputs stay intact.
|
|
cleanup() {},
|
|
reportShortfall(shortfall) {
|
|
console.error(shortfall.message);
|
|
},
|
|
},
|
|
);
|
|
if (!plan) {
|
|
throw new Error("Insufficient memory for declaration build");
|
|
}
|
|
const receipt = `compiler-inputs/${name}.json`;
|
|
const step: BuildCacheStep = {
|
|
label: `${label}-${name}`,
|
|
cache: {
|
|
env: TSDOWN_UNIFIED_CACHE_ENV,
|
|
inputs: generatorInputs,
|
|
// An empty canonical partition still owns its successful compiler
|
|
// receipt. Ordinary cache records never admit an empty inventory.
|
|
outputs: [{ path: "dist", extensions: TSDOWN_DECLARATION_EXTENSIONS }, receipt],
|
|
requiredOutputs: [...required.map((entry) => `dist/${entry}`), receipt],
|
|
restore: "always",
|
|
},
|
|
};
|
|
const params = {
|
|
rootDir: root,
|
|
artifactRoot: stage,
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_BUILD_PRIVATE_QA: process.env.OPENCLAW_BUILD_PRIVATE_QA === "1" ? "1" : "0",
|
|
},
|
|
inputSignature: (inputs: string[]) =>
|
|
before.signature("tsconfig.json", identity, inputs, liveDist),
|
|
};
|
|
const state =
|
|
process.env.OPENCLAW_BUILD_CACHE === "0"
|
|
? undefined
|
|
: resolveBuildStepCacheState(step, params);
|
|
if (!state) {
|
|
params.inputSignature([]);
|
|
}
|
|
// Record the lookup before compilation replaces the signature and cache record.
|
|
console.error(
|
|
`[${label}] ${name}: cache ${state?.fresh ? "hit" : "miss"} (${state?.reason ?? "disabled"})`,
|
|
);
|
|
return { name, output: groupOutput, required, identity, plan, step, params, state };
|
|
});
|
|
const required = prepared.flatMap((group) => group.required);
|
|
if (!required.length) {
|
|
throw new Error("Canonical declaration selection is empty");
|
|
}
|
|
const startedAt = Date.now();
|
|
for (const group of prepared) {
|
|
if (group.state?.fresh && !restoreBuildStepCacheOutputs(group.state, group.params)) {
|
|
throw new Error("Declaration cache changed before restoration; rerun the build");
|
|
}
|
|
}
|
|
// Only nonempty single-compiler private stages may overlap. Publication
|
|
// still joins the complete batch before merging any group's declarations.
|
|
const misses = prepared.filter((group) => !group.state?.fresh);
|
|
const concurrency = misses.every(
|
|
(group) => group.required.length > 0 && group.plan.invocations.length === 1,
|
|
)
|
|
? resolveStagedDeclarationConcurrency(
|
|
misses.map((group) => ({ name: group.name, maxOldSpaceMb: group.plan.maxOldSpaceMb })),
|
|
)
|
|
: 1;
|
|
const plan = {
|
|
...prepared[0]!.plan,
|
|
invocations: misses.flatMap((group) => group.plan.invocations),
|
|
};
|
|
await publishStagedDeclarations(
|
|
plan,
|
|
prepared.map((group) => ({ output: group.output, required: group.required })),
|
|
output,
|
|
liveDist,
|
|
required,
|
|
previousOutputs(root).map((file) => portableRelativePath(liveDist, file)),
|
|
() => {
|
|
const after = snapshot();
|
|
for (const group of prepared) {
|
|
const sealed = after.seal(
|
|
"tsconfig.json",
|
|
group.identity,
|
|
readDeclarationInputs(group.output, group.name),
|
|
before,
|
|
startedAt,
|
|
liveDist,
|
|
);
|
|
if (group.state?.fresh && sealed.signature !== group.state.signature) {
|
|
throw new Error(`Cached declaration membership changed: ${group.name}`);
|
|
}
|
|
if (group.state) {
|
|
group.state.signature = sealed.signature;
|
|
group.state.consumedInputs = sealed.inputs;
|
|
}
|
|
}
|
|
},
|
|
concurrency,
|
|
);
|
|
for (const group of prepared) {
|
|
if (group.state && !group.state.fresh) {
|
|
finalizeBuildStepCache(group.step, group.state, group.params);
|
|
}
|
|
}
|
|
if (prepared.every((group) => group.state?.fresh)) {
|
|
console.log(`[${label}] restored complete cached generation`);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
failures.push(error);
|
|
}
|
|
// An unjoined child may still be writing a stage; retain its inputs and error
|
|
// under the checkout's existing fail-closed ownership until explicit recovery.
|
|
for (const stage of failures.some(hasUnjoinedWork) ? [] : stages) {
|
|
try {
|
|
fs.rmSync(stage, { recursive: true, force: true });
|
|
} catch (error) {
|
|
failures.push(error);
|
|
}
|
|
}
|
|
// The private entry observes this after module evaluation. Keep unjoined build
|
|
// metadata even if removing the private staging tree also failed.
|
|
if (failures.length) {
|
|
throw failures.length === 1
|
|
? failures[0]
|
|
: new AggregateError(failures, "Declaration build and staging cleanup failed");
|
|
}
|
|
}
|