Skip to content

Commit a343fe6

Browse files
petebacondarwinpenalosa
authored andcommitted
[vitest-pool-workers] Stop externalizing devDependencies from the published bundle (#13933)
1 parent cb52154 commit a343fe6

11 files changed

Lines changed: 842 additions & 31 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@cloudflare/vitest-pool-workers": patch
3+
---
4+
5+
Derive bundler externals from `package.json` and shrink the published bundle
6+
7+
The bundler's `external` list was previously hand-maintained and out of sync with `package.json``undici` and `semver` were both listed as external despite being only `devDependencies`. The published `dist/pool/index.mjs` consequently contained a top-level `import { fetch } from "undici"` that was only resolvable because pnpm happened to hoist `undici` from other packages' devDependencies during local development.
8+
9+
The bundler now derives its `external` list from `dependencies` + `peerDependencies` in `package.json`, making it impossible for a `devDependency` to silently end up externalized.
10+
11+
Combined with the new `"sideEffects": false` declaration in `@cloudflare/workers-utils`, the unused `cloudflared` / `tunnel` exports (and their transitive `undici` import) are now tree-shaken out of the pool entirely. `dist/pool/index.mjs` no longer references `undici` at all, and shrinks from ~489 KB to ~125 KB.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@cloudflare/workers-utils": patch
3+
---
4+
5+
Mark `@cloudflare/workers-utils` as side-effect-free and properly declare `undici` as a runtime dependency
6+
7+
The package now declares `"sideEffects": false` in its `package.json` so that downstream bundlers can tree-shake unused exports. In particular, consumers that only use a subset of the package (for example, `getTodaysCompatDate` from the main entry) will no longer carry the `cloudflared` / `tunnel` exports — or their transitive dependencies — in their final bundle.
8+
9+
`undici` has been moved from `devDependencies` to `dependencies`. Previously it was incorrectly listed as a devDependency while the bundler config marked it as external, leaving the published `dist/index.mjs` with an unresolved `import { fetch } from "undici"` for anyone installing the package directly. `undici` is deliberately kept external (rather than bundled) so that downstream consumers don't end up with two copies of `undici` in their bundle — which would break `instanceof Request`/`Response`/`Headers` checks across the boundary and prevent `setGlobalDispatcher` / proxy configuration from applying to the bundled copy.
10+
11+
`vitest` has been added as an optional `peerDependency` because the `./test-helpers` sub-export uses `vitest`'s `vi`, `beforeEach`, and `afterEach` APIs at runtime; consumers that import from `./test-helpers` must have `vitest` installed themselves.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Dependencies that _are not_ bundled along with create-cloudflare.
3+
*
4+
* create-cloudflare bundles all of its dependencies into a single CJS file,
5+
* so this list is currently empty.
6+
*/
7+
export const EXTERNAL_DEPENDENCIES: string[] = [];
8+
9+
/**
10+
* Bare-specifier imports that legitimately appear in create-cloudflare's
11+
* bundled output but should NOT be treated as missing runtime dependencies.
12+
*/
13+
export const IGNORED_DIST_IMPORTS = [
14+
// `recast` (a bundled devDependency) attempts `require("babylon")` inside
15+
// a try/catch as a fallback parser when `@babel/parser` is not available.
16+
// We don't need to ship `babylon` because the primary `@babel/parser`
17+
// path is bundled and always succeeds.
18+
"babylon",
19+
];

packages/vitest-pool-workers/tsdown.config.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readdirSync } from "node:fs";
1+
import { readdirSync, readFileSync } from "node:fs";
22
import path from "node:path";
33
import { defineConfig } from "tsdown";
44
import { getBuiltinModules } from "./scripts/rtti/query.mjs";
@@ -23,6 +23,28 @@ const libPaths = [
2323
...walk(path.join(pkgRoot, "src/worker/node")),
2424
];
2525

26+
// Derive bundler externals from package.json so devDependencies are always
27+
// bundled and runtime dependencies/peer dependencies are always external.
28+
// This prevents drift between package.json and the bundler config — the
29+
// previous hand-maintained list incorrectly externalized undici and semver
30+
// (both devDependencies), leaving the published bundle with unresolved
31+
// imports for users who don't have those packages installed transitively.
32+
const pkg = JSON.parse(
33+
readFileSync(path.join(pkgRoot, "package.json"), "utf-8")
34+
) as {
35+
dependencies?: Record<string, string>;
36+
peerDependencies?: Record<string, string>;
37+
};
38+
const runtimeDeps = [
39+
...Object.keys(pkg.dependencies ?? {}),
40+
...Object.keys(pkg.peerDependencies ?? {}),
41+
];
42+
// Match the bare package name and any subpath import (e.g. `vitest/node`).
43+
const runtimeDepPatterns = runtimeDeps.flatMap((name) => [
44+
name,
45+
new RegExp(`^${name.replace(/[/\\^$+?.()|[\]{}]/g, "\\$&")}/`),
46+
]);
47+
2648
const commonOptions: UserConfig = {
2749
platform: "node",
2850
target: "esnext",
@@ -36,22 +58,8 @@ const commonOptions: UserConfig = {
3658
// Virtual/runtime modules
3759
"__VITEST_POOL_WORKERS_DEFINES",
3860
"__VITEST_POOL_WORKERS_USER_OBJECT",
39-
// All npm packages (previously handled by packages: "external")
40-
"cjs-module-lexer",
41-
"esbuild",
42-
"miniflare",
43-
"semver",
44-
"semver/*",
45-
"wrangler",
46-
"zod",
47-
"undici",
48-
"undici/*",
49-
// Peer dependencies
50-
"vitest",
51-
"vitest/*",
52-
"@vitest/runner",
53-
"@vitest/snapshot",
54-
"@vitest/snapshot/*",
61+
// Runtime dependencies and peer dependencies (derived from package.json)
62+
...runtimeDepPatterns,
5563
],
5664
sourcemap: true,
5765
outDir: path.join(pkgRoot, "dist"),

packages/workers-utils/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"files": [
1717
"dist"
1818
],
19+
"sideEffects": false,
1920
"exports": {
2021
".": {
2122
"browser": "./dist/browser.mjs",
@@ -40,6 +41,9 @@
4041
"test:ci": "vitest run",
4142
"type:tests": "tsc -p ./tests/tsconfig.json"
4243
},
44+
"dependencies": {
45+
"undici": "catalog:default"
46+
},
4347
"devDependencies": {
4448
"@cloudflare/workers-shared": "workspace:*",
4549
"@cloudflare/workers-tsconfig": "workspace:*",
@@ -57,10 +61,17 @@
5761
"tsdown": "^0.15.9",
5862
"tsup": "8.3.0",
5963
"typescript": "catalog:default",
60-
"undici": "catalog:default",
6164
"vitest": "catalog:default",
6265
"xdg-app-paths": "^8.3.0"
6366
},
67+
"peerDependencies": {
68+
"vitest": "^4.1.0"
69+
},
70+
"peerDependenciesMeta": {
71+
"vitest": {
72+
"optional": true
73+
}
74+
},
6475
"volta": {
6576
"extends": "../../package.json"
6677
},
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Dependencies that _are not_ bundled along with @cloudflare/workers-utils.
3+
*
4+
* These must be explicitly documented with a reason why they cannot be bundled.
5+
* This list is validated by `tools/deployments/validate-package-dependencies.ts`.
6+
*/
7+
export const EXTERNAL_DEPENDENCIES = [
8+
// Bundling `undici` would produce a duplicate copy in every downstream
9+
// consumer that already depends on undici (e.g. wrangler), which breaks
10+
// `instanceof Request`/`Response`/`Headers` checks across the boundary
11+
// and prevents `setGlobalDispatcher` / proxy configuration from applying
12+
// to the bundled copy. Keeping it external lets the package manager
13+
// deduplicate undici to a single shared instance.
14+
"undici",
15+
];

packages/workers-utils/tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ export default defineConfig(() => [
2020
define: {
2121
"process.env.NODE_ENV": `'${"production"}'`,
2222
},
23-
external: ["@cloudflare/*", "vitest", "msw", "undici"],
23+
external: ["@cloudflare/*", "vitest", "undici"],
2424
},
2525
]);

packages/wrangler/scripts/deps.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,31 @@ export const EXTERNAL_DEPENDENCIES = [
3737
"workerd",
3838
];
3939

40+
/**
41+
* Bare-specifier imports that legitimately appear in wrangler's bundled
42+
* output but should NOT be treated as missing runtime dependencies.
43+
*
44+
* Each entry must be justified — typically the import is guarded
45+
* (try/catch, optional require) inside a bundled-in third-party library that
46+
* probes for the consumer's environment.
47+
*/
48+
export const IGNORED_DIST_IMPORTS = [
49+
// @netlify/build-info (bundled devDependency) tries to require these
50+
// framework packages to detect what the user has installed. Each call
51+
// is guarded by try/catch and used only for framework detection.
52+
"@angular/ssr",
53+
"@cloudflare/vite-plugin",
54+
"isbot",
55+
"react-dom",
56+
"react-router",
57+
"waku",
58+
59+
// @aws-sdk/client-s3 (bundled devDependency) optionally uses this native
60+
// crypto implementation if installed; falls back to a JS implementation
61+
// when missing.
62+
"@aws-sdk/signature-v4-crt",
63+
];
64+
4065
const pathToPackageJson = path.resolve(__dirname, "..", "package.json");
4166
const packageJson = fs.readFileSync(pathToPackageJson, { encoding: "utf-8" });
4267
const { dependencies, devDependencies } = JSON.parse(packageJson);

pnpm-lock.yaml

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

0 commit comments

Comments
 (0)