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!");
});