WorkerPlugin.js 13 KB

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