0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-10 06:07:03 -04:00

fix(node): instantiating process class without new (#23865)

Popular test runners like Jest instantiate a new `Process` object
themselves and expect the class constructor to be callable without the
`new` keyword. This PR refactors our `Process` class implementation from
a proper ES2015 class to an ES5-style class which can be invoked both
with and without the `new` keyword like in Node.

Fixes https://github.com/denoland/deno/issues/23863
This commit is contained in:
Marvin Hagemeister 2024-05-17 16:24:07 +02:00 committed by Bartek Iwańczuk
parent eec8800513
commit 3d3b9a7566
No known key found for this signature in database
GPG key ID: 0C6BCDDC3B3AD750
2 changed files with 292 additions and 273 deletions

View file

@ -311,6 +311,8 @@ if (!isWindows) {
export { geteuid, getgid, getuid }; export { geteuid, getgid, getuid };
const ALLOWED_FLAGS = buildAllowedFlags();
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
function uncaughtExceptionHandler(err: any, origin: string) { function uncaughtExceptionHandler(err: any, origin: string) {
// The origin parameter can be 'unhandledRejection' or 'uncaughtException' // The origin parameter can be 'unhandledRejection' or 'uncaughtException'
@ -326,13 +328,21 @@ function uncaughtExceptionHandler(err: any, origin: string) {
let execPath: string | null = null; let execPath: string | null = null;
class Process extends EventEmitter { // The process class needs to be an ES5 class because it can be instantiated
constructor() { // in Node without the `new` keyword. It's not a true class in Node. Popular
super(); // test runners like Jest rely on this.
} // deno-lint-ignore no-explicit-any
function Process(this: any) {
// deno-lint-ignore no-explicit-any
if (!(this instanceof Process)) return new (Process as any)();
/** https://nodejs.org/api/process.html#processrelease */ EventEmitter.call(this);
get release() { }
Process.prototype = Object.create(EventEmitter.prototype);
/** https://nodejs.org/api/process.html#processrelease */
Object.defineProperty(Process.prototype, "release", {
get() {
return { return {
name: "node", name: "node",
sourceUrl: sourceUrl:
@ -340,354 +350,353 @@ class Process extends EventEmitter {
headersUrl: headersUrl:
`https://nodejs.org/download/release/${version}/node-${version}-headers.tar.gz`, `https://nodejs.org/download/release/${version}/node-${version}-headers.tar.gz`,
}; };
} },
});
/** https://nodejs.org/api/process.html#process_process_arch */ /** https://nodejs.org/api/process.html#process_process_arch */
get arch() { Object.defineProperty(Process.prototype, "arch", {
get() {
return arch; return arch;
} },
});
get report() { Object.defineProperty(Process.prototype, "report", {
get() {
return report; return report;
} },
});
get title() { Object.defineProperty(Process.prototype, "title", {
get() {
return "deno"; return "deno";
} },
set(_value) {
set title(_value) {
// NOTE(bartlomieju): this is a noop. Node.js doesn't guarantee that the // NOTE(bartlomieju): this is a noop. Node.js doesn't guarantee that the
// process name will be properly set and visible from other tools anyway. // process name will be properly set and visible from other tools anyway.
// Might revisit in the future. // Might revisit in the future.
} },
});
/** /**
* https://nodejs.org/api/process.html#process_process_argv * https://nodejs.org/api/process.html#process_process_argv
* Read permissions are required in order to get the executable route * Read permissions are required in order to get the executable route
*/ */
argv = argv; Process.prototype.argv = argv;
get argv0() { Object.defineProperty(Process.prototype, "argv0", {
get() {
return argv0; return argv0;
} },
set(_val) {},
});
set argv0(_val) {} /** https://nodejs.org/api/process.html#process_process_chdir_directory */
Process.prototype.chdir = chdir;
/** https://nodejs.org/api/process.html#process_process_chdir_directory */ /** https://nodejs.org/api/process.html#processconfig */
chdir = chdir; Process.prototype.config = {
target_defaults: {},
variables: {},
};
/** https://nodejs.org/api/process.html#processconfig */ /** https://nodejs.org/api/process.html#process_process_cwd */
config = { Process.prototype.cwd = cwd;
target_defaults: {},
variables: {},
};
/** https://nodejs.org/api/process.html#process_process_cwd */ /**
cwd = cwd; * https://nodejs.org/api/process.html#process_process_env
* Requires env permissions
*/
Process.prototype.env = env;
/** /** https://nodejs.org/api/process.html#process_process_execargv */
* https://nodejs.org/api/process.html#process_process_env Process.prototype.execArgv = [];
* Requires env permissions
*/
env = env;
/** https://nodejs.org/api/process.html#process_process_execargv */ /** https://nodejs.org/api/process.html#process_process_exit_code */
execArgv: string[] = []; Process.prototype.exit = exit;
/** https://nodejs.org/api/process.html#process_process_exit_code */ /** https://nodejs.org/api/process.html#processabort */
exit = exit; Process.prototype.abort = abort;
/** https://nodejs.org/api/process.html#processabort */ // Undocumented Node API that is used by `signal-exit` which in turn
abort = abort; // is used by `node-tap`. It was marked for removal a couple of years
// ago. See https://github.com/nodejs/node/blob/6a6b3c54022104cc110ab09044a2a0cecb8988e7/lib/internal/bootstrap/node.js#L172
Process.prototype.reallyExit = (code: number) => {
return Deno.exit(code || 0);
};
// Undocumented Node API that is used by `signal-exit` which in turn Process.prototype._exiting = _exiting;
// is used by `node-tap`. It was marked for removal a couple of years
// ago. See https://github.com/nodejs/node/blob/6a6b3c54022104cc110ab09044a2a0cecb8988e7/lib/internal/bootstrap/node.js#L172
reallyExit = (code: number) => {
return Deno.exit(code || 0);
};
_exiting = _exiting; /** https://nodejs.org/api/process.html#processexitcode_1 */
Object.defineProperty(Process.prototype, "exitCode", {
/** https://nodejs.org/api/process.html#processexitcode_1 */ get() {
get exitCode() {
return globalProcessExitCode; return globalProcessExitCode;
} },
set(code: number | undefined) {
set exitCode(code: number | undefined) {
globalProcessExitCode = code; globalProcessExitCode = code;
code = parseInt(code) || 0; code = parseInt(code) || 0;
if (!isNaN(code)) { if (!isNaN(code)) {
op_set_exit_code(code); op_set_exit_code(code);
} }
} },
});
// Typed as any to avoid importing "module" module for types // Typed as any to avoid importing "module" module for types
Process.prototype.mainModule = undefined;
/** https://nodejs.org/api/process.html#process_process_nexttick_callback_args */
Process.prototype.nextTick = _nextTick;
Process.prototype.dlopen = dlopen;
/** https://nodejs.org/api/process.html#process_process_events */
Process.prototype.on = function (
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
mainModule: any = undefined; this: any,
event: string,
/** https://nodejs.org/api/process.html#process_process_nexttick_callback_args */
nextTick = _nextTick;
dlopen = dlopen;
/** https://nodejs.org/api/process.html#process_process_events */
override on(event: "exit", listener: (code: number) => void): this;
override on(
event: typeof notImplementedEvents[number],
// deno-lint-ignore ban-types
listener: Function,
): this;
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
override on(event: string, listener: (...args: any[]) => void): this { listener: (...args: any[]) => void,
if (notImplementedEvents.includes(event)) { ) {
warnNotImplemented(`process.on("${event}")`); if (notImplementedEvents.includes(event)) {
super.on(event, listener); warnNotImplemented(`process.on("${event}")`);
} else if (event.startsWith("SIG")) { EventEmitter.prototype.on.call(this, event, listener);
if (event === "SIGBREAK" && Deno.build.os !== "windows") { } else if (event.startsWith("SIG")) {
// Ignores SIGBREAK if the platform is not windows. if (event === "SIGBREAK" && Deno.build.os !== "windows") {
} else if (event === "SIGTERM" && Deno.build.os === "windows") { // Ignores SIGBREAK if the platform is not windows.
// Ignores SIGTERM on windows. } else if (event === "SIGTERM" && Deno.build.os === "windows") {
} else { // Ignores SIGTERM on windows.
Deno.addSignalListener(event as Deno.Signal, listener);
}
} else { } else {
super.on(event, listener); Deno.addSignalListener(event as Deno.Signal, listener);
} }
} else {
return this; EventEmitter.prototype.on.call(this, event, listener);
} }
override off(event: "exit", listener: (code: number) => void): this; return this;
override off( };
event: typeof notImplementedEvents[number],
// deno-lint-ignore ban-types Process.prototype.off = function (
listener: Function,
): this;
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
override off(event: string, listener: (...args: any[]) => void): this { this: any,
if (notImplementedEvents.includes(event)) { event: string,
warnNotImplemented(`process.off("${event}")`);
super.off(event, listener);
} else if (event.startsWith("SIG")) {
if (event === "SIGBREAK" && Deno.build.os !== "windows") {
// Ignores SIGBREAK if the platform is not windows.
} else if (event === "SIGTERM" && Deno.build.os === "windows") {
// Ignores SIGTERM on windows.
} else {
Deno.removeSignalListener(event as Deno.Signal, listener);
}
} else {
super.off(event, listener);
}
return this;
}
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
override emit(event: string, ...args: any[]): boolean { listener: (...args: any[]) => void,
if (event.startsWith("SIG")) { ) {
if (event === "SIGBREAK" && Deno.build.os !== "windows") { if (notImplementedEvents.includes(event)) {
// Ignores SIGBREAK if the platform is not windows. warnNotImplemented(`process.off("${event}")`);
} else { EventEmitter.prototype.off.call(this, event, listener);
Deno.kill(Deno.pid, event as Deno.Signal); } else if (event.startsWith("SIG")) {
} if (event === "SIGBREAK" && Deno.build.os !== "windows") {
// Ignores SIGBREAK if the platform is not windows.
} else if (event === "SIGTERM" && Deno.build.os === "windows") {
// Ignores SIGTERM on windows.
} else { } else {
return super.emit(event, ...args); Deno.removeSignalListener(event as Deno.Signal, listener);
} }
} else {
return true; EventEmitter.prototype.off.call(this, event, listener);
} }
override prependListener( return this;
event: "exit", };
listener: (code: number) => void,
): this;
override prependListener(
event: typeof notImplementedEvents[number],
// deno-lint-ignore ban-types
listener: Function,
): this;
override prependListener(
event: string,
// deno-lint-ignore no-explicit-any
listener: (...args: any[]) => void,
): this {
if (notImplementedEvents.includes(event)) {
warnNotImplemented(`process.prependListener("${event}")`);
super.prependListener(event, listener);
} else if (event.startsWith("SIG")) {
if (event === "SIGBREAK" && Deno.build.os !== "windows") {
// Ignores SIGBREAK if the platform is not windows.
} else {
Deno.addSignalListener(event as Deno.Signal, listener);
}
} else {
super.prependListener(event, listener);
}
return this; Process.prototype.emit = function (
// deno-lint-ignore no-explicit-any
this: any,
event: string,
// deno-lint-ignore no-explicit-any
...args: any[]
): boolean {
if (event.startsWith("SIG")) {
if (event === "SIGBREAK" && Deno.build.os !== "windows") {
// Ignores SIGBREAK if the platform is not windows.
} else {
Deno.kill(Deno.pid, event as Deno.Signal);
}
} else {
return EventEmitter.prototype.emit.call(this, event, ...args);
} }
/** https://nodejs.org/api/process.html#process_process_pid */ return true;
get pid() { };
Process.prototype.prependListener = function (
// deno-lint-ignore no-explicit-any
this: any,
event: string,
// deno-lint-ignore no-explicit-any
listener: (...args: any[]) => void,
) {
if (notImplementedEvents.includes(event)) {
warnNotImplemented(`process.prependListener("${event}")`);
EventEmitter.prototype.prependListener.call(this, event, listener);
} else if (event.startsWith("SIG")) {
if (event === "SIGBREAK" && Deno.build.os !== "windows") {
// Ignores SIGBREAK if the platform is not windows.
} else {
Deno.addSignalListener(event as Deno.Signal, listener);
}
} else {
EventEmitter.prototype.prependListener.call(this, event, listener);
}
return this;
};
/** https://nodejs.org/api/process.html#process_process_pid */
Object.defineProperty(Process.prototype, "pid", {
get() {
return pid; return pid;
} },
});
/** https://nodejs.org/api/process.html#processppid */ /** https://nodejs.org/api/process.html#processppid */
get ppid() { Object.defineProperty(Process.prototype, "ppid", {
get() {
return Deno.ppid; return Deno.ppid;
} },
});
/** https://nodejs.org/api/process.html#process_process_platform */ /** https://nodejs.org/api/process.html#process_process_platform */
get platform() { Object.defineProperty(Process.prototype, "platform", {
get() {
return platform; return platform;
},
});
// https://nodejs.org/api/process.html#processsetsourcemapsenabledval
Process.prototype.setSourceMapsEnabled = (_val: boolean) => {
// This is a no-op in Deno. Source maps are always enabled.
// TODO(@satyarohith): support disabling source maps if needed.
};
Process.prototype.addListener = function (
// deno-lint-ignore no-explicit-any
this: any,
event: string,
// deno-lint-ignore no-explicit-any
listener: (...args: any[]) => void,
) {
if (notImplementedEvents.includes(event)) {
warnNotImplemented(`process.addListener("${event}")`);
} }
// https://nodejs.org/api/process.html#processsetsourcemapsenabledval return this.on(event, listener);
setSourceMapsEnabled(_val: boolean) { };
// This is a no-op in Deno. Source maps are always enabled.
// TODO(@satyarohith): support disabling source maps if needed. Process.prototype.removeListener = function (
// deno-lint-ignore no-explicit-any
this: any,
event: string, // deno-lint-ignore no-explicit-any
listener: (...args: any[]) => void,
) {
if (notImplementedEvents.includes(event)) {
warnNotImplemented(`process.removeListener("${event}")`);
} }
override addListener(event: "exit", listener: (code: number) => void): this; return this.off(event, listener);
override addListener( };
event: typeof notImplementedEvents[number],
// deno-lint-ignore ban-types
listener: Function,
): this;
override addListener(
event: string,
// deno-lint-ignore no-explicit-any
listener: (...args: any[]) => void,
): this {
if (notImplementedEvents.includes(event)) {
warnNotImplemented(`process.addListener("${event}")`);
}
return this.on(event, listener); /**
} * Returns the current high-resolution real time in a [seconds, nanoseconds]
* tuple.
*
* Note: You need to give --allow-hrtime permission to Deno to actually get
* nanoseconds precision values. If you don't give 'hrtime' permission, the returned
* values only have milliseconds precision.
*
* `time` is an optional parameter that must be the result of a previous process.hrtime() call to diff with the current time.
*
* These times are relative to an arbitrary time in the past, and not related to the time of day and therefore not subject to clock drift. The primary use is for measuring performance between intervals.
* https://nodejs.org/api/process.html#process_process_hrtime_time
*/
Process.prototype.hrtime = hrtime;
override removeListener( /**
event: "exit", * @private
listener: (code: number) => void, *
): this; * NodeJS internal, use process.kill instead
override removeListener( */
event: typeof notImplementedEvents[number], Process.prototype._kill = _kill;
// deno-lint-ignore ban-types
listener: Function,
): this;
override removeListener(
event: string,
// deno-lint-ignore no-explicit-any
listener: (...args: any[]) => void,
): this {
if (notImplementedEvents.includes(event)) {
warnNotImplemented(`process.removeListener("${event}")`);
}
return this.off(event, listener); /** https://nodejs.org/api/process.html#processkillpid-signal */
} Process.prototype.kill = kill;
/** Process.prototype.memoryUsage = memoryUsage;
* Returns the current high-resolution real time in a [seconds, nanoseconds]
* tuple.
*
* Note: You need to give --allow-hrtime permission to Deno to actually get
* nanoseconds precision values. If you don't give 'hrtime' permission, the returned
* values only have milliseconds precision.
*
* `time` is an optional parameter that must be the result of a previous process.hrtime() call to diff with the current time.
*
* These times are relative to an arbitrary time in the past, and not related to the time of day and therefore not subject to clock drift. The primary use is for measuring performance between intervals.
* https://nodejs.org/api/process.html#process_process_hrtime_time
*/
hrtime = hrtime;
/** /** https://nodejs.org/api/process.html#process_process_stderr */
* @private Process.prototype.stderr = stderr;
*
* NodeJS internal, use process.kill instead
*/
_kill = _kill;
/** https://nodejs.org/api/process.html#processkillpid-signal */ /** https://nodejs.org/api/process.html#process_process_stdin */
kill = kill; Process.prototype.stdin = stdin;
memoryUsage = memoryUsage; /** https://nodejs.org/api/process.html#process_process_stdout */
Process.prototype.stdout = stdout;
/** https://nodejs.org/api/process.html#process_process_stderr */ /** https://nodejs.org/api/process.html#process_process_version */
stderr = stderr; Process.prototype.version = version;
/** https://nodejs.org/api/process.html#process_process_stdin */ /** https://nodejs.org/api/process.html#process_process_versions */
stdin = stdin; Process.prototype.versions = versions;
/** https://nodejs.org/api/process.html#process_process_stdout */ /** https://nodejs.org/api/process.html#process_process_emitwarning_warning_options */
stdout = stdout; Process.prototype.emitWarning = emitWarning;
/** https://nodejs.org/api/process.html#process_process_version */ Process.prototype.binding = (name: BindingName) => {
version = version; return getBinding(name);
};
/** https://nodejs.org/api/process.html#process_process_versions */ /** https://nodejs.org/api/process.html#processumaskmask */
versions = versions; Process.prototype.umask = () => {
// Always return the system default umask value.
// We don't use Deno.umask here because it has a race
// condition bug.
// See https://github.com/denoland/deno_std/issues/1893#issuecomment-1032897779
return 0o22;
};
/** https://nodejs.org/api/process.html#process_process_emitwarning_warning_options */ /** This method is removed on Windows */
emitWarning = emitWarning; Process.prototype.getgid = getgid;
binding(name: BindingName) { /** This method is removed on Windows */
return getBinding(name); Process.prototype.getuid = getuid;
}
/** https://nodejs.org/api/process.html#processumaskmask */ /** This method is removed on Windows */
umask() { Process.prototype.geteuid = geteuid;
// Always return the system default umask value.
// We don't use Deno.umask here because it has a race
// condition bug.
// See https://github.com/denoland/deno_std/issues/1893#issuecomment-1032897779
return 0o22;
}
/** This method is removed on Windows */ // TODO(kt3k): Implement this when we added -e option to node compat mode
getgid = getgid; Process.prototype._eval = undefined;
/** This method is removed on Windows */ /** https://nodejs.org/api/process.html#processexecpath */
getuid = getuid;
/** This method is removed on Windows */ Object.defineProperty(Process.prototype, "execPath", {
geteuid = geteuid; get() {
// TODO(kt3k): Implement this when we added -e option to node compat mode
_eval: string | undefined = undefined;
/** https://nodejs.org/api/process.html#processexecpath */
get execPath() {
if (execPath) { if (execPath) {
return execPath; return execPath;
} }
execPath = Deno.execPath(); execPath = Deno.execPath();
return execPath; return execPath;
} },
set(path: string) {
set execPath(path: string) {
execPath = path; execPath = path;
} },
});
/** https://nodejs.org/api/process.html#processuptime */ /** https://nodejs.org/api/process.html#processuptime */
uptime() { Process.prototype.uptime = () => {
return Number((performance.now() / 1000).toFixed(9)); return Number((performance.now() / 1000).toFixed(9));
} };
#allowedFlags = buildAllowedFlags(); /** https://nodejs.org/api/process.html#processallowednodeenvironmentflags */
/** https://nodejs.org/api/process.html#processallowednodeenvironmentflags */ Object.defineProperty(Process.prototype, "allowedNodeEnvironmentFlags", {
get allowedNodeEnvironmentFlags() { get() {
return this.#allowedFlags; return ALLOWED_FLAGS;
} },
});
features = { inspector: false }; Process.prototype.features = { inspector: false };
// TODO(kt3k): Get the value from --no-deprecation flag. // TODO(kt3k): Get the value from --no-deprecation flag.
noDeprecation = false; Process.prototype.noDeprecation = false;
}
if (isWindows) { if (isWindows) {
delete Process.prototype.getgid; delete Process.prototype.getgid;
@ -696,6 +705,7 @@ if (isWindows) {
} }
/** https://nodejs.org/api/process.html#process_process */ /** https://nodejs.org/api/process.html#process_process */
// @ts-ignore TS doesn't work well with ES5 classes
const process = new Process(); const process = new Process();
Object.defineProperty(process, Symbol.toStringTag, { Object.defineProperty(process, Symbol.toStringTag, {

View file

@ -1094,3 +1094,12 @@ Deno.test({
assert(v >= 0); assert(v >= 0);
}, },
}); });
// Test for https://github.com/denoland/deno/issues/23863
Deno.test({
name: "instantiate process constructor without 'new' keyword",
fn() {
// This would throw
process.constructor.call({});
},
});