WorkerPlugin.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { pathToFileURL } = require("url");
  7. const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
  8. const CommentCompilationWarning = require("../CommentCompilationWarning");
  9. const {
  10. JAVASCRIPT_MODULE_TYPE_AUTO,
  11. JAVASCRIPT_MODULE_TYPE_ESM
  12. } = require("../ModuleTypeConstants");
  13. const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning");
  14. const EnableChunkLoadingPlugin = require("../javascript/EnableChunkLoadingPlugin");
  15. const { equals } = require("../util/ArrayHelpers");
  16. const createHash = require("../util/createHash");
  17. const { contextify } = require("../util/identifier");
  18. const EnableWasmLoadingPlugin = require("../wasm/EnableWasmLoadingPlugin");
  19. const ConstDependency = require("./ConstDependency");
  20. const CreateScriptUrlDependency = require("./CreateScriptUrlDependency");
  21. const {
  22. harmonySpecifierTag
  23. } = require("./HarmonyImportDependencyParserPlugin");
  24. const WorkerDependency = require("./WorkerDependency");
  25. /** @typedef {import("estree").Expression} Expression */
  26. /** @typedef {import("estree").ObjectExpression} ObjectExpression */
  27. /** @typedef {import("estree").Pattern} Pattern */
  28. /** @typedef {import("estree").Property} Property */
  29. /** @typedef {import("estree").SpreadElement} SpreadElement */
  30. /** @typedef {import("../Compiler")} Compiler */
  31. /** @typedef {import("../Entrypoint").EntryOptions} EntryOptions */
  32. /** @typedef {import("../Parser").ParserState} ParserState */
  33. /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
  34. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  35. /** @typedef {import("./HarmonyImportDependencyParserPlugin").HarmonySettings} HarmonySettings */
  36. const getUrl = module => {
  37. return pathToFileURL(module.resource).toString();
  38. };
  39. const DEFAULT_SYNTAX = [
  40. "Worker",
  41. "SharedWorker",
  42. "navigator.serviceWorker.register()",
  43. "Worker from worker_threads"
  44. ];
  45. /** @type {WeakMap<ParserState, number>} */
  46. const workerIndexMap = new WeakMap();
  47. const PLUGIN_NAME = "WorkerPlugin";
  48. class WorkerPlugin {
  49. constructor(chunkLoading, wasmLoading, module, workerPublicPath) {
  50. this._chunkLoading = chunkLoading;
  51. this._wasmLoading = wasmLoading;
  52. this._module = module;
  53. this._workerPublicPath = workerPublicPath;
  54. }
  55. /**
  56. * Apply the plugin
  57. * @param {Compiler} compiler the compiler instance
  58. * @returns {void}
  59. */
  60. apply(compiler) {
  61. if (this._chunkLoading) {
  62. new EnableChunkLoadingPlugin(this._chunkLoading).apply(compiler);
  63. }
  64. if (this._wasmLoading) {
  65. new EnableWasmLoadingPlugin(this._wasmLoading).apply(compiler);
  66. }
  67. const cachedContextify = contextify.bindContextCache(
  68. compiler.context,
  69. compiler.root
  70. );
  71. compiler.hooks.thisCompilation.tap(
  72. PLUGIN_NAME,
  73. (compilation, { normalModuleFactory }) => {
  74. compilation.dependencyFactories.set(
  75. WorkerDependency,
  76. normalModuleFactory
  77. );
  78. compilation.dependencyTemplates.set(
  79. WorkerDependency,
  80. new WorkerDependency.Template()
  81. );
  82. compilation.dependencyTemplates.set(
  83. CreateScriptUrlDependency,
  84. new CreateScriptUrlDependency.Template()
  85. );
  86. /**
  87. * @param {JavascriptParser} parser the parser
  88. * @param {Expression} expr expression
  89. * @returns {[BasicEvaluatedExpression, [number, number]]} parsed
  90. */
  91. const parseModuleUrl = (parser, expr) => {
  92. if (
  93. expr.type !== "NewExpression" ||
  94. expr.callee.type === "Super" ||
  95. expr.arguments.length !== 2
  96. )
  97. return;
  98. const [arg1, arg2] = expr.arguments;
  99. if (arg1.type === "SpreadElement") return;
  100. if (arg2.type === "SpreadElement") return;
  101. const callee = parser.evaluateExpression(expr.callee);
  102. if (!callee.isIdentifier() || callee.identifier !== "URL") return;
  103. const arg2Value = parser.evaluateExpression(arg2);
  104. if (
  105. !arg2Value.isString() ||
  106. !arg2Value.string.startsWith("file://") ||
  107. arg2Value.string !== getUrl(parser.state.module)
  108. ) {
  109. return;
  110. }
  111. const arg1Value = parser.evaluateExpression(arg1);
  112. return [arg1Value, [arg1.range[0], arg2.range[1]]];
  113. };
  114. /**
  115. * @param {JavascriptParser} parser the parser
  116. * @param {ObjectExpression} expr expression
  117. * @returns {{ expressions: Record<string, Expression | Pattern>, otherElements: (Property | SpreadElement)[], values: Record<string, any>, spread: boolean, insertType: "comma" | "single", insertLocation: number }} parsed object
  118. */
  119. const parseObjectExpression = (parser, expr) => {
  120. /** @type {Record<string, any>} */
  121. const values = {};
  122. /** @type {Record<string, Expression | Pattern>} */
  123. const expressions = {};
  124. /** @type {(Property | SpreadElement)[]} */
  125. const otherElements = [];
  126. let spread = false;
  127. for (const prop of expr.properties) {
  128. if (prop.type === "SpreadElement") {
  129. spread = true;
  130. } else if (
  131. prop.type === "Property" &&
  132. !prop.method &&
  133. !prop.computed &&
  134. prop.key.type === "Identifier"
  135. ) {
  136. expressions[prop.key.name] = prop.value;
  137. if (!prop.shorthand && !prop.value.type.endsWith("Pattern")) {
  138. const value = parser.evaluateExpression(
  139. /** @type {Expression} */ (prop.value)
  140. );
  141. if (value.isCompileTimeValue())
  142. values[prop.key.name] = value.asCompileTimeValue();
  143. }
  144. } else {
  145. otherElements.push(prop);
  146. }
  147. }
  148. const insertType = expr.properties.length > 0 ? "comma" : "single";
  149. const insertLocation =
  150. expr.properties[expr.properties.length - 1].range[1];
  151. return {
  152. expressions,
  153. otherElements,
  154. values,
  155. spread,
  156. insertType,
  157. insertLocation
  158. };
  159. };
  160. /**
  161. * @param {JavascriptParser} parser the parser
  162. * @param {object} parserOptions options
  163. */
  164. const parserPlugin = (parser, parserOptions) => {
  165. if (parserOptions.worker === false) return;
  166. const options = !Array.isArray(parserOptions.worker)
  167. ? ["..."]
  168. : parserOptions.worker;
  169. const handleNewWorker = expr => {
  170. if (expr.arguments.length === 0 || expr.arguments.length > 2)
  171. return;
  172. const [arg1, arg2] = expr.arguments;
  173. if (arg1.type === "SpreadElement") return;
  174. if (arg2 && arg2.type === "SpreadElement") return;
  175. const parsedUrl = parseModuleUrl(parser, arg1);
  176. if (!parsedUrl) return;
  177. const [url, range] = parsedUrl;
  178. if (!url.isString()) return;
  179. const {
  180. expressions,
  181. otherElements,
  182. values: options,
  183. spread: hasSpreadInOptions,
  184. insertType,
  185. insertLocation
  186. } = arg2 && arg2.type === "ObjectExpression"
  187. ? parseObjectExpression(parser, arg2)
  188. : {
  189. /** @type {Record<string, Expression | Pattern>} */
  190. expressions: {},
  191. otherElements: [],
  192. /** @type {Record<string, any>} */
  193. values: {},
  194. spread: false,
  195. insertType: arg2 ? "spread" : "argument",
  196. insertLocation: arg2 ? arg2.range : arg1.range[1]
  197. };
  198. const { options: importOptions, errors: commentErrors } =
  199. parser.parseCommentOptions(expr.range);
  200. if (commentErrors) {
  201. for (const e of commentErrors) {
  202. const { comment } = e;
  203. parser.state.module.addWarning(
  204. new CommentCompilationWarning(
  205. `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
  206. comment.loc
  207. )
  208. );
  209. }
  210. }
  211. /** @type {EntryOptions} */
  212. let entryOptions = {};
  213. if (importOptions) {
  214. if (importOptions.webpackIgnore !== undefined) {
  215. if (typeof importOptions.webpackIgnore !== "boolean") {
  216. parser.state.module.addWarning(
  217. new UnsupportedFeatureWarning(
  218. `\`webpackIgnore\` expected a boolean, but received: ${importOptions.webpackIgnore}.`,
  219. expr.loc
  220. )
  221. );
  222. } else {
  223. if (importOptions.webpackIgnore) {
  224. return false;
  225. }
  226. }
  227. }
  228. if (importOptions.webpackEntryOptions !== undefined) {
  229. if (
  230. typeof importOptions.webpackEntryOptions !== "object" ||
  231. importOptions.webpackEntryOptions === null
  232. ) {
  233. parser.state.module.addWarning(
  234. new UnsupportedFeatureWarning(
  235. `\`webpackEntryOptions\` expected a object, but received: ${importOptions.webpackEntryOptions}.`,
  236. expr.loc
  237. )
  238. );
  239. } else {
  240. Object.assign(
  241. entryOptions,
  242. importOptions.webpackEntryOptions
  243. );
  244. }
  245. }
  246. if (importOptions.webpackChunkName !== undefined) {
  247. if (typeof importOptions.webpackChunkName !== "string") {
  248. parser.state.module.addWarning(
  249. new UnsupportedFeatureWarning(
  250. `\`webpackChunkName\` expected a string, but received: ${importOptions.webpackChunkName}.`,
  251. expr.loc
  252. )
  253. );
  254. } else {
  255. entryOptions.name = importOptions.webpackChunkName;
  256. }
  257. }
  258. }
  259. if (
  260. !Object.prototype.hasOwnProperty.call(entryOptions, "name") &&
  261. options &&
  262. typeof options.name === "string"
  263. ) {
  264. entryOptions.name = options.name;
  265. }
  266. if (entryOptions.runtime === undefined) {
  267. let i = workerIndexMap.get(parser.state) || 0;
  268. workerIndexMap.set(parser.state, i + 1);
  269. let name = `${cachedContextify(
  270. parser.state.module.identifier()
  271. )}|${i}`;
  272. const hash = createHash(compilation.outputOptions.hashFunction);
  273. hash.update(name);
  274. const digest = /** @type {string} */ (
  275. hash.digest(compilation.outputOptions.hashDigest)
  276. );
  277. entryOptions.runtime = digest.slice(
  278. 0,
  279. compilation.outputOptions.hashDigestLength
  280. );
  281. }
  282. const block = new AsyncDependenciesBlock({
  283. name: entryOptions.name,
  284. entryOptions: {
  285. chunkLoading: this._chunkLoading,
  286. wasmLoading: this._wasmLoading,
  287. ...entryOptions
  288. }
  289. });
  290. block.loc = expr.loc;
  291. const dep = new WorkerDependency(url.string, range, {
  292. publicPath: this._workerPublicPath
  293. });
  294. dep.loc = expr.loc;
  295. block.addDependency(dep);
  296. parser.state.module.addBlock(block);
  297. if (compilation.outputOptions.trustedTypes) {
  298. const dep = new CreateScriptUrlDependency(
  299. expr.arguments[0].range
  300. );
  301. dep.loc = expr.loc;
  302. parser.state.module.addDependency(dep);
  303. }
  304. if (expressions.type) {
  305. const expr = expressions.type;
  306. if (options.type !== false) {
  307. const dep = new ConstDependency(
  308. this._module ? '"module"' : "undefined",
  309. expr.range
  310. );
  311. dep.loc = expr.loc;
  312. parser.state.module.addPresentationalDependency(dep);
  313. expressions.type = undefined;
  314. }
  315. } else if (insertType === "comma") {
  316. if (this._module || hasSpreadInOptions) {
  317. const dep = new ConstDependency(
  318. `, type: ${this._module ? '"module"' : "undefined"}`,
  319. insertLocation
  320. );
  321. dep.loc = expr.loc;
  322. parser.state.module.addPresentationalDependency(dep);
  323. }
  324. } else if (insertType === "spread") {
  325. const dep1 = new ConstDependency(
  326. "Object.assign({}, ",
  327. insertLocation[0]
  328. );
  329. const dep2 = new ConstDependency(
  330. `, { type: ${this._module ? '"module"' : "undefined"} })`,
  331. insertLocation[1]
  332. );
  333. dep1.loc = expr.loc;
  334. dep2.loc = expr.loc;
  335. parser.state.module.addPresentationalDependency(dep1);
  336. parser.state.module.addPresentationalDependency(dep2);
  337. } else if (insertType === "argument") {
  338. if (this._module) {
  339. const dep = new ConstDependency(
  340. ', { type: "module" }',
  341. insertLocation
  342. );
  343. dep.loc = expr.loc;
  344. parser.state.module.addPresentationalDependency(dep);
  345. }
  346. }
  347. parser.walkExpression(expr.callee);
  348. for (const key of Object.keys(expressions)) {
  349. if (expressions[key]) parser.walkExpression(expressions[key]);
  350. }
  351. for (const prop of otherElements) {
  352. parser.walkProperty(prop);
  353. }
  354. if (insertType === "spread") {
  355. parser.walkExpression(arg2);
  356. }
  357. return true;
  358. };
  359. const processItem = item => {
  360. if (item.endsWith("()")) {
  361. parser.hooks.call
  362. .for(item.slice(0, -2))
  363. .tap(PLUGIN_NAME, handleNewWorker);
  364. } else {
  365. const match = /^(.+?)(\(\))?\s+from\s+(.+)$/.exec(item);
  366. if (match) {
  367. const ids = match[1].split(".");
  368. const call = match[2];
  369. const source = match[3];
  370. (call ? parser.hooks.call : parser.hooks.new)
  371. .for(harmonySpecifierTag)
  372. .tap(PLUGIN_NAME, expr => {
  373. const settings = /** @type {HarmonySettings} */ (
  374. parser.currentTagData
  375. );
  376. if (
  377. !settings ||
  378. settings.source !== source ||
  379. !equals(settings.ids, ids)
  380. ) {
  381. return;
  382. }
  383. return handleNewWorker(expr);
  384. });
  385. } else {
  386. parser.hooks.new.for(item).tap(PLUGIN_NAME, handleNewWorker);
  387. }
  388. }
  389. };
  390. for (const item of options) {
  391. if (item === "...") {
  392. DEFAULT_SYNTAX.forEach(processItem);
  393. } else processItem(item);
  394. }
  395. };
  396. normalModuleFactory.hooks.parser
  397. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  398. .tap(PLUGIN_NAME, parserPlugin);
  399. normalModuleFactory.hooks.parser
  400. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  401. .tap(PLUGIN_NAME, parserPlugin);
  402. }
  403. );
  404. }
  405. }
  406. module.exports = WorkerPlugin;