From 87c332e0ebd72438007ddf764b238f0e4764465b Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 4 Feb 2026 13:10:53 +0000 Subject: [PATCH 1/2] fix(node): make readline Interface and Readline classes no-op stubs Change the readline Interface class and the readline/promises Interface and Readline classes to be no-op stubs instead of throwing errors. This matches the behavior of unenv's readline implementation, which allows code that depends on readline to work without crashing even though the functionality is not fully implemented. Methods now return sensible defaults (empty strings, zero values, etc.) instead of throwing ERR_METHOD_NOT_IMPLEMENTED errors. Fixes compatibility with https://github.com/cloudflare/workers-sdk/pull/11734 --- src/node/internal/internal_readline.ts | 50 +++++---- .../internal/internal_readline_promises.ts | 58 +++++----- .../api/node/tests/readline-nodejs-test.js | 100 ++++++++++++++---- 3 files changed, 138 insertions(+), 70 deletions(-) diff --git a/src/node/internal/internal_readline.ts b/src/node/internal/internal_readline.ts index 9b51bc57b45..f7f0838f313 100644 --- a/src/node/internal/internal_readline.ts +++ b/src/node/internal/internal_readline.ts @@ -24,33 +24,43 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. import { EventEmitter } from 'node-internal:events'; -import { ERR_METHOD_NOT_IMPLEMENTED } from 'node-internal:internal_errors'; +import type { Abortable } from 'node:events'; import type Readline from 'node:readline'; +// This class provides a no-op stub implementation that matches unenv's behavior. +// See: https://github.com/unjs/unenv/blob/main/src/runtime/node/internal/readline/interface.ts +// Methods are no-ops or return sensible defaults rather than throwing errors, +// which allows code that depends on readline to work without crashing. export class Interface extends EventEmitter implements Readline.Interface { - terminal: boolean; - line: string; - cursor: number; - - constructor() { - super(); - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface'); - } + terminal = false; + line = ''; + cursor = 0; getPrompt(): string { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.getPrompt'); + return ''; } setPrompt(_prompt: string): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.setPrompt'); + // No-op } prompt(_preserveCursor?: boolean): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.prompt'); + // No-op } + question(query: string, callback: (answer: string) => void): void; + question( + query: string, + options: Abortable, + callback: (answer: string) => void + ): void; question(_query: unknown, _options: unknown, _callback?: unknown): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.question'); + // If callback is the second argument (no options) + if (typeof _options === 'function') { + (_options as (answer: string) => void)(''); + } else if (typeof _callback === 'function') { + (_callback as (answer: string) => void)(''); + } } pause(): this { @@ -62,15 +72,18 @@ export class Interface extends EventEmitter implements Readline.Interface { } close(): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.close'); + // No-op } write(_data: unknown, _key?: unknown): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.write'); + // No-op } getCursorPos(): Readline.CursorPos { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.getCursorPos'); + return { + rows: 0, + cols: 0, + }; } [Symbol.dispose](): void { @@ -82,8 +95,7 @@ export class Interface extends EventEmitter implements Readline.Interface { this.close(); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [Symbol.asyncIterator](): NodeJS.AsyncIterator { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface[Symbol.asyncIterator]'); + async *[Symbol.asyncIterator](): NodeJS.AsyncIterator { + yield ''; } } diff --git a/src/node/internal/internal_readline_promises.ts b/src/node/internal/internal_readline_promises.ts index 64c635c04c4..edfd1946fa5 100644 --- a/src/node/internal/internal_readline_promises.ts +++ b/src/node/internal/internal_readline_promises.ts @@ -24,36 +24,35 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. import { EventEmitter } from 'node-internal:events'; -import { ERR_METHOD_NOT_IMPLEMENTED } from 'node-internal:internal_errors'; +import type { Abortable } from 'node:events'; import type ReadlineType from 'node:readline/promises'; -import type { CursorPos } from 'node:readline'; -import type { Direction } from 'readline'; +import type { CursorPos, Direction } from 'node:readline'; +// This class provides a no-op stub implementation that matches unenv's behavior. +// See: https://github.com/unjs/unenv/blob/main/src/runtime/node/internal/readline/promises/interface.ts +// Methods are no-ops or return sensible defaults rather than throwing errors, +// which allows code that depends on readline to work without crashing. export class Interface extends EventEmitter implements ReadlineType.Interface { - terminal: boolean; - line: string; - cursor: number; - - constructor() { - super(); - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface'); - } + terminal = false; + line = ''; + cursor = 0; getPrompt(): string { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.getPrompt'); + return ''; } setPrompt(_prompt: string): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.setPrompt'); + // No-op } prompt(_preserveCursor?: boolean): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.prompt'); + // No-op } - // eslint-disable-next-line @typescript-eslint/require-await - async question(_query: unknown, _options?: unknown): Promise { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.question'); + question(query: string): Promise; + question(query: string, options: Abortable): Promise; + question(_query: unknown, _options?: unknown): Promise { + return Promise.resolve(''); } pause(): this { @@ -65,15 +64,18 @@ export class Interface extends EventEmitter implements ReadlineType.Interface { } close(): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.close'); + // No-op } write(_data: unknown, _key?: unknown): void { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.write'); + // No-op } getCursorPos(): CursorPos { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.getCursorPos'); + return { + rows: 0, + cols: 0, + }; } [Symbol.dispose](): void { @@ -85,17 +87,14 @@ export class Interface extends EventEmitter implements ReadlineType.Interface { this.close(); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [Symbol.asyncIterator](): NodeJS.AsyncIterator { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface[Symbol.asyncIterator]'); + async *[Symbol.asyncIterator](): NodeJS.AsyncIterator { + yield ''; } } +// This class provides a no-op stub implementation that matches unenv's behavior. +// See: https://github.com/unjs/unenv/blob/main/src/runtime/node/internal/readline/promises/readline.ts export class Readline implements ReadlineType.Readline { - constructor() { - throw new ERR_METHOD_NOT_IMPLEMENTED('Readline'); - } - clearLine(_dir: Direction): this { return this; } @@ -104,9 +103,8 @@ export class Readline implements ReadlineType.Readline { return this; } - // eslint-disable-next-line @typescript-eslint/require-await - async commit(): Promise { - throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.commit'); + commit(): Promise { + return Promise.resolve(); } cursorTo(_x: number, _y?: number): this { diff --git a/src/workerd/api/node/tests/readline-nodejs-test.js b/src/workerd/api/node/tests/readline-nodejs-test.js index 6283f67514e..8f3934a839d 100644 --- a/src/workerd/api/node/tests/readline-nodejs-test.js +++ b/src/workerd/api/node/tests/readline-nodejs-test.js @@ -41,20 +41,49 @@ export const readlineEmitKeypressEvents = { export const readlineCreateInterface = { test() { - assert.throws(() => readline.createInterface(), { - code: 'ERR_METHOD_NOT_IMPLEMENTED', - message: /Interface/, - }); + // createInterface returns a no-op stub that matches unenv behavior + const rl = readline.createInterface(); + assert.ok(rl instanceof readline.Interface); + assert.strictEqual(rl.terminal, false); + assert.strictEqual(rl.line, ''); + assert.strictEqual(rl.cursor, 0); assert.strictEqual(typeof readline.createInterface, 'function'); }, }; export const readlineInterface = { test() { - assert.throws(() => new readline.Interface(), { - code: 'ERR_METHOD_NOT_IMPLEMENTED', - message: /Interface/, + // Interface constructor returns a no-op stub that matches unenv behavior + const rl = new readline.Interface(); + assert.ok(rl instanceof readline.Interface); + assert.strictEqual(rl.terminal, false); + assert.strictEqual(rl.line, ''); + assert.strictEqual(rl.cursor, 0); + + // Test methods return sensible defaults instead of throwing + assert.strictEqual(rl.getPrompt(), ''); + rl.setPrompt('test'); // Should not throw + rl.prompt(); // Should not throw + rl.close(); // Should not throw + rl.write('test'); // Should not throw + assert.deepStrictEqual(rl.getCursorPos(), { rows: 0, cols: 0 }); + assert.strictEqual(rl.pause(), rl); + assert.strictEqual(rl.resume(), rl); + + // question calls callback with empty string + let questionAnswer = null; + rl.question('test?', (answer) => { + questionAnswer = answer; + }); + assert.strictEqual(questionAnswer, ''); + + // question with options also works + let questionAnswer2 = null; + rl.question('test?', {}, (answer) => { + questionAnswer2 = answer; }); + assert.strictEqual(questionAnswer2, ''); + assert.strictEqual(typeof readline.Interface, 'function'); }, }; @@ -99,29 +128,58 @@ export const readlinePromises = { }; export const readlinePromisesInterface = { - test() { - assert.throws(() => new readline.promises.Interface(), { - code: 'ERR_METHOD_NOT_IMPLEMENTED', - message: /Interface/, - }); + async test() { + // promises.Interface constructor returns a no-op stub that matches unenv behavior + const rl = new readline.promises.Interface(); + assert.ok(rl instanceof readline.promises.Interface); + assert.strictEqual(rl.terminal, false); + assert.strictEqual(rl.line, ''); + assert.strictEqual(rl.cursor, 0); + + // Test methods return sensible defaults instead of throwing + assert.strictEqual(rl.getPrompt(), ''); + rl.setPrompt('test'); // Should not throw + rl.prompt(); // Should not throw + rl.close(); // Should not throw + rl.write('test'); // Should not throw + assert.deepStrictEqual(rl.getCursorPos(), { rows: 0, cols: 0 }); + assert.strictEqual(rl.pause(), rl); + assert.strictEqual(rl.resume(), rl); + + // question returns a promise that resolves to empty string + const answer = await rl.question('test?'); + assert.strictEqual(answer, ''); }, }; export const readlinePromisesCreateInterface = { test() { - assert.throws(() => readline.promises.createInterface(), { - code: 'ERR_METHOD_NOT_IMPLEMENTED', - message: /Interface/, - }); + // promises.createInterface returns a no-op stub that matches unenv behavior + const rl = readline.promises.createInterface(); + assert.ok(rl instanceof readline.promises.Interface); + assert.strictEqual(rl.terminal, false); + assert.strictEqual(rl.line, ''); + assert.strictEqual(rl.cursor, 0); }, }; export const readlinePromisesReadline = { - test() { - assert.throws(() => new readline.promises.Readline(), { - code: 'ERR_METHOD_NOT_IMPLEMENTED', - message: /Readline/, - }); + async test() { + // Readline constructor returns a no-op stub that matches unenv behavior + const rl = new readline.promises.Readline(process.stdout); + assert.ok(rl instanceof readline.promises.Readline); + + // Test methods return sensible defaults instead of throwing + assert.strictEqual(rl.clearLine(0), rl); + assert.strictEqual(rl.clearScreenDown(), rl); + assert.strictEqual(rl.cursorTo(0), rl); + assert.strictEqual(rl.cursorTo(0, 0), rl); + assert.strictEqual(rl.moveCursor(1, 1), rl); + assert.strictEqual(rl.rollback(), rl); + + // commit returns a promise that resolves to undefined + const result = await rl.commit(); + assert.strictEqual(result, undefined); }, }; From f67d3a0f3e0939f424a52b0d1579c0e75732d9d7 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 4 Feb 2026 20:49:21 +0000 Subject: [PATCH 2/2] address PR feedback: improve types and add comments --- src/node/internal/internal_readline.ts | 14 ++++++++++---- src/node/internal/internal_readline_promises.ts | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/node/internal/internal_readline.ts b/src/node/internal/internal_readline.ts index f7f0838f313..0fe42465473 100644 --- a/src/node/internal/internal_readline.ts +++ b/src/node/internal/internal_readline.ts @@ -54,12 +54,16 @@ export class Interface extends EventEmitter implements Readline.Interface { options: Abortable, callback: (answer: string) => void ): void; - question(_query: unknown, _options: unknown, _callback?: unknown): void { + question( + _query: string, + _optionsOrCallback: Abortable | ((answer: string) => void), + _callback?: (answer: string) => void + ): void { // If callback is the second argument (no options) - if (typeof _options === 'function') { - (_options as (answer: string) => void)(''); + if (typeof _optionsOrCallback === 'function') { + _optionsOrCallback(''); } else if (typeof _callback === 'function') { - (_callback as (answer: string) => void)(''); + _callback(''); } } @@ -95,6 +99,8 @@ export class Interface extends EventEmitter implements Readline.Interface { this.close(); } + // Yield a single empty string so that `for await...of` loops complete + // immediately without blocking, consistent with no-op stub behavior. async *[Symbol.asyncIterator](): NodeJS.AsyncIterator { yield ''; } diff --git a/src/node/internal/internal_readline_promises.ts b/src/node/internal/internal_readline_promises.ts index edfd1946fa5..194ed9bff16 100644 --- a/src/node/internal/internal_readline_promises.ts +++ b/src/node/internal/internal_readline_promises.ts @@ -51,7 +51,7 @@ export class Interface extends EventEmitter implements ReadlineType.Interface { question(query: string): Promise; question(query: string, options: Abortable): Promise; - question(_query: unknown, _options?: unknown): Promise { + question(_query: string, _options?: Abortable): Promise { return Promise.resolve(''); } @@ -87,6 +87,8 @@ export class Interface extends EventEmitter implements ReadlineType.Interface { this.close(); } + // Yield a single empty string so that `for await...of` loops complete + // immediately without blocking, consistent with no-op stub behavior. async *[Symbol.asyncIterator](): NodeJS.AsyncIterator { yield ''; }