fix(plugins): track package boundary dts freshness

This commit is contained in:
Vincent Koc
2026-04-07 12:59:38 +01:00
parent 97c031a8db
commit 76296a9d14
4 changed files with 64 additions and 41 deletions

View File

@@ -193,6 +193,7 @@ function isRelevantCompileInput(filePath) {
function collectNewestMtime(entryPath, params = {}) { function collectNewestMtime(entryPath, params = {}) {
const includeFile = params.includeFile ?? (() => true); const includeFile = params.includeFile ?? (() => true);
const skipDistDirectories = params.skipDistDirectories ?? true;
let newestMtimeMs = 0; let newestMtimeMs = 0;
function visit(currentPath) { function visit(currentPath) {
@@ -202,7 +203,7 @@ function collectNewestMtime(entryPath, params = {}) {
const stats = statSync(currentPath); const stats = statSync(currentPath);
if (stats.isDirectory()) { if (stats.isDirectory()) {
const basename = path.basename(currentPath); const basename = path.basename(currentPath);
if (basename === "dist" || basename === "node_modules") { if ((skipDistDirectories && basename === "dist") || basename === "node_modules") {
return; return;
} }
for (const child of readdirSync(currentPath)) { for (const child of readdirSync(currentPath)) {
@@ -241,10 +242,9 @@ export function isBoundaryCompileFresh(extensionId, params = {}) {
collectNewestMtime(extensionRoot, { includeFile: isRelevantCompileInput }); collectNewestMtime(extensionRoot, { includeFile: isRelevantCompileInput });
const sharedNewestInputMtimeMs = const sharedNewestInputMtimeMs =
params.sharedNewestInputMtimeMs ?? params.sharedNewestInputMtimeMs ??
Math.max( collectNewestMtime(resolve(rootDir, "packages/plugin-sdk/dist"), {
collectNewestMtime(resolve(rootDir, "dist/plugin-sdk")), skipDistDirectories: false,
collectNewestMtime(resolve(rootDir, "packages/plugin-sdk/dist")), });
);
const newestInputMtimeMs = Math.max(extensionNewestInputMtimeMs, sharedNewestInputMtimeMs); const newestInputMtimeMs = Math.max(extensionNewestInputMtimeMs, sharedNewestInputMtimeMs);
const oldestOutputMtimeMs = collectOldestMtime([ const oldestOutputMtimeMs = collectOldestMtime([
resolveBoundaryTsStampPath(extensionId, rootDir), resolveBoundaryTsStampPath(extensionId, rootDir),
@@ -553,13 +553,19 @@ async function runCompileCheck(extensionIds) {
process.stdout.write( process.stdout.write(
`preparing plugin-sdk boundary artifacts for ${extensionIds.length} plugins\n`, `preparing plugin-sdk boundary artifacts for ${extensionIds.length} plugins\n`,
); );
runNodeStep("plugin-sdk boundary prep", [prepareBoundaryArtifactsBin], 420_000); runNodeStep(
"plugin-sdk boundary prep",
[prepareBoundaryArtifactsBin, "--mode=package-boundary"],
420_000,
);
const prepElapsedMs = Date.now() - prepStartedAt; const prepElapsedMs = Date.now() - prepStartedAt;
const concurrency = resolveCompileConcurrency(); const concurrency = resolveCompileConcurrency();
const verboseFreshLogs = process.env.OPENCLAW_EXTENSION_BOUNDARY_VERBOSE_FRESH === "1"; const verboseFreshLogs = process.env.OPENCLAW_EXTENSION_BOUNDARY_VERBOSE_FRESH === "1";
const sharedNewestInputMtimeMs = Math.max( const sharedNewestInputMtimeMs = collectNewestMtime(
collectNewestMtime(resolve(repoRoot, "dist/plugin-sdk")), resolve(repoRoot, "packages/plugin-sdk/dist"),
collectNewestMtime(resolve(repoRoot, "packages/plugin-sdk/dist")), {
skipDistDirectories: false,
},
); );
process.stdout.write(`compile concurrency ${concurrency}\n`); process.stdout.write(`compile concurrency ${concurrency}\n`);
const compileStartedAt = Date.now(); const compileStartedAt = Date.now();

View File

@@ -7,6 +7,7 @@ const require = createRequire(import.meta.url);
const repoRoot = resolve(import.meta.dirname, ".."); const repoRoot = resolve(import.meta.dirname, "..");
const tscBin = require.resolve("typescript/bin/tsc"); const tscBin = require.resolve("typescript/bin/tsc");
const TYPE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".d.ts", ".js", ".mjs", ".json"]); const TYPE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".d.ts", ".js", ".mjs", ".json"]);
const VALID_MODES = new Set(["all", "package-boundary"]);
const ROOT_DTS_INPUTS = [ const ROOT_DTS_INPUTS = [
"tsconfig.json", "tsconfig.json",
@@ -36,6 +37,15 @@ function isRelevantTypeInput(filePath) {
return TYPE_INPUT_EXTENSIONS.has(path.extname(filePath)); return TYPE_INPUT_EXTENSIONS.has(path.extname(filePath));
} }
export function parseMode(argv = process.argv.slice(2)) {
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
const mode = modeArg?.slice("--mode=".length) ?? "all";
if (!VALID_MODES.has(mode)) {
throw new Error(`Unknown mode: ${mode}`);
}
return mode;
}
function collectNewestMtime(paths, params = {}) { function collectNewestMtime(paths, params = {}) {
const rootDir = params.rootDir ?? repoRoot; const rootDir = params.rootDir ?? repoRoot;
const includeFile = params.includeFile ?? (() => true); const includeFile = params.includeFile ?? (() => true);
@@ -199,8 +209,9 @@ export async function runNodeStepsInParallel(steps) {
} }
} }
export async function main() { export async function main(argv = process.argv.slice(2)) {
try { try {
const mode = parseMode(argv);
const rootDtsFresh = isArtifactSetFresh({ const rootDtsFresh = isArtifactSetFresh({
inputPaths: ROOT_DTS_INPUTS, inputPaths: ROOT_DTS_INPUTS,
outputPaths: ["dist/plugin-sdk/.tsbuildinfo"], outputPaths: ["dist/plugin-sdk/.tsbuildinfo"],
@@ -221,14 +232,16 @@ export async function main() {
}); });
const pendingSteps = []; const pendingSteps = [];
if (!rootDtsFresh) { if (mode === "all") {
pendingSteps.push({ if (!rootDtsFresh) {
label: "plugin-sdk boundary dts", pendingSteps.push({
args: [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"], label: "plugin-sdk boundary dts",
timeoutMs: 300_000, args: [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"],
}); timeoutMs: 300_000,
} else { });
process.stdout.write("[plugin-sdk boundary dts] fresh; skipping\n"); } else {
process.stdout.write("[plugin-sdk boundary dts] fresh; skipping\n");
}
} }
if (!packageDtsFresh) { if (!packageDtsFresh) {
pendingSteps.push({ pendingSteps.push({
@@ -244,13 +257,13 @@ export async function main() {
await runNodeStepsInParallel(pendingSteps); await runNodeStepsInParallel(pendingSteps);
} }
if (!entryShimsFresh || pendingSteps.length > 0) { if (mode === "all" && (!entryShimsFresh || pendingSteps.length > 0)) {
await runNodeStep( await runNodeStep(
"plugin-sdk boundary root shims", "plugin-sdk boundary root shims",
["--import", "tsx", resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts")], ["--import", "tsx", resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts")],
120_000, 120_000,
); );
} else { } else if (mode === "all") {
process.stdout.write("[plugin-sdk boundary root shims] fresh; skipping\n"); process.stdout.write("[plugin-sdk boundary root shims] fresh; skipping\n");
} }
} catch (error) { } catch (error) {

View File

@@ -206,30 +206,26 @@ describe("check-extension-package-tsc-boundary", () => {
).toBe("skipped 97 fresh plugin compiles\n"); ).toBe("skipped 97 fresh plugin compiles\n");
}); });
it("treats a plugin compile as fresh only when its outputs are newer than plugin and sdk inputs", () => { it("treats a plugin compile as fresh only when its outputs are newer than plugin and package sdk inputs", () => {
const { rootDir, extensionRoot } = createTempExtensionRoot(); const { rootDir, extensionRoot } = createTempExtensionRoot();
const extensionSourcePath = path.join(extensionRoot, "index.ts"); const extensionSourcePath = path.join(extensionRoot, "index.ts");
const extensionTsconfigPath = path.join(extensionRoot, "tsconfig.json"); const extensionTsconfigPath = path.join(extensionRoot, "tsconfig.json");
const stampPath = path.join(extensionRoot, "dist", ".boundary-tsc.stamp"); const stampPath = path.join(extensionRoot, "dist", ".boundary-tsc.stamp");
const rootSdkBuildInfoPath = path.join(rootDir, "dist", "plugin-sdk", ".tsbuildinfo"); const rootSdkTypePath = path.join(rootDir, "dist", "plugin-sdk", "core.d.ts");
const packageSdkBuildInfoPath = path.join( const packageSdkTypePath = path.join(
rootDir, rootDir,
"packages", "packages",
"plugin-sdk", "plugin-sdk",
"dist", "dist",
".tsbuildinfo", "src",
);
const entryShimStampPath = path.join(
rootDir,
"dist",
"plugin-sdk", "plugin-sdk",
".boundary-entry-shims.stamp", "core.d.ts",
); );
fs.mkdirSync(path.dirname(extensionSourcePath), { recursive: true }); fs.mkdirSync(path.dirname(extensionSourcePath), { recursive: true });
fs.mkdirSync(path.dirname(stampPath), { recursive: true }); fs.mkdirSync(path.dirname(stampPath), { recursive: true });
fs.mkdirSync(path.dirname(rootSdkBuildInfoPath), { recursive: true }); fs.mkdirSync(path.dirname(rootSdkTypePath), { recursive: true });
fs.mkdirSync(path.dirname(packageSdkBuildInfoPath), { recursive: true }); fs.mkdirSync(path.dirname(packageSdkTypePath), { recursive: true });
fs.writeFileSync(extensionSourcePath, "export const demo = 1;\n", "utf8"); fs.writeFileSync(extensionSourcePath, "export const demo = 1;\n", "utf8");
fs.writeFileSync( fs.writeFileSync(
@@ -238,26 +234,27 @@ describe("check-extension-package-tsc-boundary", () => {
"utf8", "utf8",
); );
fs.writeFileSync(stampPath, "ok\n", "utf8"); fs.writeFileSync(stampPath, "ok\n", "utf8");
fs.writeFileSync(rootSdkBuildInfoPath, "ok\n", "utf8"); fs.writeFileSync(rootSdkTypePath, "export {};\n", "utf8");
fs.writeFileSync(packageSdkBuildInfoPath, "ok\n", "utf8"); fs.writeFileSync(packageSdkTypePath, "export {};\n", "utf8");
fs.writeFileSync(entryShimStampPath, "ok\n", "utf8");
fs.utimesSync(extensionSourcePath, new Date(1_000), new Date(1_000)); fs.utimesSync(extensionSourcePath, new Date(1_000), new Date(1_000));
fs.utimesSync(extensionTsconfigPath, new Date(1_000), new Date(1_000)); fs.utimesSync(extensionTsconfigPath, new Date(1_000), new Date(1_000));
fs.utimesSync(rootSdkBuildInfoPath, new Date(2_000), new Date(2_000)); fs.utimesSync(rootSdkTypePath, new Date(500), new Date(500));
fs.utimesSync(packageSdkBuildInfoPath, new Date(2_000), new Date(2_000)); fs.utimesSync(packageSdkTypePath, new Date(2_000), new Date(2_000));
fs.utimesSync(entryShimStampPath, new Date(2_000), new Date(2_000));
fs.utimesSync(stampPath, new Date(3_000), new Date(3_000)); fs.utimesSync(stampPath, new Date(3_000), new Date(3_000));
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true); expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true);
fs.utimesSync(rootSdkBuildInfoPath, new Date(500), new Date(500)); fs.utimesSync(rootSdkTypePath, new Date(500), new Date(500));
fs.utimesSync(packageSdkBuildInfoPath, new Date(500), new Date(500)); fs.utimesSync(packageSdkTypePath, new Date(500), new Date(500));
fs.utimesSync(entryShimStampPath, new Date(500), new Date(500));
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true); expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true);
fs.utimesSync(rootSdkBuildInfoPath, new Date(4_000), new Date(4_000)); fs.utimesSync(rootSdkTypePath, new Date(4_000), new Date(4_000));
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true);
fs.utimesSync(packageSdkTypePath, new Date(4_000), new Date(4_000));
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(false); expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(false);
}); });

View File

@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { import {
createPrefixedOutputWriter, createPrefixedOutputWriter,
isArtifactSetFresh, isArtifactSetFresh,
parseMode,
runNodeStepsInParallel, runNodeStepsInParallel,
} from "../../scripts/prepare-extension-package-boundary-artifacts.mjs"; } from "../../scripts/prepare-extension-package-boundary-artifacts.mjs";
@@ -85,4 +86,10 @@ describe("prepare-extension-package-boundary-artifacts", () => {
}), }),
).toBe(false); ).toBe(false);
}); });
it("parses prep mode and rejects unknown values", () => {
expect(parseMode([])).toBe("all");
expect(parseMode(["--mode=package-boundary"])).toBe("package-boundary");
expect(() => parseMode(["--mode=nope"])).toThrow("Unknown mode: nope");
});
}); });