Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/mean-numbers-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@cloudflare/unenv-preset": patch
---

Drop `node:process` polyfill when v2 is available

Note that EventEmitters (`on`, `off`, `addListener`, `removeListener`, ...) used to be available on the import while they should not have been. They are now only available on the global process:

```
import p from "node:process";
// Working before this PR, not working after this PR
p.on("exit", exitHandler);
// Use the global process instead (works before and after the PR)
process.on("exit", exitHandler);
```
2 changes: 1 addition & 1 deletion packages/unenv-preset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
"peerDependencies": {
"unenv": "2.0.0-rc.21",
"workerd": "^1.20250912.0"
"workerd": "^1.20250924.0"
},
"peerDependenciesMeta": {
"workerd": {
Expand Down
52 changes: 50 additions & 2 deletions packages/unenv-preset/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const nativeModules = [
];

// Modules implemented via a mix of workerd APIs and polyfills.
const hybridModules = ["console", "process"];
const hybridModules = ["console"];

/**
* Creates the Cloudflare preset for the given compatibility date and compatibility flags
Expand All @@ -72,6 +72,7 @@ export function getCloudflarePreset({
const http2Overrides = getHttp2Overrides(compat);
const osOverrides = getOsOverrides(compat);
const fsOverrides = getFsOverrides(compat);
const processOverrides = getProcessOverrides(compat);

// "dynamic" as they depend on the compatibility date and flags
const dynamicNativeModules = [
Expand All @@ -80,6 +81,7 @@ export function getCloudflarePreset({
...http2Overrides.nativeModules,
...osOverrides.nativeModules,
...fsOverrides.nativeModules,
...processOverrides.nativeModules,
];

// "dynamic" as they depend on the compatibility date and flags
Expand All @@ -89,6 +91,7 @@ export function getCloudflarePreset({
...http2Overrides.hybridModules,
...osOverrides.hybridModules,
...fsOverrides.hybridModules,
...processOverrides.hybridModules,
];

return {
Expand Down Expand Up @@ -122,7 +125,7 @@ export function getCloudflarePreset({
clearImmediate: false,
setImmediate: false,
console: "@cloudflare/unenv-preset/node/console",
process: "@cloudflare/unenv-preset/node/process",
process: processOverrides.inject,
},
polyfill: ["@cloudflare/unenv-preset/polyfill/performance"],
external: dynamicNativeModules.flatMap((p) => [p, `node:${p}`]),
Expand Down Expand Up @@ -305,3 +308,48 @@ function getFsOverrides({
hybridModules: [],
};
}

/**
* Returns the overrides for `node:process` and `node:fs/promises`
*
* The native process v2 implementation:
* - is enabled starting from 2025-09-15
* - can be enabled with the "enable_nodejs_process_v2" flag
* - can be disabled with the "disable_nodejs_process_v2" flag
*/
function getProcessOverrides({
compatibilityDate,
compatibilityFlags,
}: {
compatibilityDate: string;
compatibilityFlags: string[];
}): {
nativeModules: string[];
hybridModules: string[];
inject: string | false;
} {
const disabledV2ByFlag = compatibilityFlags.includes(
"disable_nodejs_process_v2"
);

const enabledV2ByFlag = compatibilityFlags.includes(
"enable_nodejs_process_v2"
);
const enabledV2ByDate = compatibilityDate >= "2025-09-15";

const isV2 = (enabledV2ByFlag || enabledV2ByDate) && !disabledV2ByFlag;

return isV2
? {
nativeModules: ["process"],
hybridModules: [],
// We can use the native global, return `false` to drop the unenv default
inject: false,
}
: {
nativeModules: [],
hybridModules: ["process"],
// Use the module default export as the global `process`
inject: "@cloudflare/unenv-preset/node/process",
};
}
78 changes: 18 additions & 60 deletions packages/unenv-preset/src/runtime/node/process.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// The polyfill is only used with the process v1 native implementation
// process v2 implements all the APIs from workerd v1.20250924.0

import { hrtime as UnenvHrTime } from "unenv/node/internal/process/hrtime";
import { Process as UnenvProcess } from "unenv/node/internal/process/process";

Expand All @@ -16,92 +19,47 @@ export const getBuiltinModule: NodeJS.Process["getBuiltinModule"] =

const workerdProcess = getBuiltinModule("node:process");

// Workerd has 2 different implementation for `node:process`
//
// See:
// - [workerd `process` v1](https://github.com/cloudflare/workerd/blob/main/src/node/internal/legacy_process.ts)
// - [workerd `process` v2](https://github.com/cloudflare/workerd/blob/main/src/node/internal/public_process.ts)
// - [`enable_nodejs_process_v2` flag](https://github.com/cloudflare/workerd/blob/main/src/workerd/io/compatibility-date.capnp)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isWorkerdProcessV2 = (globalThis as any).Cloudflare.compatibilityFlags
.enable_nodejs_process_v2;

const unenvProcess = new UnenvProcess({
env: globalProcess.env,
// `hrtime` is only available from workerd process v2
hrtime: isWorkerdProcessV2 ? workerdProcess.hrtime : UnenvHrTime,
hrtime: UnenvHrTime,
// `nextTick` is available from workerd process v1
nextTick: workerdProcess.nextTick,
});

// APIs implemented by workerd module in both v1 and v2
// APIs implemented by workerd process in both v1 and v2
// Note that `env`, `hrtime` and `nextTick` are always retrieved from `unenv`
export const { exit, features, platform } = workerdProcess;

// APIs that can be implemented by either `unenv` or `workerd`.
// They are always retrieved from `unenv` which might use their `workerd` implementation.
export const {
// Always implemented by workerd
env,
// Only implemented in workerd v2
hrtime,
// Always implemented by workerd
nextTick,
} = unenvProcess;

// APIs that are not implemented by `workerd` (whether v1 or v2)
// They are retrieved from `unenv`.
export const {
_channel,
_debugEnd,
_debugProcess,
_disconnect,
_events,
_eventsCount,
_handleQueue,
_maxListeners,
_pendingMessage,
_send,
assert,
disconnect,
mainModule,
} = unenvProcess;

// API that are only implemented starting from v2 of workerd process
// They are retrieved from unenv when process v1 is used
export const {
// @ts-expect-error `_debugEnd` is missing typings
_debugEnd,
// @ts-expect-error `_debugProcess` is missing typings
_debugProcess,
// @ts-expect-error `_exiting` is missing typings
_exiting,
// @ts-expect-error `_fatalException` is missing typings
_fatalException,
// @ts-expect-error `_getActiveHandles` is missing typings
_getActiveHandles,
// @ts-expect-error `_getActiveRequests` is missing typings
_getActiveRequests,
// @ts-expect-error `_kill` is missing typings
_handleQueue,
_kill,
// @ts-expect-error `_linkedBinding` is missing typings
_linkedBinding,
// @ts-expect-error `_preload_modules` is missing typings
_maxListeners,
_pendingMessage,
_preload_modules,
// @ts-expect-error `_rawDebug` is missing typings
_rawDebug,
// @ts-expect-error `_startProfilerIdleNotifier` is missing typings
_send,
_startProfilerIdleNotifier,
// @ts-expect-error `_stopProfilerIdleNotifier` is missing typings
_stopProfilerIdleNotifier,
// @ts-expect-error `_tickCallback` is missing typings
_tickCallback,
abort,
addListener,
allowedNodeEnvironmentFlags,
arch,
argv,
argv0,
assert,
availableMemory,
// @ts-expect-error `binding` is missing typings
binding,
channel,
chdir,
Expand All @@ -111,11 +69,12 @@ export const {
cpuUsage,
cwd,
debugPort,
disconnect,
dlopen,
// @ts-expect-error `domain` is missing typings
domain,
emit,
emitWarning,
env,
eventNames,
execArgv,
execPath,
Expand All @@ -129,27 +88,26 @@ export const {
getMaxListeners,
getuid,
hasUncaughtExceptionCaptureCallback,
// @ts-expect-error `initgroups` is missing typings
hrtime,
initgroups,
kill,
listenerCount,
listeners,
loadEnvFile,
mainModule,
memoryUsage,
// @ts-expect-error `moduleLoadList` is missing typings
moduleLoadList,
nextTick,
off,
on,
once,
// @ts-expect-error `openStdin` is missing typings
openStdin,
permission,
pid,
ppid,
prependListener,
prependOnceListener,
rawListeners,
// @ts-expect-error `reallyExit` is missing typings
reallyExit,
ref,
release,
Expand Down Expand Up @@ -178,7 +136,7 @@ export const {
uptime,
version,
versions,
} = isWorkerdProcessV2 ? workerdProcess : unenvProcess;
} = unenvProcess;

const _process = {
abort,
Expand Down
34 changes: 17 additions & 17 deletions packages/wrangler/e2e/unenv-preset/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ export const WorkerdTests: Record<string, () => void> = {
const useV2 = getRuntimeFlagValue("enable_nodejs_process_v2");

for (const p of [mProcess, gProcess]) {
assert.equal(typeof (p as any).binding, "function");
if (useV2) {
// workerd implementation only
assert.equal(p.arch, "x64");
Expand All @@ -525,23 +526,22 @@ export const WorkerdTests: Record<string, () => void> = {

assert.doesNotThrow(() => p.chdir("/tmp"));
assert.equal(typeof p.cwd(), "string");

assert.equal(typeof p.addListener, "function");
assert.equal(typeof p.eventNames, "function");
assert.equal(typeof p.getMaxListeners, "function");
assert.equal(typeof p.listenerCount, "function");
assert.equal(typeof p.listeners, "function");
assert.equal(typeof p.off, "function");
assert.equal(typeof p.on, "function");
assert.equal(typeof p.once, "function");
assert.equal(typeof p.prependListener, "function");
assert.equal(typeof p.prependOnceListener, "function");
assert.equal(typeof p.rawListeners, "function");
assert.equal(typeof p.removeAllListeners, "function");
assert.equal(typeof p.removeListener, "function");
assert.equal(typeof p.setMaxListeners, "function");
assert.equal(typeof (p as any).binding, "function");
assert.equal(typeof p.permission, "object");
}

// Event APIs are only available on global process
assert.equal(typeof gProcess.addListener, "function");
assert.equal(typeof gProcess.eventNames, "function");
assert.equal(typeof gProcess.getMaxListeners, "function");
assert.equal(typeof gProcess.listenerCount, "function");
assert.equal(typeof gProcess.listeners, "function");
assert.equal(typeof gProcess.off, "function");
assert.equal(typeof gProcess.on, "function");
assert.equal(typeof gProcess.once, "function");
assert.equal(typeof gProcess.prependListener, "function");
assert.equal(typeof gProcess.prependOnceListener, "function");
assert.equal(typeof gProcess.rawListeners, "function");
assert.equal(typeof gProcess.removeAllListeners, "function");
assert.equal(typeof gProcess.removeListener, "function");
assert.equal(typeof gProcess.setMaxListeners, "function");
},
};
Loading
Loading