From 2e5f7809d1d56fe12d004e576434544e4d934a39 Mon Sep 17 00:00:00 2001 From: Antoine du HAMEL Date: Fri, 29 May 2020 17:59:27 +0200 Subject: [PATCH] worker_threads: add support for data URLs --- doc/api/worker_threads.md | 13 +++-- lib/internal/worker.js | 18 ++++++- test/parallel/test-worker-eval.js | 50 ++++++++++++++----- test/parallel/test-worker-unsupported-path.js | 2 - 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 94711cf81c4005..2737f4ccd45673 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -588,7 +588,8 @@ changes: * `filename` {string|URL} The path to the Worker’s main script or module. Must be either an absolute path or a relative path (i.e. relative to the current working directory) starting with `./` or `../`, or a WHATWG `URL` - object using `file:` protocol. + object using `file:` or `data:` protocol. + When using a [`data:` URL][], you must set the `type` option to `"module"`. If `options.eval` is `true`, this is a string containing JavaScript code rather than a path. * `options` {Object} @@ -601,12 +602,13 @@ changes: to specify that the parent thread and the child thread should share their environment variables; in that case, changes to one thread’s `process.env` object will affect the other thread as well. **Default:** `process.env`. - * `eval` {boolean} If `true`, interpret the first argument to the constructor + * `eval` {boolean} If `true`, interprets the first argument to the constructor as a script (or a module if `type` is set to `module`) that is executed once the worker is online. - * `type` {string} If `"module"` and `eval` is set to `true`, interpret the - first argument as an ECMAScript 2015 module instead of a script. The default - value is `"classic"`. Doesn't have any effect when `eval` is set to `false`. + * `type` {string} If `"module"`, interprets the first argument as an + ECMAScript 2015 module instead of a script. The default value is + `"classic"`. Doesn't have any effect when `filename` refers to a file, in + which case the type is inferred from the file extension. * `execArgv` {string[]} List of node CLI options passed to the worker. V8 options (such as `--max-old-space-size`) and options that affect the process (such as `--title`) are not supported. If set, this will be provided @@ -869,3 +871,4 @@ active handle in the event system. If the worker is already `unref()`ed calling [child processes]: child_process.html [contextified]: vm.html#vm_what_does_it_mean_to_contextify_an_object [v8.serdes]: v8.html#v8_serialization_api +[`data:` URL]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs diff --git a/lib/internal/worker.js b/lib/internal/worker.js index f5d0a1c6450d6c..a3ea93c48a5340 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -4,6 +4,7 @@ const { ArrayIsArray, + JSONStringify, MathMax, ObjectCreate, ObjectEntries, @@ -99,7 +100,7 @@ class Worker extends EventEmitter { argv = options.argv.map(String); } - let url; + let url, doEval; if (options.eval) { if (typeof filename !== 'string') { throw new ERR_INVALID_ARG_VALUE( @@ -109,7 +110,20 @@ class Worker extends EventEmitter { ); } url = null; + doEval = true; + } else if (filename?.protocol === 'data:') { + if (options.type !== 'module') { + throw new ERR_INVALID_ARG_VALUE( + 'options.type', + options.type, + 'must be set to "module" when filename is a data: URL' + ); + } + url = null; + doEval = true; + filename = `import ${JSONStringify(filename)}`; } else { + doEval = false; if (isURLInstance(filename)) { url = filename; filename = fileURLToPath(filename); @@ -207,7 +221,7 @@ class Worker extends EventEmitter { argv, type: messageTypes.LOAD_SCRIPT, filename, - doEval: !!options.eval, + doEval, workerType: options.type, cwdCounter: cwdCounter || workerIo.sharedCwdCounter, workerData: options.workerData, diff --git a/test/parallel/test-worker-eval.js b/test/parallel/test-worker-eval.js index 08a778fdc4b250..d2e11c95bf0129 100644 --- a/test/parallel/test-worker-eval.js +++ b/test/parallel/test-worker-eval.js @@ -10,6 +10,8 @@ const cjsLoadingESMError = /Unexpected token 'export'/; const esmLoadingCJSError = /module is not defined/; const invalidValueError = /The argument 'options\.type' must be either "module" or "classic"/; +const invalidValueError2 = + /The argument 'options\.type' must be set to "module" when filename is a data: URL/; function runInWorker(evalString, options) { return new Promise((resolve, reject) => { @@ -19,20 +21,42 @@ function runInWorker(evalString, options) { }); } -let options; +{ + const options = { eval: true }; + assert.rejects(runInWorker(moduleString, options), cjsLoadingESMError); + runInWorker(cjsString, options); // does not reject +} -options = { eval: true }; -assert.rejects(runInWorker(moduleString, options), cjsLoadingESMError); -runInWorker(cjsString, options); // does not reject +{ + const options = { eval: true, type: 'classic' }; + assert.rejects(runInWorker(moduleString, options), cjsLoadingESMError); + runInWorker(cjsString, options); // does not reject +} -options = { eval: true, type: 'classic' }; -assert.rejects(runInWorker(moduleString, options), cjsLoadingESMError); -runInWorker(cjsString, options); // does not reject +{ + const options = { eval: true, type: 'module' }; + runInWorker(moduleString, options); // does not reject + assert.rejects(runInWorker(cjsString, options), esmLoadingCJSError); +} -options = { eval: true, type: 'module' }; -runInWorker(moduleString, options); // does not reject -assert.rejects(runInWorker(cjsString, options), esmLoadingCJSError); +{ + const options = { eval: false, type: 'module' }; + runInWorker(new URL(`data:text/javascript,${moduleString}`), + options); // does not reject + assert.rejects(runInWorker(new URL(`data:text/javascript,${cjsString}`), + options), esmLoadingCJSError); +} -options = { eval: true, type: 'wasm' }; -assert.rejects(runInWorker(moduleString, options), invalidValueError); -assert.rejects(runInWorker(cjsString, options), invalidValueError); +{ + const options = { eval: true, type: 'wasm' }; + assert.rejects(runInWorker(moduleString, options), invalidValueError); + assert.rejects(runInWorker(cjsString, options), invalidValueError); +} + +{ + const options = { type: 'classic' }; + assert.rejects(runInWorker(new URL(`data:text/javascript,${moduleString}`), + options), invalidValueError2); + assert.rejects(runInWorker(new URL(`data:text/javascript,${cjsString}`), + options), invalidValueError2); +} diff --git a/test/parallel/test-worker-unsupported-path.js b/test/parallel/test-worker-unsupported-path.js index 1a5f53c821ee6c..17d282e4bc0379 100644 --- a/test/parallel/test-worker-unsupported-path.js +++ b/test/parallel/test-worker-unsupported-path.js @@ -47,6 +47,4 @@ const { Worker } = require('worker_threads'); }; assert.throws(() => { new Worker(new URL('https://www.url.com')); }, expectedErr); - assert.throws(() => { new Worker(new URL('data:application/javascript,')); }, - expectedErr); }