Repository: cloudflare/workers-sdk
Pull request #8300: Use @cloudflare/unenv-preset in wrangler
Author: login: vicb
Labels: name: e2e
description: Run wrangler + vite-plugin e2e tests on a PR
Created: 2025-02-28T10:50:43Z | Merged: 2025-03-05T15:25:55Z

To make sure the dependency resolution works fine, I have create 3 projects (`create-cloudflare@latest --framework=nuxt`) with `npm`, `pnpm`, and `yarn`. I then override the wrangler version in the `package.json` with the preview version from this PR.

I then logged the `aliasAbsolute` (in `cli.js`) and checked that all the paths were resolved as expected (i.e. they do not point to unenv@1 which is used by Nuxt).

---

<!--
Please don't delete the checkboxes <3
The following selections do not need to be completed if this PR only contains changes to .md files
-->

- Tests
  - [ ] TODO (before merge)
  - [] Tests included
  - [x] Tests not necessary because: using existing tests
- Wrangler E2E Tests CI Job required? (Use "e2e" label or ask maintainer to run separately)
  - [ ] I don't know
  - [x] Required
  - [ ] Not required because:
- Public documentation
  - [ ] TODO (before merge)
  - [ ] Cloudflare docs PR(s): <!--e.g. <https://github.com/cloudflare/cloudflare-docs/pull/>...-->
  - [x] Documentation not necessary because: no user facing change

<!--
Have you read our [Contributing guide](https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md)?
In particular, for non-trivial changes, please always engage on the issue or create a discussion or feature request issue first before writing your code.
-->

<!-- GitButler Footer Boundary Top -->
---
This is **part 2 of 2 in a stack** made with GitButler:
- <kbd>&nbsp;2&nbsp;</kbd> #8300 👈 
- <kbd>&nbsp;1&nbsp;</kbd> #7915 
<!-- GitButler Footer Boundary Bottom -->




Diff:
diff --git a/.changeset/petite-mangos-wink.md b/.changeset/petite-mangos-wink.md
new file mode 100644
index 0000000000..a67c97265d
--- /dev/null
+++ b/.changeset/petite-mangos-wink.md
@@ -0,0 +1,5 @@
+---
+"wrangler": minor
+---
+
+Use the unenv preset for Cloudflare from `@cloudflare/unenv-preset`
diff --git a/fixtures/nodejs-hybrid-app/src/index.ts b/fixtures/nodejs-hybrid-app/src/index.ts
index 63ef453e1a..1d4fad66ce 100644
--- a/fixtures/nodejs-hybrid-app/src/index.ts
+++ b/fixtures/nodejs-hybrid-app/src/index.ts
@@ -21,13 +21,15 @@ export default {
 			case "/test-random":
 				return testGetRandomValues();
 			case "/test-process":
-				return testProcessBehaviour();
+				return testProcessBehavior();
 			case "/query":
 				return testPostgresLibrary(env, ctx);
 			case "/test-x509-certificate":
 				return testX509Certificate();
 			case "/test-require-alias":
-				return testRequireUenvAliasedPackages();
+				return testRequireUnenvAliasedPackages();
+			case "/test-immediate":
+				return await testImmediate();
 		}
 
 		return new Response(
@@ -36,13 +38,34 @@ export default {
 <a href="test-random">Test getRandomValues()</a>
 <a href="test-x509-certificate">Test X509Certificate</a>
 <a href="test-require-alias">Test require unenv aliased packages</a>
+<a href="test-immediate">Test setImmediate</a>
 `,
 			{ headers: { "Content-Type": "text/html; charset=utf-8" } }
 		);
 	},
 };
 
-function testRequireUenvAliasedPackages() {
+async function testImmediate() {
+	try {
+		await new Promise((resolve, reject) => {
+			// Give it a whole second otherwise it times-out
+			setTimeout(reject, 1000);
+
+			// This setImmediate should never trigger the reject if the clearImmediate is working
+			const id = setImmediate(reject);
+			// clearImmediate should cancel reject callback
+			clearImmediate(id);
+			// This setImmediate should trigger the resolve callback
+			setImmediate(resolve);
+		});
+
+		return new Response("OK");
+	} catch (e) {
+		return new Response(`NOT OK: ${e}`);
+	}
+}
+
+function testRequireUnenvAliasedPackages() {
 	const fetch = require("cross-fetch");
 	const supportsDefaultExports = typeof fetch === "function";
 	const supportsNamedExports = typeof fetch.Headers === "function";
@@ -105,9 +128,12 @@ function testBasicNodejsProperties() {
 	assert.notEqual(Performance, undefined);
 	assert.strictEqual(global.Performance, Performance);
 	assert.strictEqual(globalThis.Performance, Performance);
+
+	assert.strictEqual(typeof performance.measure, "function");
+	assert.strictEqual(typeof performance.clearMarks, "function");
 }
 
-function testProcessBehaviour() {
+function testProcessBehavior() {
 	const originalProcess = process;
 	try {
 		assert.notEqual(process, undefined);
diff --git a/fixtures/nodejs-hybrid-app/tests/index.test.ts b/fixtures/nodejs-hybrid-app/tests/index.test.ts
index d5510c5a5b..d55c1edc91 100644
--- a/fixtures/nodejs-hybrid-app/tests/index.test.ts
+++ b/fixtures/nodejs-hybrid-app/tests/index.test.ts
@@ -58,4 +58,10 @@ describe("nodejs compat", () => {
 		const response = await fetch(`http://${ip}:${port}/test-require-alias`);
 		await expect(response.text()).resolves.toBe(`"OK!"`);
 	});
+
+	test("set/clearImmediate", async ({ expect }) => {
+		const { ip, port } = wrangler;
+		const response = await fetch(`http://${ip}:${port}/test-immediate`);
+		await expect(response.text()).resolves.toBe("OK");
+	});
 });
diff --git a/fixtures/pages-nodejs-v2-compat/tests/index.test.ts b/fixtures/pages-nodejs-v2-compat/tests/index.test.ts
index b00b716201..e19b3e5780 100644
--- a/fixtures/pages-nodejs-v2-compat/tests/index.test.ts
+++ b/fixtures/pages-nodejs-v2-compat/tests/index.test.ts
@@ -15,7 +15,7 @@ describe("Pages with Node.js compat v2", () => {
 			const response = await fetch(`http://${ip}:${port}/`);
 			const body = await response.text();
 			expect(body).toMatchInlineSnapshot(
-				`"_worker.js file, process: _debugEnd,_debugProcess,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_kill,_linkedBinding,_maxListeners,_preload_modules,_rawDebug,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,chdir,config,constrainedMemory,cpuUsage,cwd,debugPort,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,title,umask,uptime,version,versions"`
+				`"_worker.js file, process: _channel,_debugEnd,_debugProcess,_disconnect,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_handleQueue,_kill,_linkedBinding,_maxListeners,_pendingMessage,_preload_modules,_rawDebug,_send,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,channel,chdir,config,connected,constrainedMemory,cpuUsage,cwd,debugPort,disconnect,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,finalization,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,mainModule,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,permission,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,send,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,throwDeprecation,title,traceDeprecation,umask,uptime,version,versions"`
 			);
 		});
 	});
@@ -31,7 +31,7 @@ describe("Pages with Node.js compat v2", () => {
 			const response = await fetch(`http://${ip}:${port}/`);
 			const body = await response.text();
 			expect(body).toMatchInlineSnapshot(
-				`"_worker.js directory, process: _debugEnd,_debugProcess,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_kill,_linkedBinding,_maxListeners,_preload_modules,_rawDebug,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,chdir,config,constrainedMemory,cpuUsage,cwd,debugPort,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,title,umask,uptime,version,versions"`
+				`"_worker.js directory, process: _channel,_debugEnd,_debugProcess,_disconnect,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_handleQueue,_kill,_linkedBinding,_maxListeners,_pendingMessage,_preload_modules,_rawDebug,_send,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,channel,chdir,config,connected,constrainedMemory,cpuUsage,cwd,debugPort,disconnect,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,finalization,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,mainModule,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,permission,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,send,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,throwDeprecation,title,traceDeprecation,umask,uptime,version,versions"`
 			);
 		});
 	});
@@ -47,7 +47,7 @@ describe("Pages with Node.js compat v2", () => {
 			const response = await fetch(`http://${ip}:${port}/`);
 			const body = await response.text();
 			expect(body).toMatchInlineSnapshot(
-				`"Pages functions, process: _debugEnd,_debugProcess,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_kill,_linkedBinding,_maxListeners,_preload_modules,_rawDebug,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,chdir,config,constrainedMemory,cpuUsage,cwd,debugPort,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,title,umask,uptime,version,versions"`
+				`"Pages functions, process: _channel,_debugEnd,_debugProcess,_disconnect,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_handleQueue,_kill,_linkedBinding,_maxListeners,_pendingMessage,_preload_modules,_rawDebug,_send,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,channel,chdir,config,connected,constrainedMemory,cpuUsage,cwd,debugPort,disconnect,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,finalization,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,mainModule,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,permission,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,send,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,throwDeprecation,title,traceDeprecation,umask,uptime,version,versions"`
 			);
 		});
 	});
diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json
index 59f9164630..82e9cbd0d1 100644
--- a/packages/wrangler/package.json
+++ b/packages/wrangler/package.json
@@ -70,13 +70,14 @@
 	},
 	"dependencies": {
 		"@cloudflare/kv-asset-handler": "workspace:*",
+		"@cloudflare/unenv-preset": "2.0.0",
 		"@esbuild-plugins/node-globals-polyfill": "0.2.3",
 		"@esbuild-plugins/node-modules-polyfill": "0.2.2",
 		"blake3-wasm": "2.1.5",
 		"esbuild": "0.17.19",
 		"miniflare": "workspace:*",
 		"path-to-regexp": "6.3.0",
-		"unenv": "2.0.0-rc.1",
+		"unenv": "2.0.0-rc.8",
 		"workerd": "1.20250224.0"
 	},
 	"devDependencies": {
diff --git a/packages/wrangler/scripts/deps.ts b/packages/wrangler/scripts/deps.ts
index 1019e0d225..140de2673e 100644
--- a/packages/wrangler/scripts/deps.ts
+++ b/packages/wrangler/scripts/deps.ts
@@ -25,6 +25,7 @@ export const EXTERNAL_DEPENDENCIES = [
 	// unenv must be external because it contains unenv/runtime code which needs to be resolved
 	// and read when we are bundling the worker application
 	"unenv",
+	"@cloudflare/unenv-preset",
 
 	// path-to-regexp must be external because it contains runtime code which needs to be resolved
 	// and read when we are bundling the worker application.
diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts
index c51910406f..4775c4c478 100644
--- a/packages/wrangler/src/__tests__/deploy.test.ts
+++ b/packages/wrangler/src/__tests__/deploy.test.ts
@@ -10247,7 +10247,7 @@ addEventListener('fetch', event => {});`
 					)
 				)
 			).resolves.toMatchInlineSnapshot(`
-				"X [ERROR] Unexpected external import of \\"node:net\\", \\"node:stream\\", \\"node:timers/promises\\", and \\"node:tty\\".
+				"X [ERROR] Unexpected external import of \\"node:events\\", \\"node:net\\", \\"node:perf_hooks\\", \\"node:stream\\", and \\"node:tty\\".
 				Your worker has no default export, which means it is assumed to be a Service Worker format Worker.
 				Did you mean to create a ES Module format Worker?
 				If so, try adding \`export default { ... }\` in your entry-point.
diff --git a/packages/wrangler/src/__tests__/unenv/unenv.test.ts b/packages/wrangler/src/__tests__/unenv/unenv.test.ts
deleted file mode 100644
index 3caff6f0fc..0000000000
--- a/packages/wrangler/src/__tests__/unenv/unenv.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { cloudflare, defineEnv } from "unenv";
-
-describe("unenv", () => {
-	test("module resolution does not throw", async () => {
-		const env = {
-			alias: {},
-			...cloudflare,
-		};
-
-		env.alias["should/no/throw"] = "if/this/is/not/valid";
-
-		// Throws "Error: ENOTDIR: not a directory, open '.../node_modules/.pnpm/unenv-nightly@2.0.0-20241216-144314-7e05819/node_modules/unenv-nightly/dist/index.mjs/package.json'"
-		// before https://github.com/unjs/unenv/pull/378 is integrated via 2.0.0-20241218-183400-5d6aec3
-		expect(() => {
-			defineEnv({
-				nodeCompat: true,
-				presets: [env],
-				resolve: true,
-			});
-		}).not.toThrow();
-	});
-});
diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts
index 7cc968fcd8..72dd75d249 100644
--- a/packages/wrangler/src/deployment-bundle/bundle.ts
+++ b/packages/wrangler/src/deployment-bundle/bundle.ts
@@ -438,10 +438,10 @@ export async function bundleWorker(
 		plugins: [
 			aliasPlugin,
 			moduleCollector.plugin,
-			...getNodeJSCompatPlugins({
+			...(await getNodeJSCompatPlugins({
 				mode: nodejsCompatMode ?? null,
 				unenvResolvePaths,
-			}),
+			})),
 			cloudflareInternalPlugin,
 			buildResultPlugin,
 			...(plugins || []),
diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
index 6e4fbf93f0..2165ef3917 100644
--- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
+++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
@@ -2,7 +2,6 @@ import assert from "node:assert";
 import { builtinModules } from "node:module";
 import nodePath from "node:path";
 import dedent from "ts-dedent";
-import { cloudflare, env, nodeless } from "unenv";
 import { getBasePath } from "../../paths";
 import type { Plugin, PluginBuild } from "esbuild";
 
@@ -15,15 +14,24 @@ const REQUIRED_UNENV_ALIAS_NAMESPACE = "required-unenv-alias";
  * @param _unenvResolvePaths Root paths used to resolve absolute paths.
  * @returns ESBuild plugin
  */
-export function nodejsHybridPlugin(_unenvResolvePaths?: string[]): Plugin {
-	const { alias, inject, external } = env(nodeless, cloudflare);
+export async function nodejsHybridPlugin(
+	_unenvResolvePaths?: string[]
+): Promise<Plugin> {
+	// `unenv` and `@cloudflare/unenv-preset` only publish esm
+	const { defineEnv } = await import("unenv");
+	const { cloudflare } = await import("@cloudflare/unenv-preset");
+	const { alias, inject, external, polyfill } = defineEnv({
+		presets: [cloudflare],
+		npmShims: true,
+	}).env;
+
 	return {
 		name: "hybrid-nodejs_compat",
 		setup(build) {
 			errorOnServiceWorkerFormat(build);
 			handleRequireCallsToNodeJSBuiltins(build);
 			handleUnenvAliasedPackages(build, alias, external);
-			handleNodeJSGlobals(build, inject);
+			handleNodeJSGlobals(build, inject, polyfill);
 		},
 	};
 }
@@ -104,15 +112,13 @@ function handleRequireCallsToNodeJSBuiltins(build: PluginBuild) {
 function handleUnenvAliasedPackages(
 	build: PluginBuild,
 	alias: Record<string, string>,
-	external: string[]
+	external: readonly string[]
 ) {
 	// esbuild expects alias paths to be absolute
 	const aliasAbsolute: Record<string, string> = {};
 	for (const [module, unresolvedAlias] of Object.entries(alias)) {
 		try {
-			aliasAbsolute[module] = require
-				.resolve(unresolvedAlias)
-				.replace(/\.cjs$/, ".mjs");
+			aliasAbsolute[module] = require.resolve(unresolvedAlias);
 		} catch (e) {
 			// this is an alias for package that is not installed in the current app => ignore
 		}
@@ -128,8 +134,8 @@ function handleUnenvAliasedPackages(
 		// Note: Does not apply to Node.js packages that are handled in `handleRequireCallsToNodeJSBuiltins`
 		if (
 			args.kind === "require-call" &&
-			(unresolvedAlias.startsWith("unenv/runtime/npm/") ||
-				unresolvedAlias.startsWith("unenv/runtime/mock/"))
+			(unresolvedAlias.startsWith("unenv/npm/") ||
+				unresolvedAlias.startsWith("unenv/mock/"))
 		) {
 			return {
 				path: args.path,
@@ -164,13 +170,17 @@ function handleUnenvAliasedPackages(
 }
 
 /**
- * Inject node globals defined in unenv's `inject` config via virtual modules
+ * Inject node globals defined in unenv's preset `inject` and `polyfill` properties.
+ *
+ * - an `inject` injects virtual module defining the name on `globalThis`
+ * - a `polyfill` is injected directly
  */
 function handleNodeJSGlobals(
 	build: PluginBuild,
-	inject: Record<string, string | string[]>
+	inject: Record<string, string | readonly string[]>,
+	polyfill: readonly string[]
 ) {
-	const UNENV_GLOBALS_RE = /_virtual_unenv_global_polyfill-(.+)$/;
+	const UNENV_VIRTUAL_MODULE_RE = /_virtual_unenv_global_polyfill-(.+)$/;
 	const prefix = nodePath.resolve(
 		getBasePath(),
 		"_virtual_unenv_global_polyfill-"
@@ -206,15 +216,19 @@ function handleNodeJSGlobals(
 		injectsByModule.get(module)!.push({ injectedName, exportName, importName });
 	}
 
-	// Inject the virtual modules
 	build.initialOptions.inject = [
 		...(build.initialOptions.inject ?? []),
+		// Inject the virtual modules
 		...virtualModulePathToSpecifier.keys(),
+		// Inject the polyfills - needs an absolute path
+		...polyfill.map((m) => require.resolve(m)),
 	];
 
-	build.onResolve({ filter: UNENV_GLOBALS_RE }, ({ path }) => ({ path }));
+	build.onResolve({ filter: UNENV_VIRTUAL_MODULE_RE }, ({ path }) => ({
+		path,
+	}));
 
-	build.onLoad({ filter: UNENV_GLOBALS_RE }, ({ path }) => {
+	build.onLoad({ filter: UNENV_VIRTUAL_MODULE_RE }, ({ path }) => {
 		const module = virtualModulePathToSpecifier.get(path);
 		assert(module, `Expected ${path} to be mapped to a module specifier`);
 		const injects = injectsByModule.get(module);
diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts
index 30448b347f..14e8c815f1 100644
--- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts
+++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts
@@ -10,13 +10,13 @@ import type { NodeJSCompatMode } from "miniflare";
 /**
  * Returns the list of ESBuild plugins to use for a given compat mode.
  */
-export function getNodeJSCompatPlugins({
+export async function getNodeJSCompatPlugins({
 	mode,
 	unenvResolvePaths,
 }: {
 	mode: NodeJSCompatMode;
 	unenvResolvePaths?: string[];
-}): Plugin[] {
+}): Promise<Plugin[]> {
 	switch (mode) {
 		case "als":
 			return [asyncLocalStoragePlugin, nodejsCompatPlugin(mode)];
@@ -30,7 +30,7 @@ export function getNodeJSCompatPlugins({
 		case "v1":
 			return [nodejsCompatPlugin(mode)];
 		case "v2":
-			return [nodejsHybridPlugin(unenvResolvePaths)];
+			return [await nodejsHybridPlugin(unenvResolvePaths)];
 		case null:
 			return [nodejsCompatPlugin(mode)];
 	}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bafe115ea9..f4cdd7d724 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2719,6 +2719,9 @@ importers:
       '@cloudflare/kv-asset-handler':
         specifier: workspace:*
         version: link:../kv-asset-handler
+      '@cloudflare/unenv-preset':
+        specifier: 2.0.0
+        version: 2.0.0(unenv@2.0.0-rc.8)(workerd@1.20250224.0)
       '@esbuild-plugins/node-globals-polyfill':
         specifier: 0.2.3
         version: 0.2.3(esbuild@0.17.19)
@@ -2738,8 +2741,8 @@ importers:
         specifier: 6.3.0
         version: 6.3.0
       unenv:
-        specifier: 2.0.0-rc.1
-        version: 2.0.0-rc.1
+        specifier: 2.0.0-rc.8
+        version: 2.0.0-rc.8
       workerd:
         specifier: 1.20250224.0
         version: 1.20250224.0
@@ -3579,6 +3582,15 @@ packages:
       workerd:
         optional: true
 
+  '@cloudflare/unenv-preset@2.0.0':
+    resolution: {integrity: sha512-Ar4HixFYP8e990JPACno3nqe10QsjS3yVWr48z5Vop5LygdnvPa5cfNHxGoQSPavvg5aaGnF0VAWc3JJ1tBKuQ==}
+    peerDependencies:
+      unenv: 2.0.0-rc.8
+      workerd: ^1.20250124.0
+    peerDependenciesMeta:
+      workerd:
+        optional: true
+
   '@cloudflare/util-en-garde@8.0.10':
     resolution: {integrity: sha512-qdCFf90hoZzT4o4xEmxOKUf9+bEJNGh4ANnRYApo6BMyVnHoHEHAQ3nWmGSHBmo+W9hOk2Ik7r1oHLbI0O/RRg==}
 
@@ -12814,6 +12826,12 @@ snapshots:
     optionalDependencies:
       workerd: 1.20250224.0
 
+  '@cloudflare/unenv-preset@2.0.0(unenv@2.0.0-rc.8)(workerd@1.20250224.0)':
+    dependencies:
+      unenv: 2.0.0-rc.8
+    optionalDependencies:
+      workerd: 1.20250224.0
+
   '@cloudflare/util-en-garde@8.0.10':
     dependencies:
       fp-ts: 2.13.1


Reviews:
user: login: petebacondarwin
body: I think it is time to move to a workspace dependency on this package.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T13:30:23Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T07:17:30Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T07:26:50Z
user: login: petebacondarwin
body: OK let's land this and iterate. I am still unhappy with the dependency setup but we can work on that. Just for clarity here is a diagram of the current situation:

![image](https://github.com/user-attachments/assets/bf6cf0cc-9898-44ec-850c-f71d679869ae)



state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:01:58Z
user: login: petebacondarwin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:56:29Z
user: login: petebacondarwin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:56:59Z
user: login: vicb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:09:25Z

Review comments:
diff_hunk: @@ -15,15 +14,24 @@ const REQUIRED_UNENV_ALIAS_NAMESPACE = "required-unenv-alias";
  * @param _unenvResolvePaths Root paths used to resolve absolute paths.
  * @returns ESBuild plugin
  */
-export function nodejsHybridPlugin(_unenvResolvePaths?: string[]): Plugin {
-	const { alias, inject, external } = env(nodeless, cloudflare);
+export async function nodejsHybridPlugin(
+	_unenvResolvePaths?: string[]
path: packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
user: login: petebacondarwin
body: These paths are no longer used, so we can remove it all the way to the top env vars.
But this also means that the current approach to testing in `packages/unenv-preset/tests/index.test.ts` isn't actually going to test against the local build.

Instead, I think it is time to move those checks (which is really an integration test with Wrangler) into the `fixtures/nodejs-hybrid-app` fixture alongside all the other tests that give the same coverage.

Then we can remove the `devDep` from this package to Wrangler, resolving the cyclic dependency, allowing  Wrangler to have a `workspace:*` dependency on this package.

Then we have clean simple dependency setup, the latest changes are automatically get tested and released together.
created_at: 2025-03-04T13:30:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,15 +14,24 @@ const REQUIRED_UNENV_ALIAS_NAMESPACE = "required-unenv-alias";
  * @param _unenvResolvePaths Root paths used to resolve absolute paths.
  * @returns ESBuild plugin
  */
-export function nodejsHybridPlugin(_unenvResolvePaths?: string[]): Plugin {
-	const { alias, inject, external } = env(nodeless, cloudflare);
+export async function nodejsHybridPlugin(
+	_unenvResolvePaths?: string[]
path: packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
user: login: vicb
body: Let's handle this in a follow up PR.

I think the unenv-preset should use miniflare for `packages/unenv-preset/tests/index.test.ts` because we do not want to wait for wrangler to have the updated workerd before testing - we want to be able to create a preset PR has soon as workerd is out (or even with a dev build).

As discussed before while we should be able to create the PR early, it should not be mergeable (i.e. tests failing) until whatever workerd version it relies on is deployed.

Does that sound good?
created_at: 2025-03-05T07:17:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,15 +14,24 @@ const REQUIRED_UNENV_ALIAS_NAMESPACE = "required-unenv-alias";
  * @param _unenvResolvePaths Root paths used to resolve absolute paths.
  * @returns ESBuild plugin
  */
-export function nodejsHybridPlugin(_unenvResolvePaths?: string[]): Plugin {
-	const { alias, inject, external } = env(nodeless, cloudflare);
+export async function nodejsHybridPlugin(
+	_unenvResolvePaths?: string[]
path: packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts
user: login: vicb
body: And ideally we should keep the ability use those paths to have the ability to run tests a dev version of the preset.

For now this PR is using `require.resolve` to minimize the risk of having wrong resolved paths. But it would be great to switch to whatever resolution is used by unenv latest RC (it has changed recently) which allow overriding the base path.


created_at: 2025-03-05T07:26:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,13 +70,14 @@
 	},
 	"dependencies": {
 		"@cloudflare/kv-asset-handler": "workspace:*",
+		"@cloudflare/unenv-preset": "2.0.0",
 		"@esbuild-plugins/node-globals-polyfill": "0.2.3",
 		"@esbuild-plugins/node-modules-polyfill": "0.2.2",
 		"blake3-wasm": "2.1.5",
 		"esbuild": "0.17.19",
 		"miniflare": "workspace:*",
 		"path-to-regexp": "6.3.0",
-		"unenv": "2.0.0-rc.1",
+		"unenv": "2.0.0-rc.8",
path: packages/wrangler/package.json
user: login: petebacondarwin
body: Perhaps move this to the pnpm workspace catalog?

```suggestion
		"unenv": "catalog:default",
```
created_at: 2025-03-05T10:56:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,13 +70,14 @@
 	},
 	"dependencies": {
 		"@cloudflare/kv-asset-handler": "workspace:*",
+		"@cloudflare/unenv-preset": "2.0.0",
 		"@esbuild-plugins/node-globals-polyfill": "0.2.3",
 		"@esbuild-plugins/node-modules-polyfill": "0.2.2",
 		"blake3-wasm": "2.1.5",
 		"esbuild": "0.17.19",
 		"miniflare": "workspace:*",
 		"path-to-regexp": "6.3.0",
-		"unenv": "2.0.0-rc.1",
+		"unenv": "2.0.0-rc.8",
path: packages/wrangler/package.json
user: login: petebacondarwin
body: Since the vite plugn will use the same version
created_at: 2025-03-05T10:56:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,13 +70,14 @@
 	},
 	"dependencies": {
 		"@cloudflare/kv-asset-handler": "workspace:*",
+		"@cloudflare/unenv-preset": "2.0.0",
 		"@esbuild-plugins/node-globals-polyfill": "0.2.3",
 		"@esbuild-plugins/node-modules-polyfill": "0.2.2",
 		"blake3-wasm": "2.1.5",
 		"esbuild": "0.17.19",
 		"miniflare": "workspace:*",
 		"path-to-regexp": "6.3.0",
-		"unenv": "2.0.0-rc.1",
+		"unenv": "2.0.0-rc.8",
path: packages/wrangler/package.json
user: login: vicb
body: > Since the vite plugn will use the same version

Can this be done when the cute plugin is ready? 
I have no idea about when it's ready to move to rc8
created_at: 2025-03-05T14:09:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-02-28T10:50:48Z
body: ###  🦋  Changeset detected

Latest commit: c3827cc9edf14b9b3df6cea4c02fd7c719be7bb1

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

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                            | Type  |
| ------------------------------- | ----- |
| wrangler                        | Minor |
| @cloudflare/vitest-pool-workers | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/cloudflare/workers-sdk/new/WIP/test-preset?filename=.changeset/small-wolves-remain.md&value=---%0A%22wrangler%22%3A%20patch%0A%22nodejs-hybrid-app%22%3A%20patch%0A%22pages-nodejs-v2-compat%22%3A%20patch%0A---%0A%0AUse%20%40cloudflare%2Funenv-preset%20in%20wrangler%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-02-28T11:28:20Z
body: A wrangler prerelease is available for testing. You can install this latest build in your project with:

```sh
npm install --save-dev https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-wrangler-8300
```

You can reference the automatically updated head of this PR with:

```sh
npm install --save-dev https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/prs/8300/npm-package-wrangler-8300
```

Or you can use `npx` with this latest build directly:

```sh
npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-wrangler-8300 dev path/to/script.js
```

<details><summary>Additional artifacts:</summary>


cloudflare-workers-bindings-extension:
```sh
wget https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-workers-bindings-extension-8300 -O ./cloudflare-workers-bindings-extension.0.0.0-v64cb7fa9e.vsix && code --install-extension ./cloudflare-workers-bindings-extension.0.0.0-v64cb7fa9e.vsix
```



create-cloudflare:
```sh
npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-create-cloudflare-8300 --no-auto-update
```



@cloudflare/kv-asset-handler:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-kv-asset-handler-8300
```



miniflare:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-miniflare-8300
```



@cloudflare/pages-shared:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-pages-shared-8300
```



@cloudflare/unenv-preset:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-unenv-preset-8300
```



@cloudflare/vite-plugin:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-vite-plugin-8300
```



@cloudflare/vitest-pool-workers:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-vitest-pool-workers-8300
```



@cloudflare/workers-editor-shared:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-workers-editor-shared-8300
```



@cloudflare/workers-shared:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-workers-shared-8300
```



@cloudflare/workflows-shared:
```sh
npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/13673036914/npm-package-cloudflare-workflows-shared-8300
```


Note that these links will no longer work once [the GitHub Actions artifact expires](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization).

</details>

---

`wrangler@3.112.0` includes the following runtime dependencies:

|Package|Constraint|Resolved|
|-------|----------|--------|
|`miniflare`|workspace:*|3.20250214.2|
|`workerd`|1.20250224.0|1.20250224.0|
|`workerd --version`|1.20250224.0|2025-02-24|

Please ensure constraints are pinned, and `miniflare`/`workerd` minor versions match.
<!-- Sticky Pull Request Comment -->
author_association: CONTRIBUTOR
user: login: petebacondarwin
created_at: 2025-03-04T13:24:27Z
body: We should cherry-pick this commit to this PR: 678773a228ec5b7ec92f81bebb090f0ef9a290d5
author_association: CONTRIBUTOR
user: login: vicb
created_at: 2025-03-05T08:25:28Z
body: > I think it is time to move to a workspace dependency on this package.

soon :)
author_association: CONTRIBUTOR