Skip to content

Commit a613735

Browse files
committed
perf: remove execa
This removes `execa` and uses the much smaller, more modern `tinyexec` instead. Reasoning: - `execa` 640KB vs `tinyexec` 26KB - `execa` 23 packages vs `tinyexec` 1 package - `tinyexec` is widely adopted by most of the modern CLIs today (tsdown, vite, vitest, storybook, etc.) Part of #11854.
1 parent bd94600 commit a613735

File tree

10 files changed

+72
-76
lines changed

10 files changed

+72
-76
lines changed

.changeset/thin-ghosts-shout.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Migrate to `tinyexec` for process execution.

packages/create-cloudflare/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
"dotenv": "^16.0.0",
6969
"esbuild": "catalog:default",
7070
"eslint": "catalog:default",
71-
"execa": "^7.1.1",
7271
"exit-hook": "2.2.1",
7372
"get-port": "^7.1.0",
7473
"glob": "^10.3.3",

packages/wrangler/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@
129129
"dotenv-expand": "^12.0.2",
130130
"eslint": "catalog:default",
131131
"esprima": "4.0.1",
132-
"execa": "^6.1.0",
133132
"find-up": "^6.3.0",
134133
"get-port": "^7.0.0",
135134
"glob-to-regexp": "^0.4.1",
@@ -160,6 +159,7 @@
160159
"source-map": "^0.6.1",
161160
"supports-color": "^9.2.2",
162161
"timeago.js": "^4.0.2",
162+
"tinyexec": "^1.0.2",
163163
"ts-dedent": "^2.2.0",
164164
"ts-json-schema-generator": "^1.5.0",
165165
"tsup": "8.3.0",

packages/wrangler/src/__tests__/init.test.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fs from "node:fs";
22
import path from "node:path";
3-
import { execa } from "execa";
3+
import { x } from "tinyexec";
44
import { http, HttpResponse } from "msw";
55
import * as TOML from "smol-toml";
66
import dedent from "ts-dedent";
@@ -72,23 +72,27 @@ describe("init", () => {
7272
}
7373
`);
7474

75-
expect(execa).toHaveBeenCalledWith(
75+
expect(x).toHaveBeenCalledWith(
7676
"mockpm",
7777
["create", "cloudflare@^2.5.0"],
7878
{
79-
stdio: ["inherit", "pipe", "pipe"],
79+
nodeOptions: {
80+
stdio: ["inherit", "pipe", "pipe"],
81+
},
8082
}
8183
);
8284
});
8385

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

87-
expect(execa).toHaveBeenCalledWith(
89+
expect(x).toHaveBeenCalledWith(
8890
"mockpm",
8991
["create", "cloudflare@^2.5.0", "--wrangler-defaults"],
9092
{
91-
stdio: ["inherit", "pipe", "pipe"],
93+
nodeOptions: {
94+
stdio: ["inherit", "pipe", "pipe"],
95+
},
9296
}
9397
);
9498
});
@@ -124,23 +128,27 @@ describe("init", () => {
124128
}
125129
`);
126130

127-
expect(execa).toHaveBeenCalledWith(
131+
expect(x).toHaveBeenCalledWith(
128132
"mockpm",
129133
["run", "create-cloudflare"],
130134
{
131-
stdio: ["inherit", "pipe", "pipe"],
135+
nodeOptions: {
136+
stdio: ["inherit", "pipe", "pipe"],
137+
},
132138
}
133139
);
134140
});
135141

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

139-
expect(execa).toHaveBeenCalledWith(
145+
expect(x).toHaveBeenCalledWith(
140146
"mockpm",
141147
["run", "create-cloudflare", "--wrangler-defaults"],
142148
{
143-
stdio: ["inherit", "pipe", "pipe"],
149+
nodeOptions: {
150+
stdio: ["inherit", "pipe", "pipe"],
151+
},
144152
}
145153
);
146154
});
@@ -155,14 +163,16 @@ describe("init", () => {
155163
});
156164
await runWrangler("init");
157165

158-
expect(execa).toHaveBeenCalledWith(
166+
expect(x).toHaveBeenCalledWith(
159167
"mockpm",
160168
["create", "cloudflare@^2.5.0"],
161169
{
162-
env: {
163-
CREATE_CLOUDFLARE_TELEMETRY_DISABLED: "1",
170+
nodeOptions: {
171+
env: {
172+
CREATE_CLOUDFLARE_TELEMETRY_DISABLED: "1",
173+
},
174+
stdio: ["inherit", "pipe", "pipe"],
164175
},
165-
stdio: ["inherit", "pipe", "pipe"],
166176
}
167177
);
168178
});
@@ -805,8 +815,8 @@ describe("init", () => {
805815
}
806816
`);
807817

808-
expect(execa).toHaveBeenCalledTimes(1);
809-
expect(execa).toHaveBeenCalledWith(
818+
expect(x).toHaveBeenCalledTimes(1);
819+
expect(x).toHaveBeenCalledWith(
810820
"mockpm",
811821
[
812822
"create",
@@ -816,7 +826,9 @@ describe("init", () => {
816826
"existing-memory-crystal",
817827
],
818828
{
819-
stdio: ["inherit", "pipe", "pipe"],
829+
nodeOptions: {
830+
stdio: ["inherit", "pipe", "pipe"],
831+
},
820832
}
821833
);
822834
});

packages/wrangler/src/__tests__/pages/deploy.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdirSync, writeFileSync } from "node:fs";
22
import { chdir } from "node:process";
33
import { writeWranglerConfig } from "@cloudflare/workers-utils/test-helpers";
4-
import { execa } from "execa";
4+
import { x } from "tinyexec";
55
import { http, HttpResponse } from "msw";
66
import TOML from "smol-toml";
77
import dedent from "ts-dedent";
@@ -1769,7 +1769,7 @@ describe("pages deploy", () => {
17691769
it("should not error when deploying a new project with a new repo", async () => {
17701770
vi.stubEnv("CI", "false");
17711771
setIsTTY(true);
1772-
await execa("git", ["init"]);
1772+
await x("git", ["init"]);
17731773
writeFileSync("logo.png", "foobar");
17741774
mockGetUploadTokenRequest(
17751775
"<<funfetti-auth-jwt>>",

packages/wrangler/src/__tests__/vitest.setup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,14 @@ vi.mock("prompts", () => {
216216
};
217217
});
218218

219-
vi.mock("execa", async (importOriginal) => {
220-
const realModule = await importOriginal<typeof import("execa")>();
219+
vi.mock("tinyexec", async (importOriginal) => {
220+
const realModule = await importOriginal<typeof import("tinyexec")>();
221221
return {
222222
...realModule,
223-
execa: vi.fn((...args: Parameters<typeof realModule.execa>) => {
223+
x: vi.fn((...args: Parameters<typeof realModule.x>) => {
224224
return args[0] === "mockpm"
225225
? Promise.resolve()
226-
: realModule.execa(...args);
226+
: realModule.x(...args);
227227
}),
228228
};
229229
});

packages/wrangler/src/deployment-bundle/run-custom-build.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from "node:path";
33
import { Writable } from "node:stream";
44
import { configFileName, UserError } from "@cloudflare/workers-utils";
55
import chalk from "chalk";
6-
import { execaCommand } from "execa";
6+
import { x } from "tinyexec";
77
import { logger } from "../logger";
88
import type { Config } from "@cloudflare/workers-utils";
99

@@ -14,11 +14,14 @@ export async function runCommand(
1414
) {
1515
logger.log(chalk.blue(prefix), "Running:", command);
1616
try {
17-
const res = execaCommand(command, {
18-
shell: true,
19-
cwd,
17+
const res = x(command, [], {
18+
nodeOptions: {
19+
shell: true,
20+
cwd,
21+
},
22+
throwOnError: true,
2023
});
21-
res.stdout?.pipe(
24+
res.process?.stdout?.pipe(
2225
new Writable({
2326
write(chunk: Buffer, _, callback) {
2427
const lines = chunk.toString().split("\n");
@@ -29,7 +32,7 @@ export async function runCommand(
2932
},
3033
})
3134
);
32-
res.stderr?.pipe(
35+
res.process?.stderr?.pipe(
3336
new Writable({
3437
write(chunk: Buffer, _, callback) {
3538
const lines = chunk.toString().split("\n");

packages/wrangler/src/init.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
getC3CommandFromEnv,
77
UserError,
88
} from "@cloudflare/workers-utils";
9-
import { execa } from "execa";
9+
import { x, type NonZeroExitError } from "tinyexec";
1010
import { fetchResult } from "./cfetch";
1111
import { fetchWorkerDefinitionFromDash } from "./cfetch/internal";
1212
import { createCommand } from "./core/create-command";
@@ -20,7 +20,6 @@ import * as shellquote from "./utils/shell-quote";
2020
import { isWorkerNotFoundError } from "./utils/worker-not-found-error";
2121
import type { PackageManager } from "./package-manager";
2222
import type { ServiceMetadataRes } from "@cloudflare/workers-utils";
23-
import type { ExecaError } from "execa";
2423
import type { ReadableStream } from "node:stream/web";
2524

2625
export const init = createCommand({
@@ -135,25 +134,28 @@ export const init = createCommand({
135134
// if telemetry is disabled in wrangler, prevent c3 from sending metrics too
136135
const metricsConfig = readMetricsConfig();
137136
try {
138-
const childProcess = execa(packageManager.type, c3Arguments, {
139-
// Note: we need to pipe stdout and stderr otherwise execa won't include
140-
// those in the command's result/error, but we want it to so that we
141-
// can include those in the error Sentry receives
142-
stdio: ["inherit", "pipe", "pipe"],
143-
...(metricsConfig.permission?.enabled === false && {
144-
env: { CREATE_CLOUDFLARE_TELEMETRY_DISABLED: "1" },
145-
}),
137+
const childProcess = x(packageManager.type, c3Arguments, {
138+
nodeOptions: {
139+
// Note: we need to pipe stdout and stderr otherwise they won't be included
140+
// in the command's result/error, but we want it to so that we
141+
// can include those in the error Sentry receives
142+
stdio: ["inherit", "pipe", "pipe"],
143+
...(metricsConfig.permission?.enabled === false && {
144+
env: { CREATE_CLOUDFLARE_TELEMETRY_DISABLED: "1" },
145+
}),
146+
},
147+
throwOnError: true,
146148
});
147-
childProcess.stdout?.pipe(process.stdout);
148-
childProcess.stderr?.pipe(process.stderr);
149+
childProcess.process?.stdout?.pipe(process.stdout);
150+
childProcess.process?.stderr?.pipe(process.stderr);
149151
await childProcess;
150152
} catch (e: unknown) {
151-
const execaError = e as ExecaError;
152-
throw new Error(execaError.shortMessage, {
153-
// We include the execaError as the cause, in this way this
153+
const procError = e as NonZeroExitError;
154+
throw new Error(procError.message, {
155+
// We include the process error as the cause, in this way this
154156
// will be reflected in Sentry allowing us to better monitor
155157
// C3 errors
156-
cause: execaError,
158+
cause: procError,
157159
});
158160
}
159161
}

packages/wrangler/src/package-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { env } from "node:process";
22
import { UserError } from "@cloudflare/workers-utils";
3-
import { execaCommandSync } from "execa";
3+
import { x } from "tinyexec";
44
import { logger } from "./logger";
55

66
export interface PackageManager {
@@ -102,7 +102,7 @@ export const BunPackageManager: PackageManager = {
102102

103103
async function supports(name: string): Promise<boolean> {
104104
try {
105-
execaCommandSync(`${name} --version`, { stdio: "ignore" });
105+
await x(name, ['--version'], { nodeOptions: { stdio: "ignore" }, throwOnError: true });
106106
return true;
107107
} catch {
108108
return false;

pnpm-lock.yaml

Lines changed: 3 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)