diff --git a/cli/tests/integration/mod.rs b/cli/tests/integration/mod.rs index 054a4e14ad..7ed1d6e0df 100644 --- a/cli/tests/integration/mod.rs +++ b/cli/tests/integration/mod.rs @@ -70,6 +70,8 @@ mod js_unit_tests; mod lint; #[path = "lsp_tests.rs"] mod lsp; +#[path = "node_unit_tests.rs"] +mod node_unit_tests; #[path = "npm_tests.rs"] mod npm; #[path = "repl_tests.rs"] diff --git a/cli/tests/integration/node_unit_tests.rs b/cli/tests/integration/node_unit_tests.rs new file mode 100644 index 0000000000..d29d988baf --- /dev/null +++ b/cli/tests/integration/node_unit_tests.rs @@ -0,0 +1,21 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use test_util as util; + +#[test] +fn node_unit_tests() { + let _g = util::http_server(); + + let mut deno = util::deno_cmd() + .current_dir(util::root_path()) + .arg("test") + .arg("--unstable") + .arg("-A") + .arg(util::tests_path().join("unit_node")) + .spawn() + .expect("failed to spawn script"); + + let status = deno.wait().expect("failed to wait for the child process"); + assert_eq!(Some(0), status.code()); + assert!(status.success()); +} diff --git a/cli/tests/unit_node/child_process_test.ts b/cli/tests/unit_node/child_process_test.ts new file mode 100644 index 0000000000..c6e2e3ef25 --- /dev/null +++ b/cli/tests/unit_node/child_process_test.ts @@ -0,0 +1,577 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import CP from "node:child_process"; +import { Buffer } from "node:buffer"; +import { + assert, + assertEquals, + assertExists, + assertNotStrictEquals, + assertStrictEquals, + assertStringIncludes, +} from "../../../test_util/std/testing/asserts.ts"; +import { Deferred, deferred } from "../../../test_util/std/async/deferred.ts"; +import * as path from "../../../test_util/std/path/mod.ts"; + +const { spawn, execFile, execFileSync, ChildProcess } = CP; + +function withTimeout(timeoutInMS: number): Deferred { + const promise = deferred(); + const timer = setTimeout(() => { + promise.reject("Timeout"); + }, timeoutInMS); + promise.then(() => { + clearTimeout(timer); + }); + return promise; +} + +// TODO(uki00a): Once Node.js's `parallel/test-child-process-spawn-error.js` works, this test case should be removed. +Deno.test("[node/child_process spawn] The 'error' event is emitted when no binary is found", async () => { + const promise = withTimeout(1000); + const childProcess = spawn("no-such-cmd"); + childProcess.on("error", (_err: Error) => { + // TODO(@bartlomieju) Assert an error message. + promise.resolve(); + }); + await promise; +}); + +Deno.test("[node/child_process spawn] The 'exit' event is emitted with an exit code after the child process ends", async () => { + const promise = withTimeout(3000); + const childProcess = spawn(Deno.execPath(), ["--help"], { + env: { NO_COLOR: "true" }, + }); + try { + let exitCode = null; + childProcess.on("exit", (code: number) => { + promise.resolve(); + exitCode = code; + }); + await promise; + assertStrictEquals(exitCode, 0); + assertStrictEquals(childProcess.exitCode, exitCode); + } finally { + childProcess.kill(); + childProcess.stdout?.destroy(); + childProcess.stderr?.destroy(); + } +}); + +Deno.test("[node/child_process disconnect] the method exists", async () => { + const promise = withTimeout(1000); + const childProcess = spawn(Deno.execPath(), ["--help"], { + env: { NO_COLOR: "true" }, + }); + try { + childProcess.disconnect(); + childProcess.on("exit", () => { + promise.resolve(); + }); + await promise; + } finally { + childProcess.kill(); + childProcess.stdout?.destroy(); + childProcess.stderr?.destroy(); + } +}); + +Deno.test({ + name: "[node/child_process spawn] Verify that stdin and stdout work", + fn: async () => { + const promise = withTimeout(3000); + const childProcess = spawn(Deno.execPath(), ["fmt", "-"], { + env: { NO_COLOR: "true" }, + stdio: ["pipe", "pipe"], + }); + try { + assert(childProcess.stdin, "stdin should be defined"); + assert(childProcess.stdout, "stdout should be defined"); + let data = ""; + childProcess.stdout.on("data", (chunk) => { + data += chunk; + }); + childProcess.stdin.write(" console.log('hello')", "utf-8"); + childProcess.stdin.end(); + childProcess.on("close", () => { + promise.resolve(); + }); + await promise; + assertStrictEquals(data, `console.log("hello");\n`); + } finally { + childProcess.kill(); + } + }, +}); + +Deno.test({ + name: "[node/child_process spawn] stdin and stdout with binary data", + fn: async () => { + const promise = withTimeout(10000); + const p = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "./testdata/binary_stdio.js", + ); + const childProcess = spawn(Deno.execPath(), ["run", p], { + env: { NO_COLOR: "true" }, + stdio: ["pipe", "pipe"], + }); + try { + assert(childProcess.stdin, "stdin should be defined"); + assert(childProcess.stdout, "stdout should be defined"); + let data: Buffer; + childProcess.stdout.on("data", (chunk) => { + data = chunk; + }); + const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + childProcess.stdin.write(buffer); + childProcess.stdin.end(); + childProcess.on("close", () => { + promise.resolve(); + }); + await promise; + assertEquals(new Uint8Array(data!), buffer); + } finally { + childProcess.kill(); + } + }, +}); + +async function spawnAndGetEnvValue( + inputValue: string | number | boolean, +): Promise { + const promise = withTimeout(3000); + const env = spawn( + `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`, + { + env: { BAZ: String(inputValue), NO_COLOR: "true" }, + shell: true, + }, + ); + try { + let envOutput = ""; + + assert(env.stdout); + env.on("error", (err: Error) => promise.reject(err)); + env.stdout.on("data", (data) => { + envOutput += data; + }); + env.on("close", () => { + promise.resolve(envOutput.trim()); + }); + return await promise; + } finally { + env.kill(); + } +} + +Deno.test({ + ignore: Deno.build.os === "windows", + name: + "[node/child_process spawn] Verify that environment values can be numbers", + async fn() { + const envOutputValue = await spawnAndGetEnvValue(42); + assertStrictEquals(envOutputValue, "42"); + }, +}); + +Deno.test({ + ignore: Deno.build.os === "windows", + name: + "[node/child_process spawn] Verify that environment values can be booleans", + async fn() { + const envOutputValue = await spawnAndGetEnvValue(false); + assertStrictEquals(envOutputValue, "false"); + }, +}); + +/* Start of ported part */ 3; +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Ported from Node 15.5.1 + +// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-event.js` works. +Deno.test("[child_process spawn] 'spawn' event", async () => { + const timeout = withTimeout(3000); + const subprocess = spawn(Deno.execPath(), ["eval", "console.log('ok')"]); + + let didSpawn = false; + subprocess.on("spawn", function () { + didSpawn = true; + }); + + function mustNotBeCalled() { + timeout.reject(new Error("function should not have been called")); + } + + const promises = [] as Promise[]; + function mustBeCalledAfterSpawn() { + const promise = deferred(); + promises.push(promise); + return () => { + if (didSpawn) { + promise.resolve(); + } else { + promise.reject( + new Error("function should be called after the 'spawn' event"), + ); + } + }; + } + + subprocess.on("error", mustNotBeCalled); + subprocess.stdout!.on("data", mustBeCalledAfterSpawn()); + subprocess.stdout!.on("end", mustBeCalledAfterSpawn()); + subprocess.stdout!.on("close", mustBeCalledAfterSpawn()); + subprocess.stderr!.on("data", mustNotBeCalled); + subprocess.stderr!.on("end", mustBeCalledAfterSpawn()); + subprocess.stderr!.on("close", mustBeCalledAfterSpawn()); + subprocess.on("exit", mustBeCalledAfterSpawn()); + subprocess.on("close", mustBeCalledAfterSpawn()); + + try { + await Promise.race([Promise.all(promises), timeout]); + timeout.resolve(); + } finally { + subprocess.kill(); + } +}); + +// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. +Deno.test("[child_process spawn] Verify that a shell is executed", async () => { + const promise = withTimeout(3000); + const doesNotExist = spawn("does-not-exist", { shell: true }); + try { + assertNotStrictEquals(doesNotExist.spawnfile, "does-not-exist"); + doesNotExist.on("error", () => { + promise.reject("The 'error' event must not be emitted."); + }); + doesNotExist.on("exit", (code: number, signal: null) => { + assertStrictEquals(signal, null); + + if (Deno.build.os === "windows") { + assertStrictEquals(code, 1); // Exit code of cmd.exe + } else { + assertStrictEquals(code, 127); // Exit code of /bin/sh }); + } + + promise.resolve(); + }); + await promise; + } finally { + doesNotExist.kill(); + doesNotExist.stdout?.destroy(); + doesNotExist.stderr?.destroy(); + } +}); + +// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. +Deno.test({ + ignore: Deno.build.os === "windows", + name: "[node/child_process spawn] Verify that passing arguments works", + async fn() { + const promise = withTimeout(3000); + const echo = spawn("echo", ["foo"], { + shell: true, + }); + let echoOutput = ""; + + try { + assertStrictEquals( + echo.spawnargs[echo.spawnargs.length - 1].replace(/"/g, ""), + "echo foo", + ); + assert(echo.stdout); + echo.stdout.on("data", (data) => { + echoOutput += data; + }); + echo.on("close", () => { + assertStrictEquals(echoOutput.trim(), "foo"); + promise.resolve(); + }); + await promise; + } finally { + echo.kill(); + } + }, +}); + +// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. +Deno.test({ + ignore: Deno.build.os === "windows", + name: "[node/child_process spawn] Verity that shell features can be used", + async fn() { + const promise = withTimeout(3000); + const cmd = "echo bar | cat"; + const command = spawn(cmd, { + shell: true, + }); + try { + let commandOutput = ""; + + assert(command.stdout); + command.stdout.on("data", (data) => { + commandOutput += data; + }); + + command.on("close", () => { + assertStrictEquals(commandOutput.trim(), "bar"); + promise.resolve(); + }); + + await promise; + } finally { + command.kill(); + } + }, +}); + +// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. +Deno.test({ + ignore: Deno.build.os === "windows", + name: + "[node/child_process spawn] Verity that environment is properly inherited", + async fn() { + const promise = withTimeout(3000); + const env = spawn( + `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`, + { + env: { BAZ: "buzz", NO_COLOR: "true" }, + shell: true, + }, + ); + try { + let envOutput = ""; + + assert(env.stdout); + env.on("error", (err: Error) => promise.reject(err)); + env.stdout.on("data", (data) => { + envOutput += data; + }); + env.on("close", () => { + assertStrictEquals(envOutput.trim(), "buzz"); + promise.resolve(); + }); + await promise; + } finally { + env.kill(); + } + }, +}); +/* End of ported part */ + +Deno.test({ + name: "[node/child_process execFile] Get stdout as a string", + async fn() { + let child: unknown; + const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "./testdata/exec_file_text_output.js", + ); + const promise = new Promise((resolve, reject) => { + child = execFile(Deno.execPath(), ["run", script], (err, stdout) => { + if (err) reject(err); + else if (stdout) resolve(stdout as string); + else resolve(null); + }); + }); + try { + const stdout = await promise; + assertEquals(stdout, "Hello World!\n"); + } finally { + if (child instanceof ChildProcess) { + child.kill(); + } + } + }, +}); + +Deno.test({ + name: "[node/child_process execFile] Get stdout as a buffer", + async fn() { + let child: unknown; + const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "./testdata/exec_file_text_output.js", + ); + const promise = new Promise((resolve, reject) => { + child = execFile( + Deno.execPath(), + ["run", script], + { encoding: "buffer" }, + (err, stdout) => { + if (err) reject(err); + else if (stdout) resolve(stdout as Buffer); + else resolve(null); + }, + ); + }); + try { + const stdout = await promise; + assert(Buffer.isBuffer(stdout)); + assertEquals(stdout.toString("utf8"), "Hello World!\n"); + } finally { + if (child instanceof ChildProcess) { + child.kill(); + } + } + }, +}); + +Deno.test({ + name: "[node/child_process execFile] Get stderr", + async fn() { + let child: unknown; + const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "./testdata/exec_file_text_error.js", + ); + const promise = new Promise< + { err: Error | null; stderr?: string | Buffer } + >((resolve) => { + child = execFile(Deno.execPath(), ["run", script], (err, _, stderr) => { + resolve({ err, stderr }); + }); + }); + try { + const { err, stderr } = await promise; + if (child instanceof ChildProcess) { + assertEquals(child.exitCode, 1); + assertEquals(stderr, "yikes!\n"); + } else { + throw err; + } + } finally { + if (child instanceof ChildProcess) { + child.kill(); + } + } + }, +}); + +Deno.test({ + name: "[node/child_process execFile] Exceed given maxBuffer limit", + async fn() { + let child: unknown; + const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "./testdata/exec_file_text_error.js", + ); + const promise = new Promise< + { err: Error | null; stderr?: string | Buffer } + >((resolve) => { + child = execFile(Deno.execPath(), ["run", script], { + encoding: "buffer", + maxBuffer: 3, + }, (err, _, stderr) => { + resolve({ err, stderr }); + }); + }); + try { + const { err, stderr } = await promise; + if (child instanceof ChildProcess) { + assert(err); + assertEquals( + // deno-lint-ignore no-explicit-any + (err as any).code, + "ERR_CHILD_PROCESS_STDIO_MAXBUFFER", + ); + assertEquals(err.message, "stderr maxBuffer length exceeded"); + assertEquals((stderr as Buffer).toString("utf8"), "yik"); + } else { + throw err; + } + } finally { + if (child instanceof ChildProcess) { + child.kill(); + } + } + }, +}); + +Deno.test({ + name: "[node/child_process] ChildProcess.kill()", + async fn() { + const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "./testdata/infinite_loop.js", + ); + const childProcess = spawn(Deno.execPath(), ["run", script]); + const p = withTimeout(3000); + childProcess.on("exit", () => p.resolve()); + childProcess.kill("SIGKILL"); + await p; + assert(childProcess.killed); + assertEquals(childProcess.signalCode, "SIGKILL"); + assertExists(childProcess.exitCode); + }, +}); + +Deno.test({ + name: "[node/child_process] ChildProcess.unref()", + async fn() { + const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "testdata", + "child_process_unref.js", + ); + const childProcess = spawn(Deno.execPath(), [ + "run", + "-A", + "--unstable", + script, + ]); + const p = deferred(); + childProcess.on("exit", () => p.resolve()); + await p; + }, +}); + +Deno.test({ + name: "[node/child_process] child_process.fork", + async fn() { + const testdataDir = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "testdata", + ); + const script = path.join( + testdataDir, + "node_modules", + "foo", + "index.js", + ); + const p = deferred(); + const cp = CP.fork(script, [], { cwd: testdataDir, stdio: "pipe" }); + let output = ""; + cp.on("close", () => p.resolve()); + cp.stdout?.on("data", (data) => { + output += data; + }); + await p; + assertEquals(output, "foo\ntrue\ntrue\ntrue\n"); + }, +}); + +Deno.test("[node/child_process execFileSync] 'inherit' stdout and stderr", () => { + execFileSync(Deno.execPath(), ["--help"], { stdio: "inherit" }); +}); + +Deno.test( + "[node/child_process spawn] supports windowsVerbatimArguments option", + { ignore: Deno.build.os !== "windows" }, + async () => { + const cmdFinished = deferred(); + let output = ""; + const cp = spawn("cmd", ["/d", "/s", "/c", '"deno ^"--version^""'], { + stdio: "pipe", + windowsVerbatimArguments: true, + }); + cp.on("close", () => cmdFinished.resolve()); + cp.stdout?.on("data", (data) => { + output += data; + }); + await cmdFinished; + assertStringIncludes(output, "deno"); + assertStringIncludes(output, "v8"); + assertStringIncludes(output, "typescript"); + }, +); diff --git a/cli/tests/unit_node/process_test.ts b/cli/tests/unit_node/process_test.ts new file mode 100644 index 0000000000..7310e4ad7c --- /dev/null +++ b/cli/tests/unit_node/process_test.ts @@ -0,0 +1,713 @@ +// deno-lint-ignore-file no-undef +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import process, { argv, env } from "node:process"; +import { + assert, + assertEquals, + assertFalse, + assertObjectMatch, + assertStrictEquals, + assertThrows, +} from "../../../test_util/std/testing/asserts.ts"; +import { stripColor } from "../../../test_util/std/fmt/colors.ts"; +import { deferred } from "../../../test_util/std/async/deferred.ts"; +import * as path from "../../../test_util/std/path/mod.ts"; +import { delay } from "../../../test_util/std/async/delay.ts"; + +const testDir = new URL(".", import.meta.url); + +Deno.test({ + name: "process.cwd and process.chdir success", + fn() { + assertEquals(process.cwd(), Deno.cwd()); + + const currentDir = Deno.cwd(); + + const tempDir = Deno.makeTempDirSync(); + process.chdir(tempDir); + assertEquals( + Deno.realPathSync(process.cwd()), + Deno.realPathSync(tempDir), + ); + + process.chdir(currentDir); + }, +}); + +Deno.test({ + name: "process.chdir failure", + fn() { + assertThrows( + () => { + process.chdir("non-existent-directory-name"); + }, + Deno.errors.NotFound, + "file", + // On every OS Deno returns: "No such file" except for Windows, where it's: + // "The system cannot find the file specified. (os error 2)" so "file" is + // the only common string here. + ); + }, +}); + +Deno.test({ + name: "process.version", + fn() { + assertEquals(typeof process, "object"); + assertEquals(typeof process.version, "string"); + assertEquals(typeof process.versions, "object"); + assertEquals(typeof process.versions.node, "string"); + assertEquals(typeof process.versions.v8, "string"); + assertEquals(typeof process.versions.uv, "string"); + assertEquals(typeof process.versions.zlib, "string"); + assertEquals(typeof process.versions.brotli, "string"); + assertEquals(typeof process.versions.ares, "string"); + assertEquals(typeof process.versions.modules, "string"); + assertEquals(typeof process.versions.nghttp2, "string"); + assertEquals(typeof process.versions.napi, "string"); + assertEquals(typeof process.versions.llhttp, "string"); + assertEquals(typeof process.versions.openssl, "string"); + assertEquals(typeof process.versions.cldr, "string"); + assertEquals(typeof process.versions.icu, "string"); + assertEquals(typeof process.versions.tz, "string"); + assertEquals(typeof process.versions.unicode, "string"); + // These two are not present in `process.versions` in Node, but we + // add them anyway + assertEquals(typeof process.versions.deno, "string"); + assertEquals(typeof process.versions.typescript, "string"); + }, +}); + +Deno.test({ + name: "process.platform", + fn() { + assertEquals(typeof process.platform, "string"); + }, +}); + +Deno.test({ + name: "process.mainModule", + fn() { + assertEquals(process.mainModule, undefined); + // Check that it is writable + // @ts-ignore these are deprecated now + process.mainModule = "foo"; + // @ts-ignore these are deprecated now + assertEquals(process.mainModule, "foo"); + }, +}); + +Deno.test({ + name: "process.arch", + fn() { + assertEquals(typeof process.arch, "string"); + if (Deno.build.arch == "x86_64") { + assertEquals(process.arch, "x64"); + } else if (Deno.build.arch == "aarch64") { + assertEquals(process.arch, "arm64"); + } else { + throw new Error("unreachable"); + } + }, +}); + +Deno.test({ + name: "process.pid", + fn() { + assertEquals(typeof process.pid, "number"); + assertEquals(process.pid, Deno.pid); + }, +}); + +Deno.test({ + name: "process.on", + async fn() { + assertEquals(typeof process.on, "function"); + + let triggered = false; + process.on("exit", () => { + triggered = true; + }); + // @ts-ignore fix the type here + process.emit("exit"); + assert(triggered); + + const cwd = path.dirname(path.fromFileUrl(import.meta.url)); + + const command = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--quiet", + "--unstable", + "./testdata/process_exit.ts", + ], + cwd, + }); + const { stdout } = await command.output(); + + const decoder = new TextDecoder(); + assertEquals(stripColor(decoder.decode(stdout).trim()), "1\n2"); + }, +}); + +Deno.test({ + name: "process.on signal", + ignore: Deno.build.os == "windows", + async fn() { + const promise = deferred(); + let c = 0; + const listener = () => { + c += 1; + }; + process.on("SIGINT", listener); + setTimeout(async () => { + // Sends SIGINT 3 times. + for (const _ of Array(3)) { + await delay(20); + Deno.kill(Deno.pid, "SIGINT"); + } + await delay(20); + Deno.removeSignalListener("SIGINT", listener); + promise.resolve(); + }); + await promise; + assertEquals(c, 3); + }, +}); + +Deno.test({ + name: "process.off signal", + ignore: Deno.build.os == "windows", + async fn() { + const promise = deferred(); + let c = 0; + const listener = () => { + c += 1; + process.off("SIGINT", listener); + }; + process.on("SIGINT", listener); + setTimeout(async () => { + // Sends SIGINT 3 times. + for (const _ of Array(3)) { + await delay(20); + Deno.kill(Deno.pid, "SIGINT"); + } + await delay(20); + promise.resolve(); + }); + await promise; + assertEquals(c, 1); + }, +}); + +Deno.test({ + name: "process.on SIGBREAK doesn't throw", + fn() { + const listener = () => {}; + process.on("SIGBREAK", listener); + process.off("SIGBREAK", listener); + }, +}); + +Deno.test({ + name: "process.on SIGTERM doesn't throw on windows", + ignore: Deno.build.os !== "windows", + fn() { + const listener = () => {}; + process.on("SIGTERM", listener); + process.off("SIGTERM", listener); + }, +}); + +Deno.test({ + name: "process.argv", + fn() { + assert(Array.isArray(argv)); + assert(Array.isArray(process.argv)); + assert( + process.argv[0].match(/[^/\\]*deno[^/\\]*$/), + "deno included in the file name of argv[0]", + ); + assertEquals( + process.argv[1], + path.fromFileUrl(Deno.mainModule), + ); + // argv supports array methods. + assert(Array.isArray(process.argv.slice(2))); + assertEquals(process.argv.indexOf(Deno.execPath()), 0); + assertEquals(process.argv.indexOf(path.fromFileUrl(Deno.mainModule)), 1); + }, +}); + +Deno.test({ + name: "process.execArgv", + fn() { + assert(Array.isArray(process.execArgv)); + assert(process.execArgv.length == 0); + // execArgv supports array methods. + assert(Array.isArray(process.argv.slice(0))); + assertEquals(process.argv.indexOf("foo"), -1); + }, +}); + +Deno.test({ + name: "process.env", + fn() { + Deno.env.set("HELLO", "WORLD"); + + assertObjectMatch(process.env, Deno.env.toObject()); + + assertEquals(typeof (process.env.HELLO), "string"); + assertEquals(process.env.HELLO, "WORLD"); + + assertEquals(typeof env.HELLO, "string"); + assertEquals(env.HELLO, "WORLD"); + + assert(Object.getOwnPropertyNames(process.env).includes("HELLO")); + assert(Object.keys(process.env).includes("HELLO")); + + assert(Object.prototype.hasOwnProperty.call(process.env, "HELLO")); + assert( + !Object.prototype.hasOwnProperty.call( + process.env, + "SURELY_NON_EXISTENT_VAR", + ), + ); + + // deno-lint-ignore no-prototype-builtins + assert(process.env.hasOwnProperty("HELLO")); + assert("HELLO" in process.env); + assert(Object.keys(process.env.valueOf()).includes("HELLO")); + + assertEquals(process.env.toString(), "[object Object]"); + assertEquals(process.env.toLocaleString(), "[object Object]"); + + // should not error when assigning false to an env var + process.env.HELLO = false as unknown as string; + assertEquals(process.env.HELLO, "false"); + process.env.HELLO = "WORLD"; + assertEquals(process.env.HELLO, "WORLD"); + }, +}); + +Deno.test({ + name: "process.env requires scoped env permission", + permissions: { env: ["FOO"] }, + fn() { + Deno.env.set("FOO", "1"); + assert("FOO" in process.env); + assertFalse("BAR" in process.env); + assert(Object.hasOwn(process.env, "FOO")); + assertFalse(Object.hasOwn(process.env, "BAR")); + }, +}); + +Deno.test({ + name: "process.env doesn't throw with invalid env var names", + fn() { + assertEquals(process.env[""], undefined); + assertEquals(process.env["\0"], undefined); + assertEquals(process.env["=c:"], undefined); + assertFalse(Object.hasOwn(process.env, "")); + assertFalse(Object.hasOwn(process.env, "\0")); + assertFalse(Object.hasOwn(process.env, "=c:")); + assertFalse("" in process.env); + assertFalse("\0" in process.env); + assertFalse("=c:" in process.env); + }, +}); + +Deno.test({ + name: "process.stdin", + fn() { + assertEquals(process.stdin.fd, Deno.stdin.rid); + assertEquals(process.stdin.isTTY, Deno.isatty(Deno.stdin.rid)); + }, +}); + +Deno.test({ + name: "process.stdin readable with a TTY", + // TODO(PolarETech): Run this test even in non tty environment + ignore: !Deno.isatty(Deno.stdin.rid), + async fn() { + const promise = deferred(); + const expected = ["foo", "bar", null, "end"]; + const data: (string | null)[] = []; + + process.stdin.setEncoding("utf8"); + process.stdin.on("readable", () => { + data.push(process.stdin.read()); + }); + process.stdin.on("end", () => { + data.push("end"); + }); + + process.stdin.push("foo"); + process.nextTick(() => { + process.stdin.push("bar"); + process.nextTick(() => { + process.stdin.push(null); + promise.resolve(); + }); + }); + + await promise; + assertEquals(process.stdin.readableHighWaterMark, 0); + assertEquals(data, expected); + }, +}); + +Deno.test({ + name: "process.stdin readable with piping a file", + async fn() { + const expected = ["65536", "foo", "bar", "null", "end"]; + const scriptPath = "./testdata/process_stdin.ts"; + const filePath = "./testdata/process_stdin_dummy.txt"; + + const shell = Deno.build.os === "windows" ? "cmd.exe" : "/bin/sh"; + const cmd = `"${Deno.execPath()}" run ${scriptPath} < ${filePath}`; + const args = Deno.build.os === "windows" ? ["/d", "/c", cmd] : ["-c", cmd]; + + const p = new Deno.Command(shell, { + args, + stdin: "null", + stdout: "piped", + stderr: "null", + windowsRawArguments: true, + cwd: testDir, + }); + + const { stdout } = await p.output(); + const data = new TextDecoder().decode(stdout).trim().split("\n"); + assertEquals(data, expected); + }, +}); + +Deno.test({ + name: "process.stdin readable with piping a stream", + async fn() { + const expected = ["16384", "foo", "bar", "null", "end"]; + const scriptPath = "./testdata/process_stdin.ts"; + + const command = new Deno.Command(Deno.execPath(), { + args: ["run", scriptPath], + stdin: "piped", + stdout: "piped", + stderr: "null", + cwd: testDir, + }); + const child = command.spawn(); + + const writer = await child.stdin.getWriter(); + writer.ready + .then(() => writer.write(new TextEncoder().encode("foo\nbar"))) + .then(() => writer.releaseLock()) + .then(() => child.stdin.close()); + + const { stdout } = await child.output(); + const data = new TextDecoder().decode(stdout).trim().split("\n"); + assertEquals(data, expected); + }, +}); + +Deno.test({ + name: "process.stdin readable with piping a socket", + ignore: Deno.build.os === "windows", + async fn() { + const expected = ["16384", "foo", "bar", "null", "end"]; + const scriptPath = "./testdata/process_stdin.ts"; + + const listener = Deno.listen({ hostname: "127.0.0.1", port: 9000 }); + listener.accept().then(async (conn) => { + await conn.write(new TextEncoder().encode("foo\nbar")); + conn.close(); + listener.close(); + }); + + const shell = "/bin/bash"; + const cmd = + `"${Deno.execPath()}" run ${scriptPath} < /dev/tcp/127.0.0.1/9000`; + const args = ["-c", cmd]; + + const p = new Deno.Command(shell, { + args, + stdin: "null", + stdout: "piped", + stderr: "null", + cwd: testDir, + }); + + const { stdout } = await p.output(); + const data = new TextDecoder().decode(stdout).trim().split("\n"); + assertEquals(data, expected); + }, +}); + +Deno.test({ + name: "process.stdin readable with null", + async fn() { + const expected = ["65536", "null", "end"]; + const scriptPath = "./testdata/process_stdin.ts"; + + const command = new Deno.Command(Deno.execPath(), { + args: ["run", scriptPath], + stdin: "null", + stdout: "piped", + stderr: "null", + cwd: testDir, + }); + + const { stdout } = await command.output(); + const data = new TextDecoder().decode(stdout).trim().split("\n"); + assertEquals(data, expected); + }, +}); + +// TODO(kt3k): Enable this test case. 'readable' event handler in +// `process_stdin.ts` doesn't work now +Deno.test({ + name: "process.stdin readable with unsuitable stdin", + ignore: true, + // // TODO(PolarETech): Prepare a similar test that can be run on Windows + // ignore: Deno.build.os === "windows", + async fn() { + const expected = ["16384", "null", "end"]; + const scriptPath = "./testdata/process_stdin.ts"; + const directoryPath = "./testdata/"; + + const shell = "/bin/bash"; + const cmd = `"${Deno.execPath()}" run ${scriptPath} < ${directoryPath}`; + const args = ["-c", cmd]; + + const p = new Deno.Command(shell, { + args, + stdin: "null", + stdout: "piped", + stderr: "null", + windowsRawArguments: true, + cwd: testDir, + }); + + const { stdout } = await p.output(); + const data = new TextDecoder().decode(stdout).trim().split("\n"); + assertEquals(data, expected); + }, +}); + +Deno.test({ + name: "process.stdout", + fn() { + assertEquals(process.stdout.fd, Deno.stdout.rid); + const isTTY = Deno.isatty(Deno.stdout.rid); + assertEquals(process.stdout.isTTY, isTTY); + const consoleSize = isTTY ? Deno.consoleSize() : undefined; + assertEquals(process.stdout.columns, consoleSize?.columns); + assertEquals(process.stdout.rows, consoleSize?.rows); + assertEquals( + `${process.stdout.getWindowSize()}`, + `${consoleSize && [consoleSize.columns, consoleSize.rows]}`, + ); + + if (isTTY) { + assertStrictEquals(process.stdout.cursorTo(1, 2, () => {}), true); + assertStrictEquals(process.stdout.moveCursor(3, 4, () => {}), true); + assertStrictEquals(process.stdout.clearLine(1, () => {}), true); + assertStrictEquals(process.stdout.clearScreenDown(() => {}), true); + } else { + assertStrictEquals(process.stdout.cursorTo, undefined); + assertStrictEquals(process.stdout.moveCursor, undefined); + assertStrictEquals(process.stdout.clearLine, undefined); + assertStrictEquals(process.stdout.clearScreenDown, undefined); + } + }, +}); + +Deno.test({ + name: "process.stderr", + fn() { + assertEquals(process.stderr.fd, Deno.stderr.rid); + const isTTY = Deno.isatty(Deno.stderr.rid); + assertEquals(process.stderr.isTTY, isTTY); + const consoleSize = isTTY ? Deno.consoleSize() : undefined; + assertEquals(process.stderr.columns, consoleSize?.columns); + assertEquals(process.stderr.rows, consoleSize?.rows); + assertEquals( + `${process.stderr.getWindowSize()}`, + `${consoleSize && [consoleSize.columns, consoleSize.rows]}`, + ); + + if (isTTY) { + assertStrictEquals(process.stderr.cursorTo(1, 2, () => {}), true); + assertStrictEquals(process.stderr.moveCursor(3, 4, () => {}), true); + assertStrictEquals(process.stderr.clearLine(1, () => {}), true); + assertStrictEquals(process.stderr.clearScreenDown(() => {}), true); + } else { + assertStrictEquals(process.stderr.cursorTo, undefined); + assertStrictEquals(process.stderr.moveCursor, undefined); + assertStrictEquals(process.stderr.clearLine, undefined); + assertStrictEquals(process.stderr.clearScreenDown, undefined); + } + }, +}); + +Deno.test({ + name: "process.nextTick", + async fn() { + let withoutArguments = false; + process.nextTick(() => { + withoutArguments = true; + }); + + const expected = 12; + let result; + process.nextTick((x: number) => { + result = x; + }, 12); + + await delay(10); + assert(withoutArguments); + assertEquals(result, expected); + }, +}); + +Deno.test({ + name: "process.hrtime", + // TODO(kt3k): Enable this test + ignore: true, + fn() { + const [sec0, nano0] = process.hrtime(); + // seconds and nano seconds are positive integers. + assert(sec0 > 0); + assert(Number.isInteger(sec0)); + assert(nano0 > 0); + assert(Number.isInteger(nano0)); + + const [sec1, nano1] = process.hrtime(); + // the later call returns bigger value + assert(sec1 >= sec0); + assert(nano1 > nano0); + + const [sec2, nano2] = process.hrtime([sec1, nano1]); + // the difference of the 2 calls is a small positive value. + assertEquals(sec2, 0); + assert(nano2 > 0); + }, +}); + +Deno.test({ + name: "process.hrtime.bigint", + fn() { + const time = process.hrtime.bigint(); + assertEquals(typeof time, "bigint"); + assert(time > 0n); + }, +}); + +Deno.test("process.on, process.off, process.removeListener doesn't throw on unimplemented events", () => { + const events = [ + "beforeExit", + "disconnect", + "message", + "multipleResolves", + "rejectionHandled", + "uncaughtException", + "uncaughtExceptionMonitor", + "unhandledRejection", + "worker", + ]; + const handler = () => {}; + events.forEach((ev) => { + process.on(ev, handler); + assertEquals(process.listenerCount(ev), 1); + process.off(ev, handler); + assertEquals(process.listenerCount(ev), 0); + process.on(ev, handler); + assertEquals(process.listenerCount(ev), 1); + process.removeListener(ev, handler); + assertEquals(process.listenerCount(ev), 0); + }); +}); + +Deno.test("process.memoryUsage()", () => { + const mem = process.memoryUsage(); + assert(typeof mem.rss === "number"); + assert(typeof mem.heapTotal === "number"); + assert(typeof mem.heapUsed === "number"); + assert(typeof mem.external === "number"); + assert(typeof mem.arrayBuffers === "number"); + assertEquals(mem.arrayBuffers, 0); +}); + +Deno.test("process.memoryUsage.rss()", () => { + const rss = process.memoryUsage.rss(); + assert(typeof rss === "number"); +}); + +Deno.test("process.exitCode", () => { + assert(process.exitCode === undefined); + process.exitCode = 127; + assert(process.exitCode === 127); +}); + +Deno.test("process.config", () => { + assert(process.config !== undefined); + assert(process.config.target_defaults !== undefined); + assert(process.config.variables !== undefined); +}); + +Deno.test("process._exiting", () => { + // @ts-ignore fix the type here + assert(process._exiting === false); +}); + +Deno.test("process.execPath", () => { + assertEquals(process.execPath, process.argv[0]); +}); + +Deno.test("process.execPath is writable", () => { + // pnpm writes to process.execPath + // https://github.com/pnpm/pnpm/blob/67d8b65d2e8da1df3725034b8c5b1fcf3af4ad81/packages/config/src/index.ts#L175 + const originalExecPath = process.execPath; + try { + process.execPath = "/path/to/node"; + assertEquals(process.execPath, "/path/to/node"); + } finally { + process.execPath = originalExecPath; + } +}); + +Deno.test("process.getgid", () => { + if (Deno.build.os === "windows") { + assertEquals(process.getgid, undefined); + } else { + assertEquals(process.getgid?.(), Deno.gid()); + } +}); + +Deno.test("process.getuid", () => { + if (Deno.build.os === "windows") { + assertEquals(process.getuid, undefined); + } else { + assertEquals(process.getuid?.(), Deno.uid()); + } +}); + +Deno.test({ + name: "process.exit", + async fn() { + const command = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--quiet", + "--unstable", + "./testdata/process_exit2.ts", + ], + cwd: testDir, + }); + const { stdout } = await command.output(); + + const decoder = new TextDecoder(); + assertEquals(stripColor(decoder.decode(stdout).trim()), "exit"); + }, +}); diff --git a/cli/tests/unit_node/testdata/binary_stdio.js b/cli/tests/unit_node/testdata/binary_stdio.js new file mode 100644 index 0000000000..aa370a933c --- /dev/null +++ b/cli/tests/unit_node/testdata/binary_stdio.js @@ -0,0 +1,11 @@ +const buffer = new Uint8Array(10); +const nread = await Deno.stdin.read(buffer); + +if (nread != 10) { + throw new Error("Too little data read"); +} + +const nwritten = await Deno.stdout.write(buffer); +if (nwritten != 10) { + throw new Error("Too little data written"); +} diff --git a/cli/tests/unit_node/testdata/child_process_unref.js b/cli/tests/unit_node/testdata/child_process_unref.js new file mode 100644 index 0000000000..cc7815d97c --- /dev/null +++ b/cli/tests/unit_node/testdata/child_process_unref.js @@ -0,0 +1,9 @@ +import cp from "node:child_process"; +import * as path from "node:path"; + +const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "infinite_loop.js", +); +const childProcess = cp.spawn(Deno.execPath(), ["run", script]); +childProcess.unref(); diff --git a/cli/tests/unit_node/testdata/exec_file_text_error.js b/cli/tests/unit_node/testdata/exec_file_text_error.js new file mode 100644 index 0000000000..9697e60442 --- /dev/null +++ b/cli/tests/unit_node/testdata/exec_file_text_error.js @@ -0,0 +1,2 @@ +console.error("yikes!"); +Deno.exit(1); diff --git a/cli/tests/unit_node/testdata/exec_file_text_output.js b/cli/tests/unit_node/testdata/exec_file_text_output.js new file mode 100644 index 0000000000..019c0f4bc8 --- /dev/null +++ b/cli/tests/unit_node/testdata/exec_file_text_output.js @@ -0,0 +1 @@ +console.log("Hello World!"); diff --git a/cli/tests/unit_node/testdata/infinite_loop.js b/cli/tests/unit_node/testdata/infinite_loop.js new file mode 100644 index 0000000000..0e6540a7b7 --- /dev/null +++ b/cli/tests/unit_node/testdata/infinite_loop.js @@ -0,0 +1,3 @@ +while (true) { + await new Promise((resolve) => setTimeout(resolve, 1000)); +} diff --git a/cli/tests/unit_node/testdata/node_modules/foo/index.js b/cli/tests/unit_node/testdata/node_modules/foo/index.js new file mode 100644 index 0000000000..24faba789a --- /dev/null +++ b/cli/tests/unit_node/testdata/node_modules/foo/index.js @@ -0,0 +1,4 @@ +console.log("foo"); +console.log(typeof require === "function"); +console.log(typeof module === "object"); +console.log(typeof exports === "object"); diff --git a/cli/tests/unit_node/testdata/node_modules/foo/package.json b/cli/tests/unit_node/testdata/node_modules/foo/package.json new file mode 100644 index 0000000000..bde99de928 --- /dev/null +++ b/cli/tests/unit_node/testdata/node_modules/foo/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/cli/tests/unit_node/testdata/process_exit.ts b/cli/tests/unit_node/testdata/process_exit.ts new file mode 100644 index 0000000000..57351c0870 --- /dev/null +++ b/cli/tests/unit_node/testdata/process_exit.ts @@ -0,0 +1,19 @@ +import process from "node:process"; + +//deno-lint-ignore no-undef +process.on("exit", () => { + console.log(1); +}); + +function unexpected() { + console.log(null); +} +//deno-lint-ignore no-undef +process.on("exit", unexpected); +//deno-lint-ignore no-undef +process.removeListener("exit", unexpected); + +//deno-lint-ignore no-undef +process.on("exit", () => { + console.log(2); +}); diff --git a/cli/tests/unit_node/testdata/process_exit2.ts b/cli/tests/unit_node/testdata/process_exit2.ts new file mode 100644 index 0000000000..3731f745ae --- /dev/null +++ b/cli/tests/unit_node/testdata/process_exit2.ts @@ -0,0 +1,4 @@ +import process from "node:process"; + +process.on("exit", () => console.log("exit")); +process.exit(); diff --git a/cli/tests/unit_node/testdata/process_stdin.ts b/cli/tests/unit_node/testdata/process_stdin.ts new file mode 100644 index 0000000000..23562b090a --- /dev/null +++ b/cli/tests/unit_node/testdata/process_stdin.ts @@ -0,0 +1,11 @@ +import process from "node:process"; + +console.log(process.stdin.readableHighWaterMark); + +process.stdin.setEncoding("utf8"); +process.stdin.on("readable", () => { + console.log(process.stdin.read()); +}); +process.stdin.on("end", () => { + console.log("end"); +}); diff --git a/cli/tests/unit_node/testdata/process_stdin_dummy.txt b/cli/tests/unit_node/testdata/process_stdin_dummy.txt new file mode 100644 index 0000000000..a907ec3f43 --- /dev/null +++ b/cli/tests/unit_node/testdata/process_stdin_dummy.txt @@ -0,0 +1,2 @@ +foo +bar \ No newline at end of file diff --git a/core/runtime.rs b/core/runtime.rs index a2b0113629..25a09e85e7 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -841,14 +841,7 @@ impl JsRuntime { ) .await?; let receiver = runtime.mod_evaluate(id); - poll_fn(|cx| { - let r = runtime.poll_event_loop(cx, false); - // TODO(bartlomieju): some code in readable-stream polyfill in `ext/node` - // is calling `nextTick()` during snapshotting, which causes infinite loop - runtime.state.borrow_mut().has_tick_scheduled = false; - r - }) - .await?; + runtime.run_event_loop(false).await?; receiver.await? }) .with_context(|| format!("Couldn't execute '{}'", file_source.specifier)) diff --git a/ext/node/01_node.js b/ext/node/01_node.js index 543a559f18..85346a44ba 100644 --- a/ext/node/01_node.js +++ b/ext/node/01_node.js @@ -110,7 +110,7 @@ function initialize(nodeModules, nodeGlobalThisName) { value: nodeGlobalThis, }); // FIXME(bartlomieju): not nice to depend on `Deno` namespace here - internals.__bootstrapNodeProcess(Deno.args); + internals.__bootstrapNodeProcess(Deno.args, Deno.version); } internals.node = { diff --git a/ext/node/polyfills/_next_tick.ts b/ext/node/polyfills/_next_tick.ts index d5aa88218c..72a6fc1203 100644 --- a/ext/node/polyfills/_next_tick.ts +++ b/ext/node/polyfills/_next_tick.ts @@ -11,6 +11,11 @@ interface Tock { args: Array; } +let nextTickEnabled = false; +export function enableNextTick() { + nextTickEnabled = true; +} + const queue = new FixedQueue(); export function processTicksAndRejections() { @@ -71,8 +76,6 @@ export function runNextTicks() { // return; if (!core.hasTickScheduled()) { core.runMicrotasks(); - } - if (!core.hasTickScheduled()) { return true; } @@ -93,6 +96,12 @@ export function nextTick>( callback: (...args: T) => void, ...args: T ) { + // If we're snapshotting we don't want to push nextTick to be run. We'll + // enable next ticks in "__bootstrapNodeProcess()"; + if (!nextTickEnabled) { + return; + } + validateFunction(callback, "callback"); if (_exiting) { diff --git a/ext/node/polyfills/_process/process.ts b/ext/node/polyfills/_process/process.ts index 48b2e46205..24a4ae1a2b 100644 --- a/ext/node/polyfills/_process/process.ts +++ b/ext/node/polyfills/_process/process.ts @@ -126,5 +126,8 @@ export const versions = { unicode: "14.0", ngtcp2: "0.8.1", nghttp3: "0.7.0", - ...Deno.version, + // Will be filled when calling "__bootstrapNodeProcess()", + deno: "", + v8: "", + typescript: "", }; diff --git a/ext/node/polyfills/_process/streams.mjs b/ext/node/polyfills/_process/streams.mjs index 46213a4ed8..b27f75e2d8 100644 --- a/ext/node/polyfills/_process/streams.mjs +++ b/ext/node/polyfills/_process/streams.mjs @@ -9,13 +9,12 @@ import { moveCursor, } from "internal:deno_node/polyfills/internal/readline/callbacks.mjs"; import { Duplex, Readable, Writable } from "internal:deno_node/polyfills/stream.ts"; -import { stdio } from "internal:deno_node/polyfills/_process/stdio.mjs"; import { isWindows } from "internal:deno_node/polyfills/_util/os.ts"; import { fs as fsConstants } from "internal:deno_node/polyfills/internal_binding/constants.ts"; import * as files from "internal:runtime/js/40_files.js"; // https://github.com/nodejs/node/blob/00738314828074243c9a52a228ab4c68b04259ef/lib/internal/bootstrap/switches/is_main_thread.js#L41 -function createWritableStdioStream(writer, name) { +export function createWritableStdioStream(writer, name) { const stream = new Writable({ write(buf, enc, cb) { if (!writer) { @@ -92,18 +91,6 @@ function createWritableStdioStream(writer, name) { return stream; } -/** https://nodejs.org/api/process.html#process_process_stderr */ -export const stderr = stdio.stderr = createWritableStdioStream( - files.stderr, - "stderr", -); - -/** https://nodejs.org/api/process.html#process_process_stdout */ -export const stdout = stdio.stdout = createWritableStdioStream( - files.stdout, - "stdout", -); - // TODO(PolarETech): This function should be replaced by // `guessHandleType()` in "../internal_binding/util.ts". // https://github.com/nodejs/node/blob/v18.12.1/src/node_util.cc#L257 @@ -162,9 +149,10 @@ const _read = function (size) { /** https://nodejs.org/api/process.html#process_process_stdin */ // https://github.com/nodejs/node/blob/v18.12.1/lib/internal/bootstrap/switches/is_main_thread.js#L189 -export const stdin = stdio.stdin = (() => { +/** Create process.stdin */ +export const initStdin = () => { const fd = files.stdin?.rid; - let _stdin; + let stdin; const stdinType = _guessStdinType(fd); switch (stdinType) { @@ -173,7 +161,7 @@ export const stdin = stdio.stdin = (() => { // use `Readable` instead. // https://github.com/nodejs/node/blob/v18.12.1/lib/internal/bootstrap/switches/is_main_thread.js#L200 // https://github.com/nodejs/node/blob/v18.12.1/lib/internal/fs/streams.js#L148 - _stdin = new Readable({ + stdin = new Readable({ highWaterMark: 64 * 1024, autoDestroy: false, read: _read, @@ -197,7 +185,7 @@ export const stdin = stdio.stdin = (() => { // 2. Creating a net.Socket() from a fd is not currently supported. // https://github.com/nodejs/node/blob/v18.12.1/lib/internal/bootstrap/switches/is_main_thread.js#L206 // https://github.com/nodejs/node/blob/v18.12.1/lib/net.js#L329 - _stdin = new Duplex({ + stdin = new Duplex({ readable: stdinType === "TTY" ? undefined : true, writable: stdinType === "TTY" ? undefined : false, readableHighWaterMark: stdinType === "TTY" ? 0 : undefined, @@ -210,39 +198,41 @@ export const stdin = stdio.stdin = (() => { if (stdinType !== "TTY") { // Make sure the stdin can't be `.end()`-ed - _stdin._writableState.ended = true; + stdin._writableState.ended = true; } break; } default: { // Provide a dummy contentless input for e.g. non-console // Windows applications. - _stdin = new Readable({ read() {} }); - _stdin.push(null); + stdin = new Readable({ read() {} }); + stdin.push(null); } } - return _stdin; -})(); -stdin.on("close", () => files.stdin?.close()); -stdin.fd = files.stdin?.rid ?? -1; -Object.defineProperty(stdin, "isTTY", { - enumerable: true, - configurable: true, - get() { - return Deno.isatty?.(Deno.stdin.rid); - }, -}); -stdin._isRawMode = false; -stdin.setRawMode = (enable) => { - files.stdin?.setRaw?.(enable); - stdin._isRawMode = enable; + stdin.on("close", () => files.stdin?.close()); + stdin.fd = files.stdin?.rid ?? -1; + Object.defineProperty(stdin, "isTTY", { + enumerable: true, + configurable: true, + get() { + return Deno.isatty?.(Deno.stdin.rid); + }, + }); + stdin._isRawMode = false; + stdin.setRawMode = (enable) => { + files.stdin?.setRaw?.(enable); + stdin._isRawMode = enable; + return stdin; + }; + Object.defineProperty(stdin, "isRaw", { + enumerable: true, + configurable: true, + get() { + return stdin._isRawMode; + }, + }); + return stdin; }; -Object.defineProperty(stdin, "isRaw", { - enumerable: true, - configurable: true, - get() { - return stdin._isRawMode; - }, -}); + diff --git a/ext/node/polyfills/_readline.mjs b/ext/node/polyfills/_readline.mjs index 6e0968af0d..0665dbcf31 100644 --- a/ext/node/polyfills/_readline.mjs +++ b/ext/node/polyfills/_readline.mjs @@ -33,7 +33,7 @@ import promises from "internal:deno_node/polyfills/readline/promises.ts"; import { validateAbortSignal } from "internal:deno_node/polyfills/internal/validators.mjs"; import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; import { AbortError } from "internal:deno_node/polyfills/internal/errors.ts"; -import { process } from "internal:deno_node/polyfills/process.ts"; +import process from "internal:deno_node/polyfills/process.ts"; import { Interface as _Interface, diff --git a/ext/node/polyfills/child_process.ts b/ext/node/polyfills/child_process.ts index 06269e0251..cc0e17ffbd 100644 --- a/ext/node/polyfills/child_process.ts +++ b/ext/node/polyfills/child_process.ts @@ -40,7 +40,7 @@ import { promisify, } from "internal:deno_node/polyfills/util.ts"; import { createDeferredPromise } from "internal:deno_node/polyfills/internal/util.mjs"; -import { process } from "internal:deno_node/polyfills/process.ts"; +import process from "internal:deno_node/polyfills/process.ts"; import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; import { convertToValidSignal, diff --git a/ext/node/polyfills/process.ts b/ext/node/polyfills/process.ts index 828b4c6603..42c55ccc52 100644 --- a/ext/node/polyfills/process.ts +++ b/ext/node/polyfills/process.ts @@ -29,15 +29,17 @@ import { import { _exiting } from "internal:deno_node/polyfills/_process/exiting.ts"; export { _nextTick as nextTick, chdir, cwd, env, version, versions }; import { - stderr as stderr_, - stdin as stdin_, - stdout as stdout_, + createWritableStdioStream, + initStdin, } from "internal:deno_node/polyfills/_process/streams.mjs"; +import { stdio } from "internal:deno_node/polyfills/_process/stdio.mjs"; import { + enableNextTick, processTicksAndRejections, runNextTicks, } from "internal:deno_node/polyfills/_next_tick.ts"; import { isWindows } from "internal:deno_node/polyfills/_util/os.ts"; +import * as files from "internal:runtime/js/40_files.js"; // TODO(kt3k): This should be set at start up time export let arch = ""; @@ -50,11 +52,11 @@ export let pid = 0; // TODO(kt3k): Give better types to stdio objects // deno-lint-ignore no-explicit-any -const stderr = stderr_ as any; +let stderr = null as any; // deno-lint-ignore no-explicit-any -const stdin = stdin_ as any; +let stdin = null as any; // deno-lint-ignore no-explicit-any -const stdout = stdout_ as any; +let stdout = null as any; export { stderr, stdin, stdout }; import { getBinding } from "internal:deno_node/polyfills/internal_binding/mod.ts"; @@ -663,13 +665,10 @@ class Process extends EventEmitter { noDeprecation = false; } -// TODO(kt3k): Do the below at start up time. -/* -if (Deno.build.os === "windows") { +if (isWindows) { delete Process.prototype.getgid; delete Process.prototype.getuid; } -*/ /** https://nodejs.org/api/process.html#process_process */ const process = new Process(); @@ -689,13 +688,21 @@ export const removeAllListeners = process.removeAllListeners; // Should be called only once, in `runtime/js/99_main.js` when the runtime is // bootstrapped. -internals.__bootstrapNodeProcess = function (args: string[]) { +internals.__bootstrapNodeProcess = function ( + args: string[], + denoVersions: Record, +) { for (let i = 0; i < args.length; i++) { argv[i + 2] = args[i]; } + for (const [key, value] of Object.entries(denoVersions)) { + versions[key] = value; + } + core.setNextTickCallback(processTicksAndRejections); core.setMacrotaskCallback(runNextTicks); + enableNextTick(); // TODO(bartlomieju): this is buggy, see https://github.com/denoland/deno/issues/16928 // We should use a specialized API in 99_main.js instead @@ -740,12 +747,22 @@ internals.__bootstrapNodeProcess = function (args: string[]) { } }); + // Initializes stdin + stdin = stdio.stdin = process.stdin = initStdin(); + + /** https://nodejs.org/api/process.html#process_process_stderr */ + stderr = stdio.stderr = process.stderr = createWritableStdioStream( + files.stderr, + "stderr", + ); + + /** https://nodejs.org/api/process.html#process_process_stdout */ + stdout = stdio.stdout = process.stdout = createWritableStdioStream( + files.stdout, + "stdout", + ); + delete internals.__bootstrapNodeProcess; }; export default process; - -//TODO(Soremwar) -//Remove on 1.0 -//Kept for backwards compatibility with std -export { process }; diff --git a/ext/node/polyfills/timers.ts b/ext/node/polyfills/timers.ts index 57ff71a382..5a650c1cc2 100644 --- a/ext/node/polyfills/timers.ts +++ b/ext/node/polyfills/timers.ts @@ -7,9 +7,10 @@ import { import { validateFunction } from "internal:deno_node/polyfills/internal/validators.mjs"; import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; export { setUnrefTimeout } from "internal:deno_node/polyfills/internal/timers.mjs"; +import * as timers from "internal:deno_web/02_timers.js"; -const clearTimeout_ = globalThis.clearTimeout; -const clearInterval_ = globalThis.clearInterval; +const clearTimeout_ = timers.clearTimeout; +const clearInterval_ = timers.clearInterval; export function setTimeout( callback: (...args: unknown[]) => void, diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 399d12d385..fa9b0a20d2 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -515,11 +515,6 @@ function bootstrapMainRuntime(runtimeOptions) { ObjectDefineProperty(globalThis, "Deno", util.readOnly(finalDenoNs)); util.log("args", runtimeOptions.args); - - // FIXME(bartlomieju): this should be a helper function that is placed in - // "internals" namespace - // Initialize Node polyfills - // internals.__bootstrapNodeProcess(); } function bootstrapWorkerRuntime( diff --git a/tools/copyright_checker.js b/tools/copyright_checker.js index c8ddcdc919..8adab7a433 100644 --- a/tools/copyright_checker.js +++ b/tools/copyright_checker.js @@ -29,6 +29,7 @@ export async function checkCopyright() { ":!:cli/tsc/compiler.d.ts", ":!:test_util/wpt/**", ":!:cli/tools/init/templates/**", + ":!:cli/tests/unit_node/testdata/**", // rust "*.rs",