From 4511cc0a420f784fb6867729d8aa6c729dcc448f Mon Sep 17 00:00:00 2001 From: LukeGus Date: Thu, 8 Oct 2026 13:38:56 -0500 Subject: [PATCH] fix: dev stages local plugin builds on paths with spaces and builds them with the local SDK --- scripts/build-plugins.cjs | 9 ++++++--- scripts/bundled-plugins.test.ts | 35 +++++++++++++++++++++++++++++++++ scripts/dev.mjs | 11 ++++++++--- scripts/lib/bundled-plugins.cjs | 29 +++++++++++++++++++++++++-- 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/scripts/build-plugins.cjs b/scripts/build-plugins.cjs index 318c53294..1a585d11a 100644 --- a/scripts/build-plugins.cjs +++ b/scripts/build-plugins.cjs @@ -8,11 +8,12 @@ * * For development, `--local ` (or TERMIX_LOCAL_PLUGINS) points at a * folder of plugin repos like ../Termix-Plugins. A plugin there whose sources - * changed since its last build is rebuilt first, then any built one (has + * changed since its last build is rebuilt first with this checkout's SDK, so + * SDK edits apply without publishing, then any built one (has * dist/) is copied in place of its pin, or added when it has no pin. */ -const { execSync } = require("node:child_process"); +const { execFileSync } = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); const { @@ -21,6 +22,7 @@ const { extractArtifact, findLocalPluginBuilds, rebuildLocalPlugins, + localSdkBuildCommand, copyLocalBuild, buildBundledIndex, } = require("./lib/bundled-plugins.cjs"); @@ -34,9 +36,10 @@ async function main() { const localArg = flag !== -1 ? process.argv[flag + 1] : process.env.TERMIX_LOCAL_PLUGINS; const localDir = localArg ? path.resolve(root, localArg) : null; + const [node, buildArgs] = localSdkBuildCommand(); rebuildLocalPlugins(localDir, null, (repo, id) => { console.log(`building ${id}`); - execSync("npm run build", { cwd: repo, stdio: "inherit" }); + execFileSync(node, buildArgs, { cwd: repo, stdio: "inherit" }); }); const local = findLocalPluginBuilds(localDir); diff --git a/scripts/bundled-plugins.test.ts b/scripts/bundled-plugins.test.ts index 7ee32339a..2f77790c5 100644 --- a/scripts/bundled-plugins.test.ts +++ b/scripts/bundled-plugins.test.ts @@ -13,6 +13,7 @@ import { buildBundledIndex, findLocalPluginBuilds, isLocalBuildStale, + localSdkBuildCommand, rebuildLocalPlugins, copyLocalBuild, } from "./lib/bundled-plugins.cjs"; @@ -354,6 +355,40 @@ describe("rebuilding local plugins", () => { expect(isLocalBuildStale(root)).toBe(true); }); + it("is stale when the SDK CLI changed after the build", () => { + const root = repo(tempDir(), "Plugin-A", "a"); + const cli = path.join(tempDir(), "cli"); + fs.mkdirSync(cli); + fs.writeFileSync(path.join(cli, "index.mjs"), "x"); + fs.mkdirSync(path.join(root, "dist")); + fs.writeFileSync(path.join(root, "dist", "frontend.js"), "x"); + touch(path.join(root, "manifest.json"), 60); + touch(path.join(root, "src", "index.ts"), 60); + touch(path.join(cli, "index.mjs"), 60); + expect(isLocalBuildStale(root, cli)).toBe(false); + + touch(path.join(root, "dist", "frontend.js"), 30); + touch(path.join(cli, "index.mjs"), 0); + expect(isLocalBuildStale(root, cli)).toBe(true); + expect(isLocalBuildStale(root)).toBe(false); + }); + + it("builds with this checkout's SDK CLI, not the plugin's own", () => { + const [command, args] = localSdkBuildCommand(); + expect(command).toBe(process.execPath); + expect(args).toEqual([ + path.resolve( + __dirname, + "..", + "packages", + "plugin-sdk", + "cli", + "index.mjs", + ), + "build", + ]); + }); + it("builds only stale repos for bundled ids that have node_modules", () => { const dir = tempDir(); repo(dir, "Plugin-A", "a"); diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 6d8ba5a8d..0b97b69d6 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -45,7 +45,10 @@ import { } from "./lib/dev-docker.mjs"; const require = createRequire(import.meta.url); -const { copyLocalBuild } = require("./lib/bundled-plugins.cjs"); +const { + copyLocalBuild, + localSdkBuildCommand, +} = require("./lib/bundled-plugins.cjs"); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const BACKEND_ENTRY = path.join( @@ -83,7 +86,9 @@ function run(command, args, cwd = root) { const child = spawn(command, args, { cwd, stdio: ["ignore", "inherit", "inherit"], - shell: process.platform === "win32", + // npm is a .cmd on Windows and needs the shell, but the shell splits + // arguments on spaces, so a path like "Personal Projects" gets cut. + shell: process.platform === "win32" && !path.isAbsolute(command), }); child.on("error", reject); child.on("exit", (code) => @@ -541,7 +546,7 @@ function watchCoreForDocker() { async function rebuildPlugin({ id, repo }) { log(`building ${id}`); try { - await run("npm", ["run", "build"], repo); + await run(...localSdkBuildCommand(), repo); } catch (error) { warn(`${id} failed to build: ${error.message}`); return; diff --git a/scripts/lib/bundled-plugins.cjs b/scripts/lib/bundled-plugins.cjs index 940e1e02e..ddbbc034d 100644 --- a/scripts/lib/bundled-plugins.cjs +++ b/scripts/lib/bundled-plugins.cjs @@ -299,14 +299,37 @@ function newestMtime(target) { } /** True when a plugin repo has never been built or its sources changed since. */ -function isLocalBuildStale(repo) { +/** The SDK CLI in this checkout, which dev builds local plugins with. */ +const LOCAL_SDK_CLI = path.join( + __dirname, + "..", + "..", + "packages", + "plugin-sdk", + "cli", +); + +/** + * True when the build is missing, a source is newer than it, or the SDK CLI + * that builds it changed since (sdkCli, when given). + */ +function isLocalBuildStale(repo, sdkCli) { const built = newestMtime(path.join(repo, "dist")); if (!built) return true; + if (sdkCli && newestMtime(sdkCli) > built) return true; return BUILD_INPUTS.some( (entry) => newestMtime(path.join(repo, entry)) > built, ); } +/** + * The command that builds a plugin repo with this checkout's SDK instead of + * the published one in its node_modules, so SDK edits apply right away. + */ +function localSdkBuildCommand() { + return [process.execPath, [path.join(LOCAL_SDK_CLI, "index.mjs"), "build"]]; +} + /** * Builds each plugin repo whose build is missing or out of date. ids limits * it to those plugins; null builds every one. @@ -325,7 +348,8 @@ function rebuildLocalPlugins(dir, ids, run) { } catch { continue; } - if ((ids && !ids.has(id)) || !isLocalBuildStale(repo)) continue; + if ((ids && !ids.has(id)) || !isLocalBuildStale(repo, LOCAL_SDK_CLI)) + continue; run(repo, id); rebuilt.push(id); } @@ -346,6 +370,7 @@ function copyLocalBuild(repo, destination) { module.exports = { findLocalPluginBuilds, isLocalBuildStale, + localSdkBuildCommand, rebuildLocalPlugins, copyLocalBuild, parseBundledPlugins,