Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/warm-shims-resolve.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions apps/cli/src/__tests__/wrapper-entrypoints.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
});
});
27 changes: 10 additions & 17 deletions apps/cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -598,17 +599,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
const buildWrapperPackage = async (binaries: Record<string, string>) => {
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"),
Expand All @@ -622,7 +616,7 @@ const buildWrapperPackage = async (binaries: Record<string, string>) => {
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"
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions apps/cli/src/wrapper-entrypoints.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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);
};
25 changes: 16 additions & 9 deletions tests/release-bootstrap-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
// <root>/executor/ <- wrapper (bin, postinstall, package.json)
Expand All @@ -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/<platform-pkg>/bin/.
const platformBinaryPath = join(installedPlatformDir, "bin", currentRuntimeBinaryName);
Expand Down
Loading