From 6cfc67c44f81a25a9a2b5adf817a629e5b6e83d4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:34:07 +0000 Subject: [PATCH 1/2] feat(unenv-preset): add native node:tty module support Co-Authored-By: pbacondarwin@cloudflare.com --- .changeset/native-node-tty.md | 5 +++ packages/unenv-preset/src/preset.ts | 39 +++++++++++++++++++ .../wrangler/e2e/unenv-preset/preset.test.ts | 21 ++++++++++ .../wrangler/e2e/unenv-preset/worker/index.ts | 32 +++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 .changeset/native-node-tty.md diff --git a/.changeset/native-node-tty.md b/.changeset/native-node-tty.md new file mode 100644 index 000000000000..e677755fedb2 --- /dev/null +++ b/.changeset/native-node-tty.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/unenv-preset": minor +--- + +Add support for native `node:tty` module when the `enable_nodejs_tty_module` and `experimental` compatibility flags are enabled. diff --git a/packages/unenv-preset/src/preset.ts b/packages/unenv-preset/src/preset.ts index 38c10340fb86..e3c569494720 100644 --- a/packages/unenv-preset/src/preset.ts +++ b/packages/unenv-preset/src/preset.ts @@ -83,6 +83,7 @@ export function getCloudflarePreset({ const sqliteOverrides = getSqliteOverrides(compat); const dgramOverrides = getDgramOverrides(compat); const streamWrapOverrides = getStreamWrapOverrides(compat); + const ttyOverrides = getTtyOverrides(compat); // "dynamic" as they depend on the compatibility date and flags const dynamicNativeModules = [ @@ -102,6 +103,7 @@ export function getCloudflarePreset({ ...sqliteOverrides.nativeModules, ...dgramOverrides.nativeModules, ...streamWrapOverrides.nativeModules, + ...ttyOverrides.nativeModules, ]; // "dynamic" as they depend on the compatibility date and flags @@ -122,6 +124,7 @@ export function getCloudflarePreset({ ...sqliteOverrides.hybridModules, ...dgramOverrides.hybridModules, ...streamWrapOverrides.hybridModules, + ...ttyOverrides.hybridModules, ]; return { @@ -752,3 +755,39 @@ function getStreamWrapOverrides({ hybridModules: [], }; } + +/** + * Returns the overrides for `node:tty` (unenv or workerd) + * + * The native tty implementation: + * - is experimental and has no default enable date + * - can be enabled with the "enable_nodejs_tty_module" flag + * - can be disabled with the "disable_nodejs_tty_module" flag + */ +function getTtyOverrides({ + compatibilityFlags, +}: { + compatibilityDate: string; + compatibilityFlags: string[]; +}): { nativeModules: string[]; hybridModules: string[] } { + const disabledByFlag = compatibilityFlags.includes( + "disable_nodejs_tty_module" + ); + + const enabledByFlag = + compatibilityFlags.includes("enable_nodejs_tty_module") && + compatibilityFlags.includes("experimental"); + + const enabled = enabledByFlag && !disabledByFlag; + + // When enabled, use the native `tty` module from workerd + return enabled + ? { + nativeModules: ["tty"], + hybridModules: [], + } + : { + nativeModules: [], + hybridModules: [], + }; +} diff --git a/packages/wrangler/e2e/unenv-preset/preset.test.ts b/packages/wrangler/e2e/unenv-preset/preset.test.ts index 4a8a4e3ae5f2..ea4b716b69ae 100644 --- a/packages/wrangler/e2e/unenv-preset/preset.test.ts +++ b/packages/wrangler/e2e/unenv-preset/preset.test.ts @@ -553,6 +553,27 @@ const localTestConfigs: TestConfig[] = [ }, }, ], + // node:tty (experimental, no default enable date) + [ + // TODO: add test for disabled by date (no date defined yet) + // TODO: add test for enabled by date (no date defined yet) + { + name: "tty enabled by flag", + compatibilityDate: "2024-09-23", + compatibilityFlags: ["enable_nodejs_tty_module", "experimental"], + expectRuntimeFlags: { + enable_nodejs_tty_module: true, + }, + }, + { + name: "tty disabled by flag", + compatibilityDate: "2024-09-23", + compatibilityFlags: ["disable_nodejs_tty_module", "experimental"], + expectRuntimeFlags: { + enable_nodejs_tty_module: false, + }, + }, + ], ].flat() as TestConfig[]; describe.each(localTestConfigs)( diff --git a/packages/wrangler/e2e/unenv-preset/worker/index.ts b/packages/wrangler/e2e/unenv-preset/worker/index.ts index 377bec63eb5f..1ad28f47c5e8 100644 --- a/packages/wrangler/e2e/unenv-preset/worker/index.ts +++ b/packages/wrangler/e2e/unenv-preset/worker/index.ts @@ -859,6 +859,38 @@ export const WorkerdTests: Record void> = { // `JSStreamSocket` is the default export of `node:_stream_wrap` assertTypeOf(streamWrap, "default", "function"); }, + + async testTty() { + const tty = await import("node:tty"); + + // Common exports (both unenv stub and native workerd) + assertTypeOfProperties(tty, { + isatty: "function", + ReadStream: "function", + WriteStream: "function", + }); + + assertTypeOfProperties(tty.default, { + isatty: "function", + ReadStream: "function", + WriteStream: "function", + }); + + // isatty should return false (both unenv and workerd) + assert.strictEqual(tty.isatty(0), false); + assert.strictEqual(tty.isatty(1), false); + assert.strictEqual(tty.isatty(2), false); + + // Both implementations throw when calling ReadStream/WriteStream constructors + assert.throws( + () => new (tty.ReadStream as any)(0), + /not implemented|ERR_METHOD_NOT_IMPLEMENTED/ + ); + assert.throws( + () => new (tty.WriteStream as any)(1), + /not implemented|ERR_METHOD_NOT_IMPLEMENTED/ + ); + }, }; /** From be6cbec39cbc3e32620307eb7ea17eaaae41afb2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:48:42 +0000 Subject: [PATCH 2/2] fix(unenv-preset): only check for throws when native tty module is enabled Co-Authored-By: pbacondarwin@cloudflare.com --- .../wrangler/e2e/unenv-preset/worker/index.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/wrangler/e2e/unenv-preset/worker/index.ts b/packages/wrangler/e2e/unenv-preset/worker/index.ts index 1ad28f47c5e8..8fe865cafbc1 100644 --- a/packages/wrangler/e2e/unenv-preset/worker/index.ts +++ b/packages/wrangler/e2e/unenv-preset/worker/index.ts @@ -881,15 +881,18 @@ export const WorkerdTests: Record void> = { assert.strictEqual(tty.isatty(1), false); assert.strictEqual(tty.isatty(2), false); - // Both implementations throw when calling ReadStream/WriteStream constructors - assert.throws( - () => new (tty.ReadStream as any)(0), - /not implemented|ERR_METHOD_NOT_IMPLEMENTED/ - ); - assert.throws( - () => new (tty.WriteStream as any)(1), - /not implemented|ERR_METHOD_NOT_IMPLEMENTED/ - ); + // Native workerd throws when calling ReadStream/WriteStream constructors + // unenv stub does not throw - it creates stub objects + if (getRuntimeFlagValue("enable_nodejs_tty_module")) { + assert.throws( + () => new (tty.ReadStream as any)(0), + /ERR_METHOD_NOT_IMPLEMENTED/ + ); + assert.throws( + () => new (tty.WriteStream as any)(1), + /ERR_METHOD_NOT_IMPLEMENTED/ + ); + } }, };