From c6e1fad6a8269820a0085e0f6af4622132309bcb Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Fri, 2 Oct 2026 22:21:43 +1300 Subject: [PATCH] fix(cli): preserve legacy Windows launcher path --- .changeset/warm-shims-resolve.md | 5 +++ .../src/__tests__/wrapper-entrypoints.test.ts | 31 +++++++++++++++++++ apps/cli/src/build.ts | 27 ++++++---------- apps/cli/src/wrapper-entrypoints.ts | 26 ++++++++++++++++ tests/release-bootstrap-smoke.test.ts | 25 +++++++++------ 5 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 .changeset/warm-shims-resolve.md create mode 100644 apps/cli/src/__tests__/wrapper-entrypoints.test.ts create mode 100644 apps/cli/src/wrapper-entrypoints.ts diff --git a/.changeset/warm-shims-resolve.md b/.changeset/warm-shims-resolve.md new file mode 100644 index 0000000000..df0279fa16 --- /dev/null +++ b/.changeset/warm-shims-resolve.md @@ -0,0 +1,5 @@ +--- +"executor": patch +--- + +Keep the legacy `bin/executor` launcher available for stale Windows PowerShell shims while declaring `bin.mjs` as the current CLI entrypoint. diff --git a/apps/cli/src/__tests__/wrapper-entrypoints.test.ts b/apps/cli/src/__tests__/wrapper-entrypoints.test.ts new file mode 100644 index 0000000000..f5bdbeeaa3 --- /dev/null +++ b/apps/cli/src/__tests__/wrapper-entrypoints.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "@effect/vitest"; +import { spawnSync } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { writeWrapperEntrypoints } from "../wrapper-entrypoints"; + +describe("wrapper entrypoints", () => { + it("keeps the legacy bin/executor path working alongside bin.mjs", async () => { + const wrapperDir = await mkdtemp(join(tmpdir(), "executor-wrapper-entrypoints-")); + + try { + await writeWrapperEntrypoints( + wrapperDir, + 'process.stdout.write(process.argv.slice(2).join(" "));', + ); + + for (const entrypoint of [join(wrapperDir, "bin.mjs"), join(wrapperDir, "bin", "executor")]) { + const result = spawnSync(process.execPath, [entrypoint, "--version"], { + encoding: "utf8", + }); + + expect(result.error).toBeUndefined(); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toBe("--version"); + } + } finally { + await rm(wrapperDir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/cli/src/build.ts b/apps/cli/src/build.ts index 6c1f701383..2bb9c029ef 100644 --- a/apps/cli/src/build.ts +++ b/apps/cli/src/build.ts @@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; import { $ } from "bun"; import { WORKER_BUNDLER_DIRNAME, missingWorkerBundlerFiles } from "./worker-bundler-artifact"; +import { writeWrapperEntrypoints } from "./wrapper-entrypoints"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); const cliRoot = resolve(repoRoot, "apps/cli"); @@ -598,17 +599,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { const buildWrapperPackage = async (binaries: Record) => { const meta = await readMetadata(); const wrapperDir = join(distDir, meta.name); - const binDir = join(wrapperDir, "bin"); - await mkdir(binDir, { recursive: true }); - - // Node.js launcher — resolves the platform binary via require.resolve - // against optionalDependencies and execs it. No postinstall: the binary - // ships as an os/cpu-filtered optional dep, the launcher resolves it at - // runtime. This works whether or not the package manager runs postinstalls - // (bun blocks them by default). - await writeFile(join(binDir, "executor"), NODE_SHIM); - await chmod(join(binDir, "executor"), 0o755); + // Keep bin/executor as a compatibility entrypoint for stale Windows + // PowerShell shims while making bin.mjs the package's declared entrypoint. + await writeWrapperEntrypoints(wrapperDir, NODE_SHIM); await writeFile( join(wrapperDir, "package.json"), @@ -622,7 +616,7 @@ const buildWrapperPackage = async (binaries: Record) => { bugs: meta.bugs, repository: meta.repository, license: meta.license, - bin: { executor: "bin/executor" }, + bin: { executor: "bin.mjs" }, // Per-platform compiled binaries published as platform-tagged // versions of `executor` itself, referenced via npm:alias specs: // "executor-linux-x64": "npm:executor@1.4.14-linux-x64" @@ -712,11 +706,10 @@ const buildPreviewWrapperPackage = async (targets: Target[]) => { } const wrapperDir = join(distDir, meta.name); - const binDir = join(wrapperDir, "bin"); - await mkdir(binDir, { recursive: true }); - await writeFile(join(binDir, "executor"), NODE_SHIM); - await chmod(join(binDir, "executor"), 0o755); + // Keep bin/executor for stale Windows PowerShell shims created by older + // installs; current installs use the declared bin.mjs entrypoint. + await writeWrapperEntrypoints(wrapperDir, NODE_SHIM); const postinstall = PREVIEW_POSTINSTALL_SCRIPT.replaceAll("__CDN_BASE_URL__", `${cdnUrl}/${sha}`); await writeFile(join(wrapperDir, "postinstall.cjs"), postinstall); @@ -739,8 +732,8 @@ const buildPreviewWrapperPackage = async (targets: Target[]) => { bugs: meta.bugs, repository: meta.repository, license: meta.license, - bin: { executor: "bin/executor" }, - files: ["bin", "postinstall.cjs", "README.md"], + bin: { executor: "bin.mjs" }, + files: ["bin", "bin.mjs", "postinstall.cjs", "README.md"], scripts: { postinstall: "node ./postinstall.cjs" }, os: osList, cpu: cpuList, diff --git a/apps/cli/src/wrapper-entrypoints.ts b/apps/cli/src/wrapper-entrypoints.ts new file mode 100644 index 0000000000..df3aa4fe6f --- /dev/null +++ b/apps/cli/src/wrapper-entrypoints.ts @@ -0,0 +1,26 @@ +import { chmod, mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +const CURRENT_ENTRYPOINT = `#!/usr/bin/env node +import "./bin/executor.cjs"; +`; + +const LEGACY_ENTRYPOINT = `#!/usr/bin/env node +require("./executor.cjs"); +`; + +export const writeWrapperEntrypoints = async ( + wrapperDir: string, + launcherSource: string, +): Promise => { + const binDir = join(wrapperDir, "bin"); + const legacyEntrypoint = join(binDir, "executor"); + const currentEntrypoint = join(wrapperDir, "bin.mjs"); + + await mkdir(binDir, { recursive: true }); + await writeFile(join(binDir, "executor.cjs"), launcherSource); + await writeFile(legacyEntrypoint, LEGACY_ENTRYPOINT); + await writeFile(currentEntrypoint, CURRENT_ENTRYPOINT); + await chmod(legacyEntrypoint, 0o755); + await chmod(currentEntrypoint, 0o755); +}; diff --git a/tests/release-bootstrap-smoke.test.ts b/tests/release-bootstrap-smoke.test.ts index 7ddb6f9cce..d898f49113 100644 --- a/tests/release-bootstrap-smoke.test.ts +++ b/tests/release-bootstrap-smoke.test.ts @@ -105,6 +105,8 @@ describe("release bootstrap smoke", () => { const wrapperDir = join(distDir, "executor"); const platformDir = join(distDir, currentPlatformPackage); + const wrapperPackage = await readFile(join(wrapperDir, "package.json"), "utf8"); + expect(wrapperPackage).toContain('"executor": "bin.mjs"'); // Simulate the install layout npm/bun produces: // /executor/ <- wrapper (bin, postinstall, package.json) @@ -120,15 +122,20 @@ describe("release bootstrap smoke", () => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: release smoke test must clean temp install files after process checks try { - const firstRun = await runCommand( - process.execPath, - [join(installedWrapperDir, "bin", "executor"), "--help"], - installedWrapperDir, - ); - const combined = `${firstRun.stdout}\n${firstRun.stderr}`; - expect(firstRun.exitCode, combined).toBe(0); - expect(combined).not.toContain("could not locate a platform binary"); - expect(combined).not.toContain("ENOENT"); + for (const entrypoint of [ + join(installedWrapperDir, "bin.mjs"), + join(installedWrapperDir, "bin", "executor"), + ]) { + const firstRun = await runCommand( + process.execPath, + [entrypoint, "--help"], + installedWrapperDir, + ); + const combined = `${firstRun.stdout}\n${firstRun.stderr}`; + expect(firstRun.exitCode, combined).toBe(0); + expect(combined).not.toContain("could not locate a platform binary"); + expect(combined).not.toContain("ENOENT"); + } // The platform binary lives under node_modules//bin/. const platformBinaryPath = join(installedPlatformDir, "bin", currentRuntimeBinaryName);