From 7d2088132d1ab0ab2bc4cf10e0162db677f7bee7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 7 Apr 2026 12:25:57 +0100 Subject: [PATCH] perf(plugins): skip fresh boundary plugin compiles --- .../check-extension-package-tsc-boundary.mjs | 151 +++++++++++++++--- ...eck-extension-package-tsc-boundary.test.ts | 64 +++++++- 2 files changed, 188 insertions(+), 27 deletions(-) diff --git a/scripts/check-extension-package-tsc-boundary.mjs b/scripts/check-extension-package-tsc-boundary.mjs index bfff928629b..ea6fb3917a6 100644 --- a/scripts/check-extension-package-tsc-boundary.mjs +++ b/scripts/check-extension-package-tsc-boundary.mjs @@ -1,10 +1,18 @@ #!/usr/bin/env node import { spawn, spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { createRequire } from "node:module"; import os from "node:os"; -import { dirname, join, resolve } from "node:path"; +import path, { dirname, join, resolve } from "node:path"; const require = createRequire(import.meta.url); const repoRoot = resolve(import.meta.dirname, ".."); @@ -15,6 +23,7 @@ const prepareBoundaryArtifactsBin = resolve( ); const extensionPackageBoundaryBaseConfig = "../tsconfig.package-boundary.base.json"; const FAILURE_OUTPUT_TAIL_LINES = 40; +const COMPILE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".json"]); function parseMode(argv) { const modeArg = argv.find((arg) => arg.startsWith("--mode=")); @@ -76,6 +85,9 @@ export function formatBoundaryCheckSuccessSummary(params = {}) { if (Number.isInteger(params.compileCount)) { lines.push(`compiled plugins: ${params.compileCount}`); } + if (Number.isInteger(params.skippedCompileCount) && params.skippedCompileCount > 0) { + lines.push(`skipped plugins: ${params.skippedCompileCount}`); + } if (Number.isInteger(params.canaryCount)) { lines.push(`canary plugins: ${params.canaryCount}`); } @@ -148,6 +160,77 @@ function collectCanaryExtensionIds(extensionIds) { ]; } +function isRelevantCompileInput(filePath) { + const basename = path.basename(filePath); + if ( + basename === "__rootdir_boundary_canary__.ts" || + basename === "tsconfig.rootdir-canary.json" + ) { + return false; + } + if (basename.endsWith(".tsbuildinfo")) { + return false; + } + return COMPILE_INPUT_EXTENSIONS.has(path.extname(filePath)); +} + +function collectNewestMtime(entryPath, params = {}) { + const includeFile = params.includeFile ?? (() => true); + let newestMtimeMs = 0; + + function visit(currentPath) { + if (!existsSync(currentPath)) { + return; + } + const stats = statSync(currentPath); + if (stats.isDirectory()) { + const basename = path.basename(currentPath); + if (basename === "dist" || basename === "node_modules") { + return; + } + for (const child of readdirSync(currentPath)) { + visit(path.join(currentPath, child)); + } + return; + } + if (!includeFile(currentPath)) { + return; + } + newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs); + } + + visit(entryPath); + return newestMtimeMs; +} + +function collectOldestMtime(paths) { + let oldestMtimeMs = Number.POSITIVE_INFINITY; + + for (const entryPath of paths) { + if (!existsSync(entryPath)) { + return null; + } + oldestMtimeMs = Math.min(oldestMtimeMs, statSync(entryPath).mtimeMs); + } + + return Number.isFinite(oldestMtimeMs) ? oldestMtimeMs : null; +} + +export function isBoundaryCompileFresh(extensionId, params = {}) { + const rootDir = params.rootDir ?? repoRoot; + const extensionRoot = resolve(rootDir, "extensions", extensionId); + const newestInputMtimeMs = Math.max( + collectNewestMtime(extensionRoot, { includeFile: isRelevantCompileInput }), + collectNewestMtime(resolve(rootDir, "extensions", extensionId, "tsconfig.json")), + collectNewestMtime(resolve(rootDir, "dist/plugin-sdk")), + collectNewestMtime(resolve(rootDir, "packages/plugin-sdk/dist")), + ); + const oldestOutputMtimeMs = collectOldestMtime([ + resolve(rootDir, "extensions", extensionId, "dist", ".boundary-tsc.tsbuildinfo"), + ]); + return oldestOutputMtimeMs !== null && oldestOutputMtimeMs >= newestInputMtimeMs; +} + function runNodeStep(label, args, timeoutMs) { const startedAt = Date.now(); const result = spawnSync(process.execPath, args, { @@ -444,29 +527,43 @@ async function runCompileCheck(extensionIds) { const concurrency = resolveCompileConcurrency(); process.stdout.write(`compile concurrency ${concurrency}\n`); const compileStartedAt = Date.now(); - const steps = extensionIds.map((extensionId, index) => { - const tsBuildInfoPath = resolveBoundaryTsBuildInfoPath(extensionId); - mkdirSync(dirname(tsBuildInfoPath), { recursive: true }); - return { - label: extensionId, - onStart() { - process.stdout.write(`[${index + 1}/${extensionIds.length}] ${extensionId}\n`); - }, - args: [ - tscBin, - "-p", - resolve(repoRoot, "extensions", extensionId, "tsconfig.json"), - "--noEmit", - "--incremental", - "--tsBuildInfoFile", - tsBuildInfoPath, - ], - timeoutMs: 120_000, - }; - }); - await runNodeStepsWithConcurrency(steps, concurrency); + let skippedCompileCount = 0; + const steps = extensionIds + .map((extensionId, index) => { + const tsBuildInfoPath = resolveBoundaryTsBuildInfoPath(extensionId); + mkdirSync(dirname(tsBuildInfoPath), { recursive: true }); + if (isBoundaryCompileFresh(extensionId)) { + skippedCompileCount += 1; + process.stdout.write( + `[${index + 1}/${extensionIds.length}] ${extensionId} (fresh; skipping)\n`, + ); + return null; + } + return { + label: extensionId, + onStart() { + process.stdout.write(`[${index + 1}/${extensionIds.length}] ${extensionId}\n`); + }, + args: [ + tscBin, + "-p", + resolve(repoRoot, "extensions", extensionId, "tsconfig.json"), + "--noEmit", + "--incremental", + "--tsBuildInfoFile", + tsBuildInfoPath, + ], + timeoutMs: 120_000, + }; + }) + .filter(Boolean); + if (steps.length > 0) { + await runNodeStepsWithConcurrency(steps, concurrency); + } return { prepElapsedMs, + compileCount: steps.length, + skippedCompileCount, compileElapsedMs: Date.now() - compileStartedAt, }; } @@ -532,13 +629,16 @@ export async function main(argv = process.argv.slice(2)) { const releaseBoundaryLock = acquireBoundaryCheckLock(); const teardownCanaryCleanup = installCanaryArtifactCleanup(cleanupExtensionIds); let prepElapsedMs; + let compileCount = 0; + let skippedCompileCount = 0; let compileElapsedMs; let canaryElapsedMs; try { cleanupCanaryArtifactsForExtensions(cleanupExtensionIds); if (mode === "all" || mode === "compile") { - ({ prepElapsedMs, compileElapsedMs } = await runCompileCheck(optInExtensionIds)); + ({ prepElapsedMs, compileCount, skippedCompileCount, compileElapsedMs } = + await runCompileCheck(optInExtensionIds)); } if (shouldRunCanary) { ({ canaryElapsedMs } = runCanaryCheck(canaryExtensionIds)); @@ -546,7 +646,8 @@ export async function main(argv = process.argv.slice(2)) { process.stdout.write( formatBoundaryCheckSuccessSummary({ mode, - compileCount: mode === "canary" ? 0 : optInExtensionIds.length, + compileCount, + skippedCompileCount, canaryCount: shouldRunCanary ? canaryExtensionIds.length : 0, prepElapsedMs, compileElapsedMs, diff --git a/test/scripts/check-extension-package-tsc-boundary.test.ts b/test/scripts/check-extension-package-tsc-boundary.test.ts index c7d548f12ad..97bb53e07ae 100644 --- a/test/scripts/check-extension-package-tsc-boundary.test.ts +++ b/test/scripts/check-extension-package-tsc-boundary.test.ts @@ -9,6 +9,7 @@ import { formatBoundaryCheckSuccessSummary, formatStepFailure, installCanaryArtifactCleanup, + isBoundaryCompileFresh, resolveBoundaryCheckLockPath, resolveCanaryArtifactPaths, runNodeStepAsync, @@ -138,7 +139,8 @@ describe("check-extension-package-tsc-boundary", () => { expect( formatBoundaryCheckSuccessSummary({ mode: "all", - compileCount: 97, + compileCount: 84, + skippedCompileCount: 13, canaryCount: 12, prepElapsedMs: 12_345, compileElapsedMs: 54_321, @@ -149,7 +151,8 @@ describe("check-extension-package-tsc-boundary", () => { [ "extension package boundary check passed", "mode: all", - "compiled plugins: 97", + "compiled plugins: 84", + "skipped plugins: 13", "canary plugins: 12", "prep elapsed: 12345ms", "compile elapsed: 54321ms", @@ -165,6 +168,7 @@ describe("check-extension-package-tsc-boundary", () => { formatBoundaryCheckSuccessSummary({ mode: "compile", compileCount: 97, + skippedCompileCount: 0, canaryCount: 0, prepElapsedMs: 12_345, compileElapsedMs: 54_321, @@ -185,6 +189,62 @@ describe("check-extension-package-tsc-boundary", () => { ); }); + it("treats a plugin compile as fresh only when its outputs are newer than plugin and sdk inputs", () => { + const { rootDir, extensionRoot } = createTempExtensionRoot(); + const extensionSourcePath = path.join(extensionRoot, "index.ts"); + const extensionTsconfigPath = path.join(extensionRoot, "tsconfig.json"); + const buildInfoPath = path.join(extensionRoot, "dist", ".boundary-tsc.tsbuildinfo"); + const rootSdkBuildInfoPath = path.join(rootDir, "dist", "plugin-sdk", ".tsbuildinfo"); + const packageSdkBuildInfoPath = path.join( + rootDir, + "packages", + "plugin-sdk", + "dist", + ".tsbuildinfo", + ); + const entryShimStampPath = path.join( + rootDir, + "dist", + "plugin-sdk", + ".boundary-entry-shims.stamp", + ); + + fs.mkdirSync(path.dirname(extensionSourcePath), { recursive: true }); + fs.mkdirSync(path.dirname(buildInfoPath), { recursive: true }); + fs.mkdirSync(path.dirname(rootSdkBuildInfoPath), { recursive: true }); + fs.mkdirSync(path.dirname(packageSdkBuildInfoPath), { recursive: true }); + + fs.writeFileSync(extensionSourcePath, "export const demo = 1;\n", "utf8"); + fs.writeFileSync( + extensionTsconfigPath, + '{ "extends": "../tsconfig.package-boundary.base.json" }\n', + "utf8", + ); + fs.writeFileSync(buildInfoPath, "ok\n", "utf8"); + fs.writeFileSync(rootSdkBuildInfoPath, "ok\n", "utf8"); + fs.writeFileSync(packageSdkBuildInfoPath, "ok\n", "utf8"); + fs.writeFileSync(entryShimStampPath, "ok\n", "utf8"); + + fs.utimesSync(extensionSourcePath, 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(packageSdkBuildInfoPath, new Date(2_000), new Date(2_000)); + fs.utimesSync(entryShimStampPath, new Date(2_000), new Date(2_000)); + fs.utimesSync(buildInfoPath, new Date(3_000), new Date(3_000)); + + expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true); + + fs.utimesSync(rootSdkBuildInfoPath, new Date(500), new Date(500)); + fs.utimesSync(packageSdkBuildInfoPath, new Date(500), new Date(500)); + fs.utimesSync(entryShimStampPath, new Date(500), new Date(500)); + + expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true); + + fs.utimesSync(rootSdkBuildInfoPath, new Date(4_000), new Date(4_000)); + + expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(false); + }); + it("keeps full failure output on the thrown error for canary detection", async () => { await expect( runNodeStepAsync(