Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- fix(cli): stop prepublish from re-rebuilding the already-built ESM-only opencode-plugin dist (#11787)
6 changes: 4 additions & 2 deletions scripts/build/prepublish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,11 @@ if (existsSync(cliSrcFile)) {
// flow for every downstream user.
const opencodePluginSrc = join(ROOT, "@omniroute", "opencode-plugin");
const opencodePluginDist = join(opencodePluginSrc, "dist", "index.js");
const opencodePluginCjs = join(opencodePluginSrc, "dist", "index.cjs");
if (existsSync(opencodePluginSrc) && existsSync(join(opencodePluginSrc, "package.json"))) {
const pluginAlreadyBuilt = existsSync(opencodePluginDist) && existsSync(opencodePluginCjs);
// The plugin's tsup config is ESM-only (format: ["esm"]), so a successful
// build only ever produces dist/index.js (+ dist/index.d.ts) — never
// dist/index.cjs. Gate the skip solely on dist/index.js.
const pluginAlreadyBuilt = existsSync(opencodePluginDist);
if (!pluginAlreadyBuilt) {
console.log("\n 🔨 Building @omniroute/opencode-plugin (tsup)...");
try {
Expand Down
95 changes: 95 additions & 0 deletions tests/unit/prepublish-opencode-plugin-build-skip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Regression test for issue #11787.
//
// scripts/build/prepublish.ts gates the "@omniroute/opencode-plugin already
// built -> skip rebuild" fast path on both dist/index.js AND dist/index.cjs
// existing. @omniroute/opencode-plugin/tsup.config.ts is ESM-only
// (format: ["esm"]), so a successful `tsup` run in that package never
// produces dist/index.cjs -- the old predicate could never be true.
//
// This test builds the REAL plugin package with the REAL tsup config (no
// mocks) and then evaluates the fixed predicate copied from prepublish.ts
// against the resulting dist/, proving the "already built" skip path now
// works for an ESM-only build, and still correctly reports "not built" when
// dist/ is absent.
import { test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveLocalBinEntry, isNativeExecutable } from "../../scripts/build/buildToolRunner.mjs";
import { resolveBundledNpmEntry } from "../../scripts/build/resolveNpmEntry.ts";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..");
const opencodePluginSrc = join(ROOT, "@omniroute", "opencode-plugin");
const opencodePluginDist = join(opencodePluginSrc, "dist", "index.js");
const opencodePluginCjs = join(opencodePluginSrc, "dist", "index.cjs");

test("prepublish pluginAlreadyBuilt predicate recognizes a real ESM-only tsup build (#11787)", () => {
rmSync(join(opencodePluginSrc, "dist"), { recursive: true, force: true });
// The plugin is a standalone package, not an npm workspace member (see
// scripts/build/prepublish.ts's own comment above the equivalent build step) — a
// root `npm ci` never populates its node_modules, so a fresh checkout (CI, or any
// devbox that hasn't built the plugin before) needs its own install first. Mirror
// prepublish.ts's own install step instead of hardcoding a `.bin/tsup` path that
// only exists on a devbox someone happened to `npm install` in already.
if (!existsSync(join(opencodePluginSrc, "node_modules"))) {
const npmEntry = resolveBundledNpmEntry("npm-cli.js");
const installArgs = ["install", "--no-audit", "--no-fund"];
if (npmEntry) {
execFileSync(process.execPath, [npmEntry, ...installArgs], {
cwd: opencodePluginSrc,
stdio: "inherit",
});
} else {
execFileSync("npm", installArgs, { cwd: opencodePluginSrc, stdio: "inherit" });
}
}
// Resolve the real tsup binary the same way scripts/build/prepublish.ts's
// runBuildTool() does — never a hardcoded `.bin/tsup` path, since npm's hoisting can
// place the binary at the plugin's own node_modules/.bin OR the repo root's,
// depending on the install.
const localEntry = resolveLocalBinEntry("tsup", "tsup", opencodePluginSrc);
if (localEntry) {
if (isNativeExecutable(localEntry)) {
execFileSync(localEntry, [], { cwd: opencodePluginSrc, stdio: "inherit" });
} else {
execFileSync(process.execPath, [localEntry], { cwd: opencodePluginSrc, stdio: "inherit" });
}
} else {
const npxEntry = resolveBundledNpmEntry("npx-cli.js");
if (npxEntry) {
execFileSync(process.execPath, [npxEntry, "tsup"], {
cwd: opencodePluginSrc,
stdio: "inherit",
});
} else {
execFileSync("npx", ["tsup"], { cwd: opencodePluginSrc, stdio: "inherit" });
}
}

assert.equal(existsSync(opencodePluginDist), true, "dist/index.js should exist after tsup");
assert.equal(
existsSync(opencodePluginCjs),
false,
"dist/index.cjs should NOT exist for an ESM-only tsup build"
);

// This is the fixed predicate from scripts/build/prepublish.ts.
const pluginAlreadyBuilt = existsSync(opencodePluginDist);

assert.equal(
pluginAlreadyBuilt,
true,
"expected the ESM-only build to be recognized as already built (index.js present is sufficient)"
);
});

test("prepublish pluginAlreadyBuilt predicate is false when dist/ is absent (#11787)", () => {
rmSync(join(opencodePluginSrc, "dist"), { recursive: true, force: true });

const pluginAlreadyBuilt = existsSync(opencodePluginDist);

assert.equal(pluginAlreadyBuilt, false, "expected a missing dist/ to require a rebuild");
});