diff --git a/Cargo.lock b/Cargo.lock index afb2a6a272..86dce5088b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5132,9 +5132,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "relative-path" -version = "1.9.0" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" [[package]] name = "reqwest" diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f4541f8866..16e69250b7 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -329,6 +329,7 @@ deno_core::extension!(deno_node, ops::require::op_require_package_imports_resolve

, ops::require::op_require_break_on_next_statement, ops::util::op_node_guess_handle_type, + ops::worker_threads::op_worker_threads_filename

, ops::crypto::op_node_create_private_key, ops::crypto::op_node_create_public_key, ops::ipc::op_node_child_ipc_pipe, diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index ae798d1819..8aed274bc2 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -11,4 +11,5 @@ pub mod require; pub mod util; pub mod v8; pub mod winerror; +pub mod worker_threads; pub mod zlib; diff --git a/ext/node/ops/require.rs b/ext/node/ops/require.rs index 6ca0b4a1a1..d14e1d732a 100644 --- a/ext/node/ops/require.rs +++ b/ext/node/ops/require.rs @@ -195,7 +195,9 @@ pub fn op_require_resolve_deno_dir( resolver .resolve_package_folder_from_package( &request, - &ModuleSpecifier::from_file_path(parent_filename).unwrap(), + &ModuleSpecifier::from_file_path(&parent_filename).unwrap_or_else(|_| { + panic!("Url::from_file_path: [{:?}]", parent_filename) + }), NodeResolutionMode::Execution, ) .ok() diff --git a/ext/node/ops/worker_threads.rs b/ext/node/ops/worker_threads.rs new file mode 100644 index 0000000000..18a4157d42 --- /dev/null +++ b/ext/node/ops/worker_threads.rs @@ -0,0 +1,87 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::url::Url; +use deno_core::OpState; +use deno_fs::FileSystemRc; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; + +use crate::resolution; +use crate::NodePermissions; +use crate::NodeResolver; +use crate::NpmResolverRc; + +fn ensure_read_permission

( + state: &mut OpState, + file_path: &Path, +) -> Result<(), AnyError> +where + P: NodePermissions + 'static, +{ + let resolver = state.borrow::(); + let permissions = state.borrow::

(); + resolver.ensure_read_permission(permissions, file_path) +} + +#[op2] +#[string] +pub fn op_worker_threads_filename

( + state: &mut OpState, + #[string] specifier: String, +) -> Result +where + P: NodePermissions + 'static, +{ + if specifier.starts_with("data:") { + return Ok(specifier); + } + let url: Url = if specifier.starts_with("file:") { + Url::parse(&specifier)? + } else { + let path = PathBuf::from(&specifier); + if path.is_relative() && !specifier.starts_with('.') { + return Err(generic_error( + "Relative path entries must start with '.' or '..'", + )); + } + ensure_read_permission::

(state, &path)?; + let fs = state.borrow::(); + let canonicalized_path = + deno_core::strip_unc_prefix(fs.realpath_sync(&path)?); + Url::from_file_path(canonicalized_path) + .map_err(|e| generic_error(format!("URL from Path-String: {:#?}", e)))? + }; + let url_path = url + .to_file_path() + .map_err(|e| generic_error(format!("URL to Path-String: {:#?}", e)))?; + ensure_read_permission::

(state, &url_path)?; + let fs = state.borrow::(); + if !fs.exists_sync(&url_path) { + return Err(generic_error(format!("File not found [{:?}]", url_path))); + } + let node_resolver = state.borrow::>(); + match node_resolver.url_to_node_resolution(url)? { + resolution::NodeResolution::Esm(u) => Ok(u.to_string()), + resolution::NodeResolution::CommonJs(u) => wrap_cjs(u), + _ => Err(generic_error("Neither ESM nor CJS")), + } +} + +/// +/// Wrap a CJS file-URL and the required setup in a stringified `data:`-URL +/// +fn wrap_cjs(url: Url) -> Result { + let path = url + .to_file_path() + .map_err(|e| generic_error(format!("URL to Path: {:#?}", e)))?; + let filename = path.file_name().unwrap().to_string_lossy(); + Ok(format!( + "data:text/javascript,import {{ createRequire }} from \"node:module\";\ + const require = createRequire(\"{}\"); require(\"./{}\");", + url, filename, + )) +} diff --git a/ext/node/polyfills/worker_threads.ts b/ext/node/polyfills/worker_threads.ts index 49f2f3e3e8..ab3834132e 100644 --- a/ext/node/polyfills/worker_threads.ts +++ b/ext/node/polyfills/worker_threads.ts @@ -9,7 +9,7 @@ import { op_host_recv_message, op_host_terminate_worker, op_message_port_recv_message_sync, - op_require_read_closest_package_json, + op_worker_threads_filename, } from "ext:core/ops"; import { deserializeJsMessageData, @@ -24,7 +24,6 @@ import { log } from "ext:runtime/06_util.js"; import { notImplemented } from "ext:deno_node/_utils.ts"; import { EventEmitter } from "node:events"; import { BroadcastChannel } from "ext:deno_broadcast_channel/01_broadcast_channel.js"; -import { isAbsolute, resolve } from "node:path"; const { ObjectPrototypeIsPrototypeOf } = primordials; const { @@ -32,14 +31,8 @@ const { Symbol, SymbolFor, SymbolIterator, - StringPrototypeEndsWith, - StringPrototypeReplace, - StringPrototypeMatch, - StringPrototypeReplaceAll, - StringPrototypeToString, StringPrototypeTrim, SafeWeakMap, - SafeRegExp, SafeMap, TypeError, } = primordials; @@ -66,74 +59,6 @@ export interface WorkerOptions { name?: string; } -const WHITESPACE_ENCODINGS: Record = { - "\u0009": "%09", - "\u000A": "%0A", - "\u000B": "%0B", - "\u000C": "%0C", - "\u000D": "%0D", - "\u0020": "%20", -}; - -function encodeWhitespace(string: string): string { - return StringPrototypeReplaceAll(string, new SafeRegExp(/[\s]/g), (c) => { - return WHITESPACE_ENCODINGS[c] ?? c; - }); -} - -function toFileUrlPosix(path: string): URL { - if (!isAbsolute(path)) { - throw new TypeError("Must be an absolute path."); - } - const url = new URL("file:///"); - url.pathname = encodeWhitespace( - StringPrototypeReplace( - StringPrototypeReplace(path, new SafeRegExp(/%/g), "%25"), - new SafeRegExp(/\\/g), - "%5C", - ), - ); - return url; -} - -function toFileUrlWin32(path: string): URL { - if (!isAbsolute(path)) { - throw new TypeError("Must be an absolute path."); - } - const { 0: _, 1: hostname, 2: pathname } = StringPrototypeMatch( - path, - new SafeRegExp(/^(?:[/\\]{2}([^/\\]+)(?=[/\\](?:[^/\\]|$)))?(.*)/), - ); - const url = new URL("file:///"); - url.pathname = encodeWhitespace( - StringPrototypeReplace(pathname, new SafeRegExp(/%/g), "%25"), - ); - if (hostname != null && hostname != "localhost") { - url.hostname = hostname; - if (!url.hostname) { - throw new TypeError("Invalid hostname."); - } - } - return url; -} - -/** - * Converts a path string to a file URL. - * - * ```ts - * toFileUrl("/home/foo"); // new URL("file:///home/foo") - * toFileUrl("\\home\\foo"); // new URL("file:///home/foo") - * toFileUrl("C:\\Users\\foo"); // new URL("file:///C:/Users/foo") - * toFileUrl("\\\\127.0.0.1\\home\\foo"); // new URL("file://127.0.0.1/home/foo") - * ``` - * @param path to convert to file URL - */ -function toFileUrl(path: string): URL { - return core.build.os == "windows" - ? toFileUrlWin32(path) - : toFileUrlPosix(path); -} - const privateWorkerRef = Symbol("privateWorkerRef"); class NodeWorker extends EventEmitter { #id = 0; @@ -162,29 +87,23 @@ class NodeWorker extends EventEmitter { constructor(specifier: URL | string, options?: WorkerOptions) { super(); - if (options?.eval === true) { + + if ( + typeof specifier === "object" && + !(specifier.protocol === "data:" || specifier.protocol === "file:") + ) { + throw new TypeError( + "node:worker_threads support only 'file:' and 'data:' URLs", + ); + } + if (options?.eval) { specifier = `data:text/javascript,${specifier}`; - } else if (typeof specifier === "string") { - specifier = resolve(specifier); - let pkg; - try { - pkg = op_require_read_closest_package_json(specifier); - } catch (_) { - // empty catch block when package json might not be present - } - if ( - !(StringPrototypeEndsWith( - StringPrototypeToString(specifier), - ".mjs", - )) || - (pkg && pkg.exists && pkg.typ == "module") - ) { - const cwdFileUrl = toFileUrl(Deno.cwd()); - specifier = - `data:text/javascript,(async function() {const { createRequire } = await import("node:module");const require = createRequire("${cwdFileUrl}");require("${specifier}");})();`; - } else { - specifier = toFileUrl(specifier as string); - } + } else if ( + !(typeof specifier === "object" && specifier.protocol === "data:") + ) { + // deno-lint-ignore prefer-primordials + specifier = specifier.toString(); + specifier = op_worker_threads_filename(specifier); } // TODO(bartlomieu): this doesn't match the Node.js behavior, it should be diff --git a/tests/unit_node/testdata/worker_module/cjs-file.cjs b/tests/unit_node/testdata/worker_module/cjs-file.cjs new file mode 100644 index 0000000000..af2e21c353 --- /dev/null +++ b/tests/unit_node/testdata/worker_module/cjs-file.cjs @@ -0,0 +1,23 @@ +const { add } = require("./other_cjs_file.cjs"); + +const missing_toplevel_async = async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve; + }, 500); + }); +}; + +async function main() { + /// async code doesn't seem to work within this CJS wrapper :( + //const p = await missing_toplevel_async(); + + const sum = add(2, 3); + if (sum != 5) { + throw ("Bad calculator!"); + } + + postMessage("hallo"); +} + +main(); diff --git a/tests/unit_node/testdata/worker_module/index.js b/tests/unit_node/testdata/worker_module/index.js index a3e976b65a..cbb433986a 100644 --- a/tests/unit_node/testdata/worker_module/index.js +++ b/tests/unit_node/testdata/worker_module/index.js @@ -1,3 +1,3 @@ import { myFunction } from "./other_file.js"; -myFunction().then(() => {}); +await myFunction(); diff --git a/tests/unit_node/testdata/worker_module/nested/index.js b/tests/unit_node/testdata/worker_module/nested/index.js new file mode 100644 index 0000000000..b77376a16d --- /dev/null +++ b/tests/unit_node/testdata/worker_module/nested/index.js @@ -0,0 +1,3 @@ +import { myFunction } from "../other_file.js"; + +await myFunction(); diff --git a/tests/unit_node/testdata/worker_module/other_cjs_file.cjs b/tests/unit_node/testdata/worker_module/other_cjs_file.cjs new file mode 100644 index 0000000000..45ae337bf5 --- /dev/null +++ b/tests/unit_node/testdata/worker_module/other_cjs_file.cjs @@ -0,0 +1,5 @@ +module.exports = { + add: (a, b) => { + return a + b; + }, +}; diff --git a/tests/unit_node/testdata/worker_module/other_file.js b/tests/unit_node/testdata/worker_module/other_file.js index 41789dfe8f..6bb9b6b83f 100644 --- a/tests/unit_node/testdata/worker_module/other_file.js +++ b/tests/unit_node/testdata/worker_module/other_file.js @@ -1,3 +1,8 @@ export async function myFunction() { - await new Promise((resolve) => setTimeout(resolve, 100)); + await new Promise((resolve) => + setTimeout(() => { + postMessage("hallo"); + resolve; + }, 100) + ); } diff --git a/tests/unit_node/testdata/worker_module/βάρβαροι.js b/tests/unit_node/testdata/worker_module/βάρβαροι.js new file mode 100644 index 0000000000..cee92889c6 --- /dev/null +++ b/tests/unit_node/testdata/worker_module/βάρβαροι.js @@ -0,0 +1,9 @@ +export async function myFunction() { + await new Promise((resolve) => + setTimeout(() => { + postMessage("hallo"); + resolve; + }, 100) + ); +} +await myFunction(); diff --git a/tests/unit_node/worker_threads_test.ts b/tests/unit_node/worker_threads_test.ts index f2ce00c847..66460940df 100644 --- a/tests/unit_node/worker_threads_test.ts +++ b/tests/unit_node/worker_threads_test.ts @@ -4,9 +4,10 @@ import { assert, assertEquals, assertObjectMatch, + assertThrows, fail, } from "@std/assert/mod.ts"; -import { fromFileUrl, relative } from "@std/path/mod.ts"; +import { fromFileUrl, relative, sep } from "@std/path/mod.ts"; import * as workerThreads from "node:worker_threads"; import { EventEmitter, once } from "node:events"; @@ -20,42 +21,42 @@ Deno.test("[node/worker_threads] MessageChannel are MessagePort are exported", ( }); Deno.test({ - name: "[worker_threads] isMainThread", + name: "[node/worker_threads] isMainThread", fn() { assertEquals(workerThreads.isMainThread, true); }, }); Deno.test({ - name: "[worker_threads] threadId", + name: "[node/worker_threads] threadId", fn() { assertEquals(workerThreads.threadId, 0); }, }); Deno.test({ - name: "[worker_threads] resourceLimits", + name: "[node/worker_threads] resourceLimits", fn() { assertObjectMatch(workerThreads.resourceLimits, {}); }, }); Deno.test({ - name: "[worker_threads] parentPort", + name: "[node/worker_threads] parentPort", fn() { assertEquals(workerThreads.parentPort, null); }, }); Deno.test({ - name: "[worker_threads] workerData", + name: "[node/worker_threads] workerData", fn() { assertEquals(workerThreads.workerData, null); }, }); Deno.test({ - name: "[worker_threads] setEnvironmentData / getEnvironmentData", + name: "[node/worker_threads] setEnvironmentData / getEnvironmentData", fn() { workerThreads.setEnvironmentData("test", "test"); assertEquals(workerThreads.getEnvironmentData("test"), "test"); @@ -63,7 +64,7 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] Worker threadId", + name: "[node/worker_threads] Worker threadId", async fn() { const worker = new workerThreads.Worker( new URL("./testdata/worker_threads.mjs", import.meta.url), @@ -85,7 +86,7 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] Worker basics", + name: "[node/worker_threads] Worker basics", async fn() { workerThreads.setEnvironmentData("test", "test"); workerThreads.setEnvironmentData(1, { @@ -118,7 +119,7 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] Worker eval", + name: "[node/worker_threads] Worker eval", async fn() { const worker = new workerThreads.Worker( ` @@ -135,17 +136,141 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] worker thread with type module", - fn() { - const worker = new workerThreads.Worker( - new URL("./testdata/worker_module/index.js", import.meta.url), - ); - worker.terminate(); + name: "[node/worker_threads] worker thread with type module", + async fn() { + function p() { + return new Promise((resolve, reject) => { + const worker = new workerThreads.Worker( + new URL("./testdata/worker_module/index.js", import.meta.url), + ); + worker.on("error", (e) => reject(e.message)); + worker.on("message", () => resolve(worker)); + }); + } + await p(); }, }); Deno.test({ - name: "[worker_threads] inheritances", + name: "[node/worker_threads] worker thread in nested module", + async fn() { + function p() { + return new Promise((resolve, reject) => { + const worker = new workerThreads.Worker( + new URL("./testdata/worker_module/nested/index.js", import.meta.url), + ); + worker.on("error", (e) => reject(e.message)); + worker.on("message", () => resolve(worker)); + }); + } + await p(); + }, +}); + +Deno.test({ + name: "[node/worker_threads] .cjs worker file within module", + async fn() { + function p() { + return new Promise((resolve, reject) => { + const worker = new workerThreads.Worker( + new URL("./testdata/worker_module/cjs-file.cjs", import.meta.url), + ); + worker.on("error", (e) => reject(e.message)); + worker.on("message", () => resolve(worker)); + }); + } + await p(); + }, +}); + +Deno.test({ + name: "[node/worker_threads] relativ path string", + async fn() { + function p() { + return new Promise((resolve, reject) => { + const worker = new workerThreads.Worker( + "./tests/unit_node/testdata/worker_module/index.js", + ); + worker.on("error", (e) => reject(e.message)); + worker.on("message", () => resolve(worker)); + }); + } + await p(); + }, +}); + +Deno.test({ + name: "[node/worker_threads] utf-8 path string", + async fn() { + function p() { + return new Promise((resolve, reject) => { + const worker = new workerThreads.Worker( + "./tests/unit_node/testdata/worker_module/βάρβαροι.js", + ); + worker.on("error", (e) => reject(e.message)); + worker.on("message", () => resolve(worker)); + }); + } + await p(); + }, +}); + +Deno.test({ + name: "[node/worker_threads] utf-8 path URL", + async fn() { + function p() { + return new Promise((resolve, reject) => { + const worker = new workerThreads.Worker( + new URL( + "./testdata/worker_module/βάρβαροι.js", + import.meta.url, + ), + ); + worker.on("error", (e) => reject(e.message)); + worker.on("message", () => resolve(worker)); + }); + } + await p(); + }, +}); + +Deno.test({ + name: "[node/worker_threads] throws on relativ path without leading dot", + fn() { + assertThrows( + () => { + new workerThreads.Worker( + "tests/unit_node/testdata/worker_module/index.js", + ); + }, + ); + }, +}); + +Deno.test({ + name: "[node/worker_threads] throws on unsupported URL protcol", + fn() { + assertThrows( + () => { + new workerThreads.Worker(new URL("https://example.com")); + }, + ); + }, +}); + +Deno.test({ + name: "[node/worker_threads] throws on non-existend file", + fn() { + assertThrows( + () => { + new workerThreads.Worker(new URL("file://very/unlikely")); + }, + ); + }, +}); + +Deno.test({ + name: "[node/worker_threads] inheritances", async fn() { const worker = new workerThreads.Worker( ` @@ -168,7 +293,7 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] Worker workerData", + name: "[node/worker_threads] Worker workerData", async fn() { const worker = new workerThreads.Worker( new URL("./testdata/worker_threads.mjs", import.meta.url), @@ -192,12 +317,14 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] Worker with relative path", + name: "[node/worker_threads] Worker with relative path", async fn() { - const worker = new workerThreads.Worker(relative( - Deno.cwd(), - fromFileUrl(new URL("./testdata/worker_threads.mjs", import.meta.url)), - )); + const worker = new workerThreads.Worker( + `.${sep}` + relative( + Deno.cwd(), + fromFileUrl(new URL("./testdata/worker_threads.mjs", import.meta.url)), + ), + ); worker.postMessage("Hello, how are you my thread?"); assertEquals((await once(worker, "message"))[0], "I'm fine!"); worker.terminate(); @@ -205,7 +332,7 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] unref", + name: "[node/worker_threads] unref", async fn() { const timeout = setTimeout(() => fail("Test timed out"), 60_000); const child = new Deno.Command(Deno.execPath(), { @@ -220,7 +347,7 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] SharedArrayBuffer", + name: "[node/worker_threads] SharedArrayBuffer", async fn() { const sab = new SharedArrayBuffer(Uint8Array.BYTES_PER_ELEMENT); const uint = new Uint8Array(sab); @@ -240,7 +367,7 @@ Deno.test({ }); Deno.test({ - name: "[worker_threads] Worker workerData with MessagePort", + name: "[node/worker_threads] Worker workerData with MessagePort", async fn() { const { port1: mainPort, port2: workerPort } = new workerThreads .MessageChannel(); @@ -256,7 +383,7 @@ Deno.test({ workerData, } from "node:worker_threads"; parentPort.on("message", (msg) => { - console.log("message from main", msg); + /* console.log("message from main", msg); */ parentPort.postMessage("Hello from worker on parentPort!"); workerData.workerPort.postMessage("Hello from worker on workerPort!"); });