fix: dev stages local plugin builds on paths with spaces and builds them with the local SDK

This commit is contained in:
LukeGus committed 2026-10-08 13:38:56 -05:00
1 parent 878f1ebaa3
commit 4511cc0a42
4 files changed
+76 -8

No files matched your search

+6 -3
View File
@@ -8,11 +8,12 @@
*
* For development, `--local <dir>` (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);
+35
View File
@@ -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");
+8 -3
View File
@@ -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;
+27 -2
View File
@@ -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,