/** * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ import path from 'path'; import WORKER_PLUGIN_SYMBOL from './symbol'; import ParserHelpers from 'webpack/lib/ParserHelpers'; let HarmonyImportSpecifierDependency; try { HarmonyImportSpecifierDependency = require('webpack/lib/dependencies/HarmonyImportSpecifierDependency'); } catch (e) {} const NAME = 'WorkerPlugin'; const workerLoader = path.resolve(__dirname, 'loader.js'); export default class WorkerPlugin { constructor (options) { this.options = options || {}; this[WORKER_PLUGIN_SYMBOL] = true; } apply (compiler) { compiler.hooks.normalModuleFactory.tap(NAME, factory => { let workerId = 0; factory.hooks.parser.for('javascript/auto').tap(NAME, parser => parse(parser, false)); factory.hooks.parser.for('javascript/dynamic').tap(NAME, parser => parse(parser, false)); factory.hooks.parser.for('javascript/esm').tap(NAME, parser => parse(parser, true)); const parse = (parser, esModule) => { const handleWorker = workerTypeString => expr => { const dep = parser.evaluateExpression(expr.arguments[0]); if (!dep.isString()) { parser.state.module.warnings.push({ message: `new ${workerTypeString}() will only be bundled if passed a String.` }); return false; } const optsExpr = expr.arguments[1]; let hasInitOptions = false; let typeModuleExpr; let opts; if (optsExpr) { opts = {}; for (let i = optsExpr.properties.length; i--;) { const prop = optsExpr.properties[i]; if (prop.type === 'Property' && !prop.computed && !prop.shorthand && !prop.method) { opts[prop.key.name] = parser.evaluateExpression(prop.value).string; if (prop.key.name === 'type') { typeModuleExpr = prop; } else { hasInitOptions = true; } } } } if (!opts || opts.type !== 'module') { parser.state.module.warnings.push({ message: `new ${workerTypeString}() will only be bundled if passed options that include { type: 'module' }.${opts ? `\n Received: new ${workerTypeString}()(${JSON.stringify(dep.string)}, ${JSON.stringify(opts)})` : ''}` }); return false; } const isStrictModule = esModule || (parser.state.buildMeta && parser.state.buildMeta.strictHarmonyModule); // Querystring-encoded loader prefix (faster/cleaner than JSON parameters): const loaderRequest = `${workerLoader}?name=${encodeURIComponent(opts.name || workerId)}${isStrictModule ? '&esModule' : ''}!${dep.string}`; // Unique ID for the worker URL variable: const id = `__webpack__worker__${workerId++}`; // .mjs / strict harmony mode if (isStrictModule) { const module = parser.state.current; if (!HarmonyImportSpecifierDependency) { throw Error(`${NAME}: Failed to import HarmonyImportSpecifierDependency. This plugin requires Webpack version 4.`); } // This is essentially the internals of "prepend an import to the module": const dependency = new HarmonyImportSpecifierDependency( loaderRequest, module, workerId, // no idea if this actually needs to be unique. 0 seemed to work. safety first? parser.scope, 'default', id, // this never gets used expr.arguments[0].range, // replace the usage/callsite with the generated reference: X_IMPORT_0["default"] true ); // avoid serializing the full loader filepath: (this gets prepended to unique suffix) dependency.userRequest = dep.string; module.addDependency(dependency); } else { // For CommonJS/Auto const req = `require(${JSON.stringify(loaderRequest)})`; ParserHelpers.toConstantDependency(parser, id)(expr.arguments[0]); ParserHelpers.addParsedVariableToModule(parser, id, req); } // update/remove the WorkerInitOptions argument if (this.options.workerType) { ParserHelpers.toConstantDependency(parser, JSON.stringify(this.options.workerType))(typeModuleExpr.value); } else if (this.options.preserveTypeModule !== true) { if (hasInitOptions) { // there might be other options - to avoid trailing comma issues, replace the type value with undefined but *leave the key*: ParserHelpers.toConstantDependency(parser, 'type:undefined')(typeModuleExpr); } else { // there was only a `{type}` option, we replace the opts argument with undefined to avoid trailing comma issues: ParserHelpers.toConstantDependency(parser, 'undefined')(optsExpr); } } return true; }; if (this.options.worker !== false) { parser.hooks.new.for('Worker').tap(NAME, handleWorker('Worker')); } if (this.options.sharedWorker) { parser.hooks.new.for('SharedWorker').tap(NAME, handleWorker('SharedWorker')); } }; }); } }