Skip to content
Open
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
58 changes: 38 additions & 20 deletions src/node/internal/internal_readline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,47 @@
// 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: unknown, _options: unknown, _callback?: unknown): void {
throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.question');
question(query: string, callback: (answer: string) => void): void;
question(
query: string,
options: Abortable,
callback: (answer: string) => void
): void;
question(
_query: string,
_optionsOrCallback: Abortable | ((answer: string) => void),
_callback?: (answer: string) => void
): void {
// If callback is the second argument (no options)
if (typeof _optionsOrCallback === 'function') {
_optionsOrCallback('');
} else if (typeof _callback === 'function') {
_callback('');
}
}

pause(): this {
Expand All @@ -62,15 +76,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 {
Expand All @@ -82,8 +99,9 @@ export class Interface extends EventEmitter implements Readline.Interface {
this.close();
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Symbol.asyncIterator](): NodeJS.AsyncIterator<string, undefined, any> {
throw new ERR_METHOD_NOT_IMPLEMENTED('Interface[Symbol.asyncIterator]');
// 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<string> {
yield '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment to why we are returning this?

}
}
60 changes: 30 additions & 30 deletions src/node/internal/internal_readline_promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.question');
question(query: string): Promise<string>;
question(query: string, options: Abortable): Promise<string>;
question(_query: string, _options?: Abortable): Promise<string> {
return Promise.resolve('');
}

pause(): this {
Expand All @@ -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 {
Expand All @@ -85,17 +87,16 @@ export class Interface extends EventEmitter implements ReadlineType.Interface {
this.close();
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Symbol.asyncIterator](): NodeJS.AsyncIterator<string, undefined, any> {
throw new ERR_METHOD_NOT_IMPLEMENTED('Interface[Symbol.asyncIterator]');
// 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<string> {
yield '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a small comment?

}
}

// 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;
}
Expand All @@ -104,9 +105,8 @@ export class Readline implements ReadlineType.Readline {
return this;
}

// eslint-disable-next-line @typescript-eslint/require-await
async commit(): Promise<void> {
throw new ERR_METHOD_NOT_IMPLEMENTED('Interface.commit');
commit(): Promise<void> {
return Promise.resolve();
}

cursorTo(_x: number, _y?: number): this {
Expand Down
100 changes: 79 additions & 21 deletions src/workerd/api/node/tests/readline-nodejs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
};
Expand Down Expand Up @@ -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);
},
};

Expand Down
Loading