Skip to content
Open
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/thin-ghosts-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

Replace `execa` with `tinyexec` for running subprocesses, shrinking the bundled Wrangler output.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"endgroup",
"esbuild",
"eslintcache",
"execa",
"fedramp",
"filestat",
"haikunate",
Expand Down Expand Up @@ -46,6 +45,7 @@
"selfsigned",
"subrequests",
"textfile",
"tinyexec",
"tsbuildinfo",
"turborepo",
"undici",
Expand Down
1 change: 0 additions & 1 deletion packages/create-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"dns2": "^2.1.0",
"dotenv": "catalog:default",
"esbuild": "catalog:default",
"execa": "^7.1.1",
"exit-hook": "2.2.1",
"get-port": "^7.1.0",
"haikunator": "^2.1.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/wrangler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@
"dotenv-expand": "^12.0.2",
"empathic": "^2.0.0",
"esprima": "4.0.1",
"execa": "^6.1.0",
"get-port": "^7.0.0",
"glob-to-regexp": "^0.4.1",
"https-proxy-agent": "7.0.2",
Expand Down Expand Up @@ -173,6 +172,7 @@
"smol-toml": "catalog:default",
"supports-color": "^9.2.2",
"timeago.js": "4.0.2",
"tinyexec": "^1.2.4",
"tree-kill": "catalog:default",
"ts-dedent": "^2.2.0",
"ts-json-schema-generator": "^1.5.0",
Expand Down
12 changes: 12 additions & 0 deletions packages/wrangler/src/__tests__/custom-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ describe("Custom Builds", () => {
}
});

// POSIX-only: `kill` is not a `cmd.exe` command, and Windows has no real
// signals anyway — a terminated process there reports a non-zero exit code,
// which `throwOnError` already covers.
it.skipIf(process.platform === "win32")(
"runCustomBuild throws UserError when a command is terminated by a signal",
async ({ expect }) => {
await expect(
runCustomBuild("/", "/", { command: "kill -TERM $$" }, undefined)
).rejects.toThrow(UserError);
}
);

it("runCommand aborts the custom build command", async ({ expect }) => {
const aborter = new AbortController();
const commandPromise = runCommand(
Expand Down
114 changes: 88 additions & 26 deletions packages/wrangler/src/__tests__/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as fs from "node:fs";
import path from "node:path";
import { runInTempDir } from "@cloudflare/workers-utils/test-helpers";
import { execa } from "execa";
import { http, HttpResponse } from "msw";
import * as TOML from "smol-toml";
import { NonZeroExitError, x } from "tinyexec";
import dedent from "ts-dedent";
import { parseConfigFileTextToJson } from "typescript";
import { FormData } from "undici";
Expand Down Expand Up @@ -76,19 +76,27 @@ describe("init", () => {
}
`);

expect(execa).toHaveBeenCalledWith("mockpm", ["create", "cloudflare"], {
stdio: ["inherit", "pipe", "pipe"],
expect(x).toHaveBeenCalledWith("mockpm", ["create", "cloudflare"], {
nodeOptions: {
stdio: ["inherit", "pipe", "pipe"],
},
throwOnError: true,
nodePath: false,
});
});

it("if `-y` is used, delegate to c3 with --wrangler-defaults", async () => {
await runWrangler("init -y");

expect(execa).toHaveBeenCalledWith(
expect(x).toHaveBeenCalledWith(
"mockpm",
["create", "cloudflare", "--wrangler-defaults"],
{
stdio: ["inherit", "pipe", "pipe"],
nodeOptions: {
stdio: ["inherit", "pipe", "pipe"],
},
throwOnError: true,
nodePath: false,
}
);
});
Expand All @@ -104,9 +112,9 @@ describe("init", () => {
(getPackageManager as Mock).mockResolvedValue(mockPackageManager);

// Update the mock to handle "yarn" for these tests
(execa as Mock).mockImplementation((command: string) => {
(x as Mock).mockImplementation((command: string) => {
if (command === "yarn" || command === "mockpm") {
return Promise.resolve();
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
Expand All @@ -116,15 +124,19 @@ describe("init", () => {
await runWrangler("init");

// No version specifier needed since C3 has auto-update behavior
expect(execa).toHaveBeenCalledWith("yarn", ["create", "cloudflare"], {
stdio: ["inherit", "pipe", "pipe"],
expect(x).toHaveBeenCalledWith("yarn", ["create", "cloudflare"], {
nodeOptions: {
stdio: ["inherit", "pipe", "pipe"],
},
throwOnError: true,
nodePath: false,
});
});

test("uses C3 command without version specifier when using --from-dash with yarn", async () => {
await runWrangler("init --from-dash my-worker");

expect(execa).toHaveBeenCalledWith(
expect(x).toHaveBeenCalledWith(
"yarn",
[
"create",
Expand All @@ -134,7 +146,11 @@ describe("init", () => {
"my-worker",
],
{
stdio: ["inherit", "pipe", "pipe"],
nodeOptions: {
stdio: ["inherit", "pipe", "pipe"],
},
throwOnError: true,
nodePath: false,
}
);
});
Expand Down Expand Up @@ -171,23 +187,27 @@ describe("init", () => {
}
`);

expect(execa).toHaveBeenCalledWith(
"mockpm",
["run", "create-cloudflare"],
{
expect(x).toHaveBeenCalledWith("mockpm", ["run", "create-cloudflare"], {
nodeOptions: {
stdio: ["inherit", "pipe", "pipe"],
}
);
},
throwOnError: true,
nodePath: false,
});
});

it("if `-y` is used, delegate to c3 with --wrangler-defaults", async () => {
await runWrangler("init -y");

expect(execa).toHaveBeenCalledWith(
expect(x).toHaveBeenCalledWith(
"mockpm",
["run", "create-cloudflare", "--wrangler-defaults"],
{
stdio: ["inherit", "pipe", "pipe"],
nodeOptions: {
stdio: ["inherit", "pipe", "pipe"],
},
throwOnError: true,
nodePath: false,
}
);
});
Expand All @@ -202,11 +222,49 @@ describe("init", () => {
});
await runWrangler("init");

expect(execa).toHaveBeenCalledWith("mockpm", ["create", "cloudflare"], {
env: {
CREATE_CLOUDFLARE_TELEMETRY_DISABLED: "1",
expect(x).toHaveBeenCalledWith("mockpm", ["create", "cloudflare"], {
nodeOptions: {
env: {
CREATE_CLOUDFLARE_TELEMETRY_DISABLED: "1",
},
stdio: ["inherit", "pipe", "pipe"],
},
stdio: ["inherit", "pipe", "pipe"],
throwOnError: true,
nodePath: false,
});
});

describe("when C3 fails", () => {
it("reports a stable message that names the command and carries the output on the cause", async ({
expect,
}) => {
const output = { stdout: "some stdout", stderr: "some stderr" };
const cause = new NonZeroExitError(
{ pid: 1234, killed: false, exitCode: 2 },
{ ...output, exitCode: 2 }
);
(x as Mock).mockRejectedValueOnce(cause);

await expect(runWrangler("init")).rejects.toThrow(
expect.objectContaining({
// The message must not embed the command output, otherwise every
// C3 failure becomes a distinct Sentry issue.
message: "`mockpm create cloudflare` failed with exit code 2",
cause,
})
);
});

it("reports a process terminated by a signal as a failure", async ({
expect,
}) => {
// `throwOnError` does not fire for signal termination: tinyexec resolves
// with no exit code at all.
(x as Mock).mockResolvedValueOnce({ stdout: "", stderr: "" });

await expect(runWrangler("init")).rejects.toThrow(
"`mockpm create cloudflare` was terminated by a signal"
);
});
});
});
Expand Down Expand Up @@ -848,8 +906,8 @@ describe("init", () => {
}
`);

expect(execa).toHaveBeenCalledTimes(1);
expect(execa).toHaveBeenCalledWith(
expect(x).toHaveBeenCalledTimes(1);
expect(x).toHaveBeenCalledWith(
"mockpm",
[
"create",
Expand All @@ -859,7 +917,11 @@ describe("init", () => {
"existing-memory-crystal",
],
{
stdio: ["inherit", "pipe", "pipe"],
nodeOptions: {
stdio: ["inherit", "pipe", "pipe"],
},
throwOnError: true,
nodePath: false,
}
);
});
Expand Down
4 changes: 2 additions & 2 deletions packages/wrangler/src/__tests__/pages/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
writeWranglerConfig,
} from "@cloudflare/workers-utils/test-helpers";
import ci from "ci-info";
import { execa } from "execa";
import { http, HttpResponse } from "msw";
import TOML from "smol-toml";
import { x } from "tinyexec";
import dedent from "ts-dedent";
/* eslint-disable-next-line no-restricted-imports --
* Uses expect in MSW handlers outside test callbacks
Expand Down Expand Up @@ -1973,7 +1973,7 @@ describe("pages deploy", () => {
}) => {
vi.mocked(ci).isCI = false;
setIsTTY(true);
await execa("git", ["init"]);
await x("git", ["init"], { throwOnError: true, nodePath: false });
writeFileSync("logo.png", "foobar");
mockGetUploadTokenRequest(
expect,
Expand Down
23 changes: 17 additions & 6 deletions packages/wrangler/src/__tests__/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,25 @@ vi.mock("prompts", () => {
};
});

vi.mock("execa", async (importOriginal) => {
const realModule = await importOriginal<typeof import("execa")>();
vi.mock("tinyexec", async (importOriginal) => {
const realModule = await importOriginal<typeof import("tinyexec")>();
// `x()` returns an `ExecProcess`: a thenable that also exposes the spawned
// process. Mimic enough of that shape that callers which reach for `.process`
// or `.pid` behave the same way they would for a real (already exited)
// command.
const mockResult = () =>
Object.assign(Promise.resolve({ stdout: "", stderr: "", exitCode: 0 }), {
process: undefined,
pid: undefined,
exitCode: 0,
killed: false,
aborted: false,
kill: () => false,
});
return {
...realModule,
execa: vi.fn((...args: Parameters<typeof realModule.execa>) => {
return args[0] === "mockpm"
? Promise.resolve()
: realModule.execa(...args);
x: vi.fn((...args: Parameters<typeof realModule.x>) => {
return args[0] === "mockpm" ? mockResult() : realModule.x(...args);
}),
};
});
Expand Down
Loading
Loading