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

Filter by extension

Filter by extension

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

Use the native `node:process` v2 when it is available

Note that we only enable this if all of the following conditions are met:

- compatibility_date >= 2025-09-15 or process v2 enabled by flag (enable_nodejs_process_v2)
- `fetch_iterable_type_support` and `fetch_iterable_type_support_override_adjustment` are active (explicitly specified or implied by date or other flags).

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

```ts
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);
```
4 changes: 4 additions & 0 deletions fixtures/worker-logs/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ describe("'wrangler dev' correctly displays logs", () => {
const getOutput = await getWranglerDevOutput("module", [
"--compatibility-flags=enable_nodejs_process_v2",
"--compatibility-flags=nodejs_compat",
// Those flags are needed to enable the new process.v2 implementation
// See `getProcessOverrides` in `packages/unenv-preset/src/preset.ts`
"--compatibility-flags=fetch_iterable_type_support",
"--compatibility-flags=fetch_iterable_type_support_override_adjustment",
]);
await vi.waitFor(() =>
expect(getOutput()).toEqual([
Expand Down
2 changes: 1 addition & 1 deletion fixtures/worker-logs/wrangler.module.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "worker-logs",
"compatibility_date": "2025-09-06",
"compatibility_date": "2026-01-01",
"main": "src/module.js",
"compatibility_flags": ["nodejs_compat"],
}
113 changes: 108 additions & 5 deletions packages/unenv-preset/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ const nativeModules = [
"zlib",
];

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

/**
* Creates the Cloudflare preset for the given compatibility date and compatibility flags
*
Expand Down Expand Up @@ -84,6 +81,7 @@ export function getCloudflarePreset({
const dgramOverrides = getDgramOverrides(compat);
const streamWrapOverrides = getStreamWrapOverrides(compat);
const replOverrides = getReplOverrides(compat);
const processOverrides = getProcessOverrides(compat);

// "dynamic" as they depend on the compatibility date and flags
const dynamicNativeModules = [
Expand All @@ -104,11 +102,11 @@ export function getCloudflarePreset({
...dgramOverrides.nativeModules,
...streamWrapOverrides.nativeModules,
...replOverrides.nativeModules,
...processOverrides.nativeModules,
];

// "dynamic" as they depend on the compatibility date and flags
const dynamicHybridModules = [
...hybridModules,
...httpOverrides.hybridModules,
...http2Overrides.hybridModules,
...osOverrides.hybridModules,
Expand All @@ -125,6 +123,7 @@ export function getCloudflarePreset({
...dgramOverrides.hybridModules,
...streamWrapOverrides.hybridModules,
...replOverrides.hybridModules,
...processOverrides.hybridModules,
];

return {
Expand Down Expand Up @@ -158,7 +157,7 @@ export function getCloudflarePreset({
clearImmediate: false,
setImmediate: false,
...consoleOverrides.inject,
process: "@cloudflare/unenv-preset/node/process",
...processOverrides.inject,
},
polyfill: ["@cloudflare/unenv-preset/polyfill/performance"],
external: dynamicNativeModules.flatMap((p) => [p, `node:${p}`]),
Expand Down Expand Up @@ -799,3 +798,107 @@ function getReplOverrides({
hybridModules: [],
};
}

/**
* Returns the overrides for `node:process` (unenv or workerd)
*
* 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
* - can only be used when the fixes for iterable request/response bodies are enabled
*/
function getProcessOverrides({
compatibilityDate,
compatibilityFlags,
}: {
compatibilityDate: string;
compatibilityFlags: string[];
}): {
nativeModules: string[];
hybridModules: string[];
inject: { process: string | false };
} {
const disabledV2ByFlag = compatibilityFlags.includes(
"disable_nodejs_process_v2"
);

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

// When `node:process` v2 is enabled, astro will detect workerd as it was Node.js.
// This causes astro to take a code path that uses iterable request/response bodies.
// So we need to make sure that the fixes for iterable bodies are also enabled.
// @see https://github.com/cloudflare/workers-sdk/issues/10855
const hasFixes = hasFetchIterableFixes({
compatibilityDate,
compatibilityFlags,
});

const useV2 =
hasFixes && (enabledV2ByFlag || enabledV2ByDate) && !disabledV2ByFlag;

return useV2
? {
nativeModules: ["process"],
hybridModules: [],
// We can use the native global, return `false` to drop the unenv default
inject: { process: false },
}
: {
nativeModules: [],
hybridModules: ["process"],
// Use the module default export as the global `process`
inject: { process: "@cloudflare/unenv-preset/node/process" },
};
}

/**
* Workerd fixes iterable request/response bodies when both these compatibility flags are used:
* - `fetch_iterable_type_support`
* - `fetch_iterable_type_support_override_adjustment`
*
* @see https://github.com/cloudflare/workerd/issues/2746
* @see https://github.com/cloudflare/workerd/blob/main/src/workerd/io/compatibility-date.capnp
*/
function hasFetchIterableFixes({
compatibilityDate,
compatibilityFlags,
}: {
compatibilityDate: string;
compatibilityFlags: string[];
}): boolean {
const supportEnabledByFlag = compatibilityFlags.includes(
"fetch_iterable_type_support"
);

const supportDisabledByFlag = compatibilityFlags.includes(
"no_fetch_iterable_type_support"
);

const supportEnabledByDate = compatibilityDate >= "2026-02-19";

const supportEnabled =
(supportEnabledByDate || supportEnabledByFlag) && !supportDisabledByFlag;

if (!supportEnabled) {
return false;
}

const adjustmentEnabledByFlag = compatibilityFlags.includes(
"fetch_iterable_type_support_override_adjustment"
);
const adjustmentDisabledByFlag = compatibilityFlags.includes(
"no_fetch_iterable_type_support_override_adjustment"
);
// At this point, we know that `supportEnabled` is `true`
const adjustmentImpliedBySupport = compatibilityDate >= "2026-01-15";

const adjustmentEnabled =
(adjustmentEnabledByFlag || adjustmentImpliedBySupport) &&
!adjustmentDisabledByFlag;

return adjustmentEnabled;
}
79 changes: 19 additions & 60 deletions packages/unenv-preset/src/runtime/node/process.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// This polyfill is only used with the process v1 native implementation since
// process v2 implements all the APIs from workerd v1.20250924.0
// Therefore there is no need to conditionally export items from this file.

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

Expand All @@ -16,92 +20,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 +70,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 +89,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 +137,7 @@ export const {
uptime,
version,
versions,
} = isWorkerdProcessV2 ? workerdProcess : unenvProcess;
} = unenvProcess;

const _process = {
abort,
Expand Down
Loading
Loading