Skip to content

Commit a8b3d7b

Browse files
aduh95addaleax
authored andcommitted
worker: allow URL in Worker constructor
The explicit goal is to let users use `import.meta.url` to re-load thecurrent module inside a Worker instance. Fixes: #30780 PR-URL: #31664 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
1 parent 04028aa commit a8b3d7b

8 files changed

+104
-20
lines changed

doc/api/worker_threads.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,10 @@ if (isMainThread) {
513513
<!-- YAML
514514
added: v10.5.0
515515
changes:
516+
- version: REPLACEME
517+
pr-url: https://github.com/nodejs/node/pull/31664
518+
description: The `filename` parameter can be a WHATWG `URL` object using
519+
`file:` protocol.
516520
- version: v13.2.0
517521
pr-url: https://github.com/nodejs/node/pull/26628
518522
description: The `resourceLimits` option was introduced.
@@ -521,9 +525,10 @@ changes:
521525
description: The `argv` option was introduced.
522526
-->
523527

524-
* `filename` {string} The path to the Worker’s main script. Must be
525-
either an absolute path or a relative path (i.e. relative to the
526-
current working directory) starting with `./` or `../`.
528+
* `filename` {string|URL} The path to the Worker’s main script or module. Must
529+
be either an absolute path or a relative path (i.e. relative to the
530+
current working directory) starting with `./` or `../`, or a WHATWG `URL`
531+
object using `file:` protocol.
527532
If `options.eval` is `true`, this is a string containing JavaScript code
528533
rather than a path.
529534
* `options` {Object}
@@ -536,8 +541,9 @@ changes:
536541
to specify that the parent thread and the child thread should share their
537542
environment variables; in that case, changes to one thread’s `process.env`
538543
object will affect the other thread as well. **Default:** `process.env`.
539-
* `eval` {boolean} If `true`, interpret the first argument to the constructor
540-
as a script that is executed once the worker is online.
544+
* `eval` {boolean} If `true` and the first argument is a `string`, interpret
545+
the first argument to the constructor as a script that is executed once the
546+
worker is online.
541547
* `execArgv` {string[]} List of node CLI options passed to the worker.
542548
V8 options (such as `--max-old-space-size`) and options that affect the
543549
process (such as `--title`) are not supported. If set, this will be provided

lib/internal/errors.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -1394,9 +1394,14 @@ E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
13941394
E('ERR_WORKER_NOT_RUNNING', 'Worker instance not running', Error);
13951395
E('ERR_WORKER_OUT_OF_MEMORY',
13961396
'Worker terminated due to reaching memory limit: %s', Error);
1397-
E('ERR_WORKER_PATH',
1398-
'The worker script filename must be an absolute path or a relative ' +
1399-
'path starting with \'./\' or \'../\'. Received "%s"',
1397+
E('ERR_WORKER_PATH', (filename) =>
1398+
'The worker script or module filename must be an absolute path or a ' +
1399+
'relative path starting with \'./\' or \'../\'.' +
1400+
(filename.startsWith('file://') ?
1401+
' If you want to pass a file:// URL, you must wrap it around `new URL`.' :
1402+
''
1403+
) +
1404+
` Received "${filename}"`,
14001405
TypeError);
14011406
E('ERR_WORKER_UNSERIALIZABLE_ERROR',
14021407
'Serializing an uncaught exception failed', Error);

lib/internal/url.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -1348,8 +1348,7 @@ function getPathFromURLPosix(url) {
13481348
function fileURLToPath(path) {
13491349
if (typeof path === 'string')
13501350
path = new URL(path);
1351-
else if (path == null || !path[searchParams] ||
1352-
!path[searchParams][searchParams])
1351+
else if (!isURLInstance(path))
13531352
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
13541353
if (path.protocol !== 'file:')
13551354
throw new ERR_INVALID_URL_SCHEME('file');
@@ -1396,9 +1395,13 @@ function pathToFileURL(filepath) {
13961395
return outURL;
13971396
}
13981397

1398+
function isURLInstance(fileURLOrPath) {
1399+
return fileURLOrPath != null && fileURLOrPath[searchParams] &&
1400+
fileURLOrPath[searchParams][searchParams];
1401+
}
1402+
13991403
function toPathIfFileURL(fileURLOrPath) {
1400-
if (fileURLOrPath == null || !fileURLOrPath[searchParams] ||
1401-
!fileURLOrPath[searchParams][searchParams])
1404+
if (!isURLInstance(fileURLOrPath))
14021405
return fileURLOrPath;
14031406
return fileURLToPath(fileURLOrPath);
14041407
}
@@ -1431,6 +1434,7 @@ module.exports = {
14311434
fileURLToPath,
14321435
pathToFileURL,
14331436
toPathIfFileURL,
1437+
isURLInstance,
14341438
URL,
14351439
URLSearchParams,
14361440
domainToASCII,

lib/internal/worker.js

+27-7
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ const {
2727
ERR_INVALID_ARG_TYPE,
2828
// eslint-disable-next-line no-unused-vars
2929
ERR_WORKER_INIT_FAILED,
30+
ERR_INVALID_ARG_VALUE,
3031
} = errorCodes;
31-
const { validateString } = require('internal/validators');
3232
const { getOptionValue } = require('internal/options');
3333

3434
const workerIo = require('internal/worker/io');
@@ -45,7 +45,7 @@ const {
4545
WritableWorkerStdio
4646
} = workerIo;
4747
const { deserializeError } = require('internal/error-serdes');
48-
const { pathToFileURL } = require('url');
48+
const { fileURLToPath, isURLInstance, pathToFileURL } = require('internal/url');
4949

5050
const {
5151
ownsProcessState,
@@ -86,7 +86,6 @@ class Worker extends EventEmitter {
8686
constructor(filename, options = {}) {
8787
super();
8888
debug(`[${threadId}] create new worker`, filename, options);
89-
validateString(filename, 'filename');
9089
if (options.execArgv && !ArrayIsArray(options.execArgv)) {
9190
throw new ERR_INVALID_ARG_TYPE('options.execArgv',
9291
'Array',
@@ -99,11 +98,33 @@ class Worker extends EventEmitter {
9998
}
10099
argv = options.argv.map(String);
101100
}
102-
if (!options.eval) {
103-
if (!path.isAbsolute(filename) && !/^\.\.?[\\/]/.test(filename)) {
101+
102+
let url;
103+
if (options.eval) {
104+
if (typeof filename !== 'string') {
105+
throw new ERR_INVALID_ARG_VALUE(
106+
'options.eval',
107+
options.eval,
108+
'must be false when \'filename\' is not a string'
109+
);
110+
}
111+
url = null;
112+
} else {
113+
if (isURLInstance(filename)) {
114+
url = filename;
115+
filename = fileURLToPath(filename);
116+
} else if (typeof filename !== 'string') {
117+
throw new ERR_INVALID_ARG_TYPE(
118+
'filename',
119+
['string', 'URL'],
120+
filename
121+
);
122+
} else if (path.isAbsolute(filename) || /^\.\.?[\\/]/.test(filename)) {
123+
filename = path.resolve(filename);
124+
url = pathToFileURL(filename);
125+
} else {
104126
throw new ERR_WORKER_PATH(filename);
105127
}
106-
filename = path.resolve(filename);
107128

108129
const ext = path.extname(filename);
109130
if (ext !== '.js' && ext !== '.mjs' && ext !== '.cjs') {
@@ -125,7 +146,6 @@ class Worker extends EventEmitter {
125146
options.env);
126147
}
127148

128-
const url = options.eval ? null : pathToFileURL(filename);
129149
// Set up the C++ handle for the worker, as well as some internal wiring.
130150
this[kHandle] = new WorkerImpl(url,
131151
env === process.env ? null : env,

test/parallel/test-worker-type-check.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const { Worker } = require('worker_threads');
2020
{
2121
code: 'ERR_INVALID_ARG_TYPE',
2222
name: 'TypeError',
23-
message: 'The "filename" argument must be of type string.' +
23+
message: 'The "filename" argument must be of type string ' +
24+
'or an instance of URL.' +
2425
common.invalidArgTypeHelper(val)
2526
}
2627
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import '../common/index.mjs';
2+
import assert from 'assert';
3+
import { Worker } from 'worker_threads';
4+
5+
const re = /The argument 'options\.eval' must be false when 'filename' is not a string\./;
6+
assert.throws(() => new Worker(new URL(import.meta.url), { eval: true }), re);

test/parallel/test-worker-unsupported-path.js

+24
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { Worker } = require('worker_threads');
1313
assert.throws(() => { new Worker('/b'); }, expectedErr);
1414
assert.throws(() => { new Worker('/c.wasm'); }, expectedErr);
1515
assert.throws(() => { new Worker('/d.txt'); }, expectedErr);
16+
assert.throws(() => { new Worker(new URL('file:///C:/e.wasm')); }, expectedErr);
1617
}
1718

1819
{
@@ -26,3 +27,26 @@ const { Worker } = require('worker_threads');
2627
assert.throws(() => { new Worker('file:///file_url'); }, expectedErr);
2728
assert.throws(() => { new Worker('https://www.url.com'); }, expectedErr);
2829
}
30+
31+
{
32+
assert.throws(
33+
() => { new Worker('file:///file_url'); },
34+
/If you want to pass a file:\/\/ URL, you must wrap it around `new URL`/
35+
);
36+
assert.throws(
37+
() => { new Worker('relative_no_dot'); },
38+
// eslint-disable-next-line node-core/no-unescaped-regexp-dot
39+
/^((?!If you want to pass a file:\/\/ URL, you must wrap it around `new URL`).)*$/s
40+
);
41+
}
42+
43+
{
44+
const expectedErr = {
45+
code: 'ERR_INVALID_URL_SCHEME',
46+
name: 'TypeError'
47+
};
48+
assert.throws(() => { new Worker(new URL('https://www.url.com')); },
49+
expectedErr);
50+
assert.throws(() => { new Worker(new URL('data:application/javascript,')); },
51+
expectedErr);
52+
}

test/parallel/test-worker.mjs

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { mustCall } from '../common/index.mjs';
2+
import assert from 'assert';
3+
import { Worker, isMainThread, parentPort } from 'worker_threads';
4+
5+
const TEST_STRING = 'Hello, world!';
6+
7+
if (isMainThread) {
8+
const w = new Worker(new URL(import.meta.url));
9+
w.on('message', mustCall((message) => {
10+
assert.strictEqual(message, TEST_STRING);
11+
}));
12+
} else {
13+
setImmediate(() => {
14+
process.nextTick(() => {
15+
parentPort.postMessage(TEST_STRING);
16+
});
17+
});
18+
}

0 commit comments

Comments
 (0)