Skip to content

perf(wrangler): remove execa - #12369

Open
43081j wants to merge 2 commits into
cloudflare:mainfrom
43081j:execaless
Open

perf(wrangler): remove execa#12369
43081j wants to merge 2 commits into
cloudflare:mainfrom
43081j:execaless

Conversation

@43081j

@43081j 43081j commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

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.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s): N/A
    • Documentation not necessary because: Internal code change only

Open with Devin

@43081j
43081j requested review from a team as code owners February 2, 2026 17:32
@changeset-bot

changeset-bot Bot commented Feb 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c47d183

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
wrangler Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Feb 2, 2026
@43081j 43081j changed the title perf: remove execa perf(wrangler): remove execa Feb 2, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Feb 2, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@12369

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@12369

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@12369

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@12369

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@12369

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@12369

miniflare

npm i https://pkg.pr.new/miniflare@12369

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@12369

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@12369

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@12369

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@12369

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@12369

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@12369

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@12369

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@12369

wrangler

npm i https://pkg.pr.new/wrangler@12369

commit: c47d183

devin-ai-integration[bot]

This comment was marked as resolved.

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @43081j!

@github-project-automation github-project-automation Bot moved this from Untriaged to Approved in workers-sdk Feb 3, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

@petebacondarwin
petebacondarwin force-pushed the execaless branch 4 times, most recently from 3723c61 to 56b361e Compare February 3, 2026 20:50
@petebacondarwin

Copy link
Copy Markdown
Contributor

The windows CI job appears to keep hanging but also this needs another rebase.

@43081j

43081j commented Feb 3, 2026

Copy link
Copy Markdown
Contributor Author

Strange. When I'm next on a Windows machine I can try run them locally and see what happens if that helps

@petebacondarwin petebacondarwin moved this from Approved to In Progress in workers-sdk Feb 4, 2026
@petebacondarwin
petebacondarwin marked this pull request as draft February 4, 2026 09:42
@43081j
43081j force-pushed the execaless branch 2 times, most recently from 40277f9 to 5f1a98a Compare February 7, 2026 10:14
@petebacondarwin

Copy link
Copy Markdown
Contributor

Hi @43081j — we're doing some cleanup of stale draft PRs.

This PR has been quiet since 2026-02-13. Could you let us know within 14 days whether you're still planning to continue?

  • If yes: please leave a comment with a status update or push a rebase, and we'll keep it open.
  • If no: no need to reply — we'll close it after 14 days, and you can always reopen later if you pick it back up.

Thanks!

@workers-devprod workers-devprod left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeowners reviews satisfied

@github-project-automation github-project-automation Bot moved this from In Progress to Approved in workers-sdk Apr 28, 2026
@43081j
43081j marked this pull request as ready for review April 28, 2026 13:43
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@43081j

43081j commented Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

@petebacondarwin i've revived it, thanks for the ping as i also forgot about this one.

assuming CI is happy, let me know if there's any changes you or any other maintainers want

devin-ai-integration[bot]

This comment was marked as resolved.

@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

devin-ai-integration[bot]

This comment was marked as resolved.

Comment thread packages/wrangler/src/package-manager.ts Outdated
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@43081j

43081j commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@jamesopstad other than that seemingly stuck windows task, it builds!

looks like it is just stuck building for some reason

@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

devin-ai-integration[bot]

This comment was marked as resolved.

@petebacondarwin

Copy link
Copy Markdown
Contributor

Rebased this onto latest main (it was ~450 commits behind and conflicting) and fixed up a few things found along the way. The migration commit is preserved with your authorship; my changes are in a separate follow-up commit.

Conflict resolution

run-custom-build.ts gained AbortSignal + tree-kill teardown on main since this was opened. Merged that with the tinyexec change and retyped the helper from ExecaChildProcess to tinyexec's Result.

Behavioural gaps between execa and tinyexec

  1. Signal termination was silently treated as success. throwOnError only fires for non-zero exit codes. A process killed by a signal reports exitCode === undefined and tinyexec resolves — execa rejected. For custom builds this meant an aborted build looked like a successful one. Both existing abort tests in custom-build.test.ts fail without a guard, so this would have shipped a real regression in wrangler dev. Guarded at all three call sites.

  2. C3 failure messages. NonZeroExitError.message is just Process exited with non-zero status (1) — it doesn't name the command the way execa's shortMessage did. Concatenating the captured stdout/stderr to compensate had three problems: the output is already streamed live so it printed twice; the command name was still lost; and every failure got a unique message, which defeats Sentry grouping (the whole point of the original comment there). Now the message is `npm create cloudflare` failed with exit code 1 and the captured output travels on the cause, which is what Sentry reports.

  3. nodePath. Defaults to true, which prepends every ancestor node_modules/.bin and dirname(process.execPath) to PATH. execa v6 didn't (preferLocal defaulted to false). You had this on init/package-manager but not run-custom-build; set consistently.

  4. execaCommand normalised whitespace (it split the string into file + args). We now hand the raw string to the shell, so command.trim() — on Windows the shell is cmd.exe /d /s /c "<command>", which is not whitespace tolerant.

Other

  • package-manager.supports() now checks exitCode === 0 instead of relying on throwOnError, which covers non-zero exits, signals and ENOENT in one go.
  • e as NonZeroExitErrorinstanceof; the cast was unsound for ENOENT (a plain Error with no .output).
  • The tinyexec mock in vitest.setup.ts returned a bare Promise, so .process was undefined and the piping path was a silent no-op in tests. Now returns something Result-shaped.
  • Added tests for the signal path and the C3 failure paths (all verified to fail without the corresponding fix).
  • Regenerated the lockfile; dropped execa from the cSpell dictionary.

Not addressed — worth a follow-up

execa v6 defaulted to cleanup: true, registering a signal-exit hook that killed the direct child when the parent exited. tinyexec has no equivalent and wrangler has no global child cleanup, so a subprocess can now be orphaned if wrangler exits without going through the abort path (deploy/versions upload/types custom builds and autoconfig pass no AbortSignal). It's narrow — Ctrl-C still kills the process group on POSIX, and dev teardown is handled explicitly by BundlerController — and execa only killed the direct child rather than the tree, so it was already partial. Still, it deserves its own PR rather than being bolted on here.

Verification: pnpm check clean; full wrangler suite (4826) and C3 (241) pass locally. The four red checks on the previous run (Tests (Windows, fixtures), Tests (Windows, packages-and-tools), C3 E2E (pnpm@11.5.1) - frameworks, Vite Plugin Playground (macos, vite-8)) are pre-existing flakes — they reproduce on unrelated PRs (#14924, #14952), and wrangler's own tests passed in the Windows run.

One thing worth adding to the changeset if you like: x(command, [], { shell: true }) passes zero args, which sidesteps Node's DEP0190 deprecation warning that execaCommand triggered on Node >= 22.

devin-ai-integration[bot]

This comment was marked as resolved.

@43081j

43081j commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author
  1. Signal termination was silently treated as success. throwOnError only fires for non-zero exit codes. A process killed by a signal reports exitCode === undefined and tinyexec resolves — execa rejected

this is a bug and was fixed in tinylibs/tinyexec#146 which should be in the next tinyexec release.

2. C3 failure messages. NonZeroExitError.message is just Process exited with non-zero status (1) — it doesn't name the command the way execa's shortMessage did. Concatenating the captured stdout/stderr to compensate had three problems: the output is already streamed live so it printed twice; the command name was still lost; and every failure got a unique message, which defeats Sentry grouping (the whole point of the original comment there). Now the message is `npm create cloudflare` failed with exit code 1 and the captured output travels on the cause, which is what Sentry reports.

This may be solved by tinylibs/tinyexec#151

execa v6 defaulted to cleanup: true, registering a signal-exit hook that killed the direct child when the parent exited. [...]

signal-exit is basically a brute-force, catch-all solution too. basically listens on a whole bunch of signals and events in the hope that it can brute force some code into running before the process exits.

i do wonder if there is a simpler way, knowing exactly what we've spawned and the cases in which it can be left dangling. worth some thought instead of trying to port a "lazy" solution like that i think

edit: 1.3.0 is out which should fix those two 👀

43081j and others added 2 commits August 12, 2026 07:01
Two behavioural differences surfaced while migrating:

- `throwOnError` only covers non-zero exit codes, so a process killed by a
  signal resolved successfully where execa rejected. For custom builds this
  silently turned an aborted build into a successful one. Guard all three
  call sites on a missing exit code.

- `NonZeroExitError.message` does not name the command the way execa's
  `shortMessage` did. Embedding C3's captured stdout/stderr to compensate
  printed the output twice (it is already streamed live) and gave every
  failure a unique message, defeating Sentry grouping. Name the command and
  exit code instead, and let the output travel on the cause.

Also narrows the C3 error with `instanceof` rather than an unchecked cast,
trims the custom build command before handing it to the shell (`cmd.exe` is
not whitespace tolerant, and `execaCommand` used to normalise this), and
adds coverage for the abort, signal and C3 failure paths.
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@petebacondarwin

Copy link
Copy Markdown
Contributor

We recently landed #14994, a large change to Miniflare's configuration internals that touched ~177 files across the repo. Leaving this PR on its old base was likely to cause conflicts, so we've rebased it onto the latest main and force-pushed the result.

Your local copy of this branch is now out of date. Before you push again, please reset to the new version:

git fetch origin
git checkout execaless
git reset --hard origin/execaless

Because the base moved a long way, it's also worth reinstalling before you carry on — the lockfile changed:

pnpm install

Sorry for the interruption. If the rebase looks wrong, or CI now fails in a way that seems related to the Miniflare config change rather than your own work, comment here and we'll help get it sorted.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +89 to +96
const { exitCode } = await x(name, ["--version"], {
nodeOptions: { stdio: "ignore" },
// Disable tinyexec's default PATH manipulation, which prepends every
// ancestor `node_modules/.bin` and the directory holding the running Node
// binary, so that we only detect package managers that are actually
// available on the user's own PATH.
nodePath: false,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Package managers may no longer be found on Windows, breaking project creation

Package managers are now looked up by launching them directly (x(name, ["--version"], …) at packages/wrangler/src/package-manager.ts:89) instead of through the previous helper that knew how to start Windows command shims, so on Windows npm/pnpm/yarn can appear to be missing.
Impact: Windows users may see "Unable to find a package manager" and wrangler init can fail to start the project-creation tool.

Mechanism: execa used cross-spawn for Windows `.cmd` shims, tinyexec spawns directly

execa@6 depends on cross-spawn, which exists precisely to make spawn("npm", …) work on Windows, where npm/pnpm/yarn are npm.cmd shims. libuv's PATH search only appends .com and .exe, so a bare spawn("npm") without shell: true fails with ENOENT (and Node rejects an explicit .cmd target without shell). tinyexec calls node:child_process.spawn directly and does not perform cross-spawn-style shim resolution.

Two call sites are affected:

  • packages/wrangler/src/package-manager.ts:89-96 — a spawn failure is swallowed by the catch and reported as "not supported", so every detection returns false and getPackageManager() throws the "Unable to find a package manager" UserError.
  • packages/wrangler/src/init.ts:139x(packageManager.type, c3Arguments, …) spawns the package manager binary directly to delegate to C3.

The existing Windows test path exercises exactly this: packages/wrangler/src/__tests__/helpers/mock-bin.ts:22-27 creates the mock binaries as cmd-shims on win32.

Note that runCommand in run-custom-build.ts is unaffected because it passes shell: true.

Prompt for agents
On Windows, `npm`, `pnpm` and `yarn` are `.cmd` shims. `execa` (the previous implementation) depended on `cross-spawn`, which resolves and runs those shims correctly; `tinyexec` calls `node:child_process.spawn` directly, and libuv's PATH search only tries `.com`/`.exe`, so `x("npm", ["--version"])` fails with ENOENT on Windows. Verify tinyexec@1.2.4's behaviour here (run the Windows unit tests for `packages/wrangler/src/__tests__/package-manager.test.ts`, which install cmd-shims via `helpers/mock-bin.ts`). If it does not resolve shims, both `supports()` in packages/wrangler/src/package-manager.ts and the C3 delegation spawn in packages/wrangler/src/init.ts need a Windows-safe strategy (e.g. keeping cross-spawn for these two call sites, appending `.cmd` on win32, or passing `shell: true` with properly escaped arguments).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tinyexec does the same work as cross-spawn under the hood, so this comment doesn't seem right 👀

It too handles windows specific resolution of binaries

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Approved

Development

Successfully merging this pull request may close these issues.

5 participants