From 3fcc7a200fc728e00e363318a462a3804f906059 Mon Sep 17 00:00:00 2001 From: Khoi Pham Date: Mon, 28 Sep 2020 17:07:45 +0700 Subject: [PATCH] feat: add option `crossOrigin` handle cross origin worker drawing inspiration from https://benohead.com/blog/2017/12/06/cross-domain-cross-browser-web-workers/ --- README.md | 53 +++++++--- src/index.js | 2 + src/options.json | 3 + src/runtime/crossOrigin.js | 34 ++++++ src/utils.js | 22 ++++ .../crossOrigin-option.test.js.snap | 32 ++++++ .../validate-options.test.js.snap | 16 +-- test/crossOrigin-option.test.js | 100 ++++++++++++++++++ test/helpers/getResultFromBrowser.js | 4 +- 9 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 src/runtime/crossOrigin.js create mode 100644 test/__snapshots__/crossOrigin-option.test.js.snap create mode 100644 test/crossOrigin-option.test.js diff --git a/README.md b/README.md index ddb67e6..5e896b9 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,15 @@ And run `webpack` via your preferred method. ## Options -| Name | Type | Default | Description | -| :-----------------------------------: | :-------------------------: | :-----------------------------: | :-------------------------------------------------------------------------------- | -| **[`worker`](#worker)** | `{String\|Object}` | `Worker` | Allows to set web worker constructor name and options | -| **[`publicPath`](#publicpath)** | `{String\|Function}` | based on `output.publicPath` | specifies the public URL address of the output files when referenced in a browser | -| **[`filename`](#filename)** | `{String\|Function}` | based on `output.filename` | The filename of entry chunks for web workers | -| **[`chunkFilename`](#chunkfilename)** | `{String}` | based on `output.chunkFilename` | The filename of non-entry chunks for web workers | -| **[`inline`](#inline)** | `'no-fallback'\|'fallback'` | `undefined` | Allow to inline the worker as a `BLOB` | -| **[`esModule`](#esmodule)** | `{Boolean}` | `true` | Use ES modules syntax | +| Name | Type | Default | Description | +| :-----------------------------------: | :-------------------------: | :-----------------------------: | :-------------------------------------------------------------------------------------------- | +| **[`worker`](#worker)** | `{String\|Object}` | `Worker` | Allows to set web worker constructor name and options | +| **[`crossOrigin`](#crossorigin)** | `{String}` | `undefined` | Specifies origin and path to serve worker file from in case worker is from a different origin | +| **[`publicPath`](#publicpath)** | `{String\|Function}` | based on `output.publicPath` | specifies the public URL address of the output files when referenced in a browser | +| **[`filename`](#filename)** | `{String\|Function}` | based on `output.filename` | The filename of entry chunks for web workers | +| **[`chunkFilename`](#chunkfilename)** | `{String}` | based on `output.chunkFilename` | The filename of non-entry chunks for web workers | +| **[`inline`](#inline)** | `'no-fallback'\|'fallback'` | `undefined` | Allow to inline the worker as a `BLOB` | +| **[`esModule`](#esmodule)** | `{Boolean}` | `true` | Use ES modules syntax | ### `worker` @@ -133,6 +134,32 @@ module.exports = { }; ``` +### `crossOrigin` + +Type: `String` +Default: `undefined` + +When worker file must be served from a different domain such as when using a CDN, set this to the domain and path that house the worker. +Note that this should probably be unset during development. + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.worker\.(c|m)?js$/i, + loader: 'worker-loader', + options: { + crossOrigin: 'https://my-cdn.com/path/', + }, + }, + ], + }, +}; +``` + ### `publicPath` Type: `String|Function` @@ -500,11 +527,12 @@ Even downloads from the `webpack-dev-server` could be blocked. There are two workarounds: -Firstly, you can inline the worker as a blob instead of downloading it as an external script via the [`inline`](#inline) parameter +Firstly, you can specifies the CDN domain and path via the [`crossOrigin`](#crossorigin) option **App.js** ```js +// This will cause the worker to be downloaded from `https://my-cdn.com/abc/file.worker.js` import Worker from './file.worker.js'; ``` @@ -516,19 +544,18 @@ module.exports = { rules: [ { loader: 'worker-loader', - options: { inline: 'fallback' }, + options: { crossOrigin: 'https://my-cdn.com/abc/' }, }, ], }, }; ``` -Secondly, you may override the base download URL for your worker script via the [`publicPath`](#publicpath) option +Secondly, you may inline the worker as a blob instead of downloading it as an external script via the [`inline`](#inline) parameter **App.js** ```js -// This will cause the worker to be downloaded from `/workers/file.worker.js` import Worker from './file.worker.js'; ``` @@ -540,7 +567,7 @@ module.exports = { rules: [ { loader: 'worker-loader', - options: { publicPath: '/workers/' }, + options: { inline: 'fallback' }, }, ], }, diff --git a/src/index.js b/src/index.js index d7cb59a..6c648da 100644 --- a/src/index.js +++ b/src/index.js @@ -65,11 +65,13 @@ export function pitch(request) { const publicPath = options.publicPath ? options.publicPath : compilerOptions.output.publicPath; + const crossOrigin = options.crossOrigin || false; workerContext.options = { filename, chunkFilename, publicPath, + crossOrigin, globalObject: 'self', }; diff --git a/src/options.json b/src/options.json index 1d65d7b..f4a025a 100644 --- a/src/options.json +++ b/src/options.json @@ -52,6 +52,9 @@ "inline": { "enum": ["no-fallback", "fallback"] }, + "crossOrigin": { + "type": "string" + }, "esModule": { "type": "boolean" } diff --git a/src/runtime/crossOrigin.js b/src/runtime/crossOrigin.js new file mode 100644 index 0000000..2755344 --- /dev/null +++ b/src/runtime/crossOrigin.js @@ -0,0 +1,34 @@ +/* eslint-env browser */ +/* eslint-disable no-undef, no-use-before-define, new-cap */ +// initial solution by Benohead's Software Blog https://benohead.com/blog/2017/12/06/cross-domain-cross-browser-web-workers/ + +module.exports = (workerConstructor, workerOptions, workerUrl) => { + let worker = null; + let blob; + + try { + blob = new Blob([`importScripts('${workerUrl}');`], { + type: 'application/javascript', + }); + } catch (e) { + const BlobBuilder = + window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder; + + blobBuilder = new BlobBuilder(); + + blobBuilder.append(`importScripts('${workerUrl}');`); + + blob = blobBuilder.getBlob('application/javascript'); + } + + const URL = window.URL || window.webkitURL; + const blobUrl = URL.createObjectURL(blob); + worker = new window[workerConstructor](blobUrl, workerOptions); + + URL.revokeObjectURL(blobUrl); + + return worker; +}; diff --git a/src/utils.js b/src/utils.js index 7a7a034..9271164 100644 --- a/src/utils.js +++ b/src/utils.js @@ -79,6 +79,28 @@ ${ )}, ${fallbackWorkerPath});\n}\n`; } + if (options.crossOrigin) { + const CrossOriginWorkerPath = stringifyRequest( + loaderContext, + `!!${require.resolve('./runtime/crossOrigin.js')}` + ); + + return ` +${ + esModule + ? `import worker from ${CrossOriginWorkerPath};` + : `var worker = require(${CrossOriginWorkerPath});` +} + +${ + esModule ? 'export default' : 'module.exports =' +} function() {\n return worker(${JSON.stringify( + workerConstructor + )}, ${JSON.stringify(workerOptions)}, ${JSON.stringify( + options.crossOrigin + )} + ${JSON.stringify(workerFilename)});\n}\n`; + } + return `${ esModule ? 'export default' : 'module.exports =' } function() {\n return new ${workerConstructor}(__webpack_public_path__ + ${JSON.stringify( diff --git a/test/__snapshots__/crossOrigin-option.test.js.snap b/test/__snapshots__/crossOrigin-option.test.js.snap new file mode 100644 index 0000000..7b0eb27 --- /dev/null +++ b/test/__snapshots__/crossOrigin-option.test.js.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`"crossOrigin" option should not work by default: errors 1`] = `Array []`; + +exports[`"crossOrigin" option should not work by default: module 1`] = ` +"export default function() { + return new Worker(__webpack_public_path__ + \\"test.worker.js\\"); +} +" +`; + +exports[`"crossOrigin" option should not work by default: result 1`] = `"{\\"postMessage\\":true,\\"onmessage\\":true}"`; + +exports[`"crossOrigin" option should not work by default: warnings 1`] = `Array []`; + +exports[`"crossOrigin" option should work with crossOrigin enabled and "esModule" with "false" value: errors 1`] = `Array []`; + +exports[`"crossOrigin" option should work with crossOrigin enabled and "esModule" with "false" value: result 1`] = `"{\\"postMessage\\":true,\\"onmessage\\":true}"`; + +exports[`"crossOrigin" option should work with crossOrigin enabled and "esModule" with "false" value: warnings 1`] = `Array []`; + +exports[`"crossOrigin" option should work with crossOrigin enabled and "esModule" with "true" value: errors 1`] = `Array []`; + +exports[`"crossOrigin" option should work with crossOrigin enabled and "esModule" with "true" value: result 1`] = `"{\\"postMessage\\":true,\\"onmessage\\":true}"`; + +exports[`"crossOrigin" option should work with crossOrigin enabled and "esModule" with "true" value: warnings 1`] = `Array []`; + +exports[`"crossOrigin" option should work with crossOrigin enabled: errors 1`] = `Array []`; + +exports[`"crossOrigin" option should work with crossOrigin enabled: result 1`] = `"{\\"postMessage\\":true,\\"onmessage\\":true}"`; + +exports[`"crossOrigin" option should work with crossOrigin enabled: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 463d6d5..118d61f 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -66,49 +66,49 @@ exports[`validate options should throw an error on the "publicPath" option with exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` "Invalid options object. Worker Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { worker?, publicPath?, filename?, chunkFilename?, inline?, esModule? }" + object { worker?, publicPath?, filename?, chunkFilename?, inline?, crossOrigin?, esModule? }" `; exports[`validate options should throw an error on the "worker" option with "[]" value 1`] = ` diff --git a/test/crossOrigin-option.test.js b/test/crossOrigin-option.test.js new file mode 100644 index 0000000..a1d4e58 --- /dev/null +++ b/test/crossOrigin-option.test.js @@ -0,0 +1,100 @@ +import getPort from 'get-port'; + +import { + compile, + getCompiler, + getErrors, + getModuleSource, + getResultFromBrowser, + getWarnings, +} from './helpers'; + +describe('"crossOrigin" option', () => { + it('should not work by default', async () => { + const compiler = getCompiler('./basic/entry.js'); + const stats = await compile(compiler); + const result = await getResultFromBrowser(stats); + + expect(getModuleSource('./basic/worker.js', stats)).toMatchSnapshot( + 'module' + ); + expect(stats.compilation.assets['test.worker.js']).toBeDefined(); + expect(result).toMatchSnapshot('result'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should work with crossOrigin enabled', async () => { + const port = await getPort(); + const compiler = getCompiler('./basic/entry.js', { + crossOrigin: `http://localhost:${port}/public-path-static/`, + }); + const stats = await compile(compiler); + const result = await getResultFromBrowser(stats, port); + const moduleSource = getModuleSource('./basic/worker.js', stats); + + expect(moduleSource.indexOf('crossOrigin.js') > 0).toBe(true); + expect( + moduleSource.indexOf('__webpack_public_path__ + "test.worker.js"') === -1 + ).toBe(true); + expect( + moduleSource.indexOf( + `"http://localhost:${port}/public-path-static/" + "test.worker.js"` + ) > 0 + ).toBe(true); + expect(stats.compilation.assets['test.worker.js']).toBeDefined(); + expect(result).toMatchSnapshot('result'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should work with crossOrigin enabled and "esModule" with "false" value', async () => { + const port = await getPort(); + const compiler = getCompiler('./basic/entry.js', { + crossOrigin: `http://localhost:${port}/public-path-static/`, + esModule: false, + }); + const stats = await compile(compiler); + const result = await getResultFromBrowser(stats, port); + const moduleSource = getModuleSource('./basic/worker.js', stats); + + expect(moduleSource.indexOf('crossOrigin.js') > 0).toBe(true); + expect( + moduleSource.indexOf('__webpack_public_path__ + "test.worker.js"') === -1 + ).toBe(true); + expect( + moduleSource.indexOf( + `"http://localhost:${port}/public-path-static/" + "test.worker.js"` + ) > 0 + ).toBe(true); + expect(stats.compilation.assets['test.worker.js']).toBeDefined(); + expect(result).toMatchSnapshot('result'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should work with crossOrigin enabled and "esModule" with "true" value', async () => { + const port = await getPort(); + const compiler = getCompiler('./basic/entry.js', { + crossOrigin: `http://localhost:${port}/public-path-static/`, + esModule: true, + }); + const stats = await compile(compiler); + const result = await getResultFromBrowser(stats, port); + const moduleSource = getModuleSource('./basic/worker.js', stats); + + expect(moduleSource.indexOf('crossOrigin.js') > 0).toBe(true); + expect( + moduleSource.indexOf('__webpack_public_path__ + "test.worker.js"') === -1 + ).toBe(true); + expect( + moduleSource.indexOf( + `"http://localhost:${port}/public-path-static/" + "test.worker.js"` + ) > 0 + ).toBe(true); + expect(stats.compilation.assets['test.worker.js']).toBeDefined(); + expect(result).toMatchSnapshot('result'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); +}); diff --git a/test/helpers/getResultFromBrowser.js b/test/helpers/getResultFromBrowser.js index 64771b1..3aab401 100644 --- a/test/helpers/getResultFromBrowser.js +++ b/test/helpers/getResultFromBrowser.js @@ -4,10 +4,10 @@ import getPort from 'get-port'; import express from 'express'; import puppeteer from 'puppeteer'; -export default async function getResultFromBrowser(stats) { +export default async function getResultFromBrowser(stats, serverPort) { const assets = Object.entries(stats.compilation.assets); const app = express(); - const port = await getPort(); + const port = serverPort || (await getPort()); const server = app.listen(port); app.use(