WorkerPlugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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. expressions: {},
  184. otherElements: [],
  185. values: {},
  186. spread: false,
  187. insertType: arg2 ? "spread" : "argument",
  188. insertLocation: arg2 ? arg2.range : arg1.range[1]
  189. };
  190. const { options: importOptions, errors: commentErrors } =
  191. parser.parseCommentOptions(expr.range);
  192. if (commentErrors) {
  193. for (const e of commentErrors) {
  194. const { comment } = e;
  195. parser.state.module.addWarning(
  196. new CommentCompilationWarning(
  197. `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
  198. comment.loc
  199. )
  200. );
  201. }
  202. }
  203. /** @type {EntryOptions} */
  204. let entryOptions = {};
  205. if (importOptions) {
  206. if (importOptions.webpackIgnore !== undefined) {
  207. if (typeof importOptions.webpackIgnore !== "boolean") {
  208. parser.state.module.addWarning(
  209. new UnsupportedFeatureWarning(
  210. `\`webpackIgnore\` expected a boolean, but received: ${importOptions.webpackIgnore}.`,
  211. expr.loc
  212. )
  213. );
  214. } else {
  215. if (importOptions.webpackIgnore) {
  216. return false;
  217. }
  218. }
  219. }
  220. if (importOptions.webpackEntryOptions !== undefined) {
  221. if (
  222. typeof importOptions.webpackEntryOptions !== "object" ||
  223. importOptions.webpackEntryOptions === null
  224. ) {
  225. parser.state.module.addWarning(
  226. new UnsupportedFeatureWarning(
  227. `\`webpackEntryOptions\` expected a object, but received: ${importOptions.webpackEntryOptions}.`,
  228. expr.loc
  229. )
  230. );
  231. } else {
  232. Object.assign(
  233. entryOptions,
  234. importOptions.webpackEntryOptions
  235. );
  236. }
  237. }
  238. if (importOptions.webpackChunkName !== undefined) {
  239. if (typeof importOptions.webpackChunkName !== "string") {
  240. parser.state.module.addWarning(
  241. new UnsupportedFeatureWarning(
  242. `\`webpackChunkName\` expected a string, but received: ${importOptions.webpackChunkName}.`,
  243. expr.loc
  244. )
  245. );
  246. } else {
  247. entryOptions.name = importOptions.webpackChunkName;
  248. }
  249. }
  250. }
  251. if (
  252. !Object.prototype.hasOwnProperty.call(entryOptions, "name") &&
  253. options &&
  254. typeof options.name === "string"
  255. ) {
  256. entryOptions.name = options.name;
  257. }
  258. if (entryOptions.runtime === undefined) {
  259. let i = workerIndexMap.get(parser.state) || 0;
  260. workerIndexMap.set(parser.state, i + 1);
  261. let name = `${cachedContextify(
  262. parser.state.module.identifier()
  263. )}|${i}`;
  264. const hash = createHash(compilation.outputOptions.hashFunction);
  265. hash.update(name);
  266. const digest = /** @type {string} */ (
  267. hash.digest(compilation.outputOptions.hashDigest)
  268. );
  269. entryOptions.runtime = digest.slice(
  270. 0,
  271. compilation.outputOptions.hashDigestLength
  272. );
  273. }
  274. const block = new AsyncDependenciesBlock({
  275. name: entryOptions.name,
  276. entryOptions: {
  277. chunkLoading: this._chunkLoading,
  278. wasmLoading: this._wasmLoading,
  279. ...entryOptions
  280. }
  281. });
  282. block.loc = expr.loc;
  283. const dep = new WorkerDependency(url.string, range);
  284. dep.loc = expr.loc;
  285. block.addDependency(dep);
  286. parser.state.module.addBlock(block);
  287. if (compilation.outputOptions.trustedTypes) {
  288. const dep = new CreateScriptUrlDependency(
  289. expr.arguments[0].range
  290. );
  291. dep.loc = expr.loc;
  292. parser.state.module.addDependency(dep);
  293. }
  294. if (expressions.type) {
  295. const expr = expressions.type;
  296. if (options.type !== false) {
  297. const dep = new ConstDependency(
  298. this._module ? '"module"' : "undefined",
  299. expr.range
  300. );
  301. dep.loc = expr.loc;
  302. parser.state.module.addPresentationalDependency(dep);
  303. expressions.type = undefined;
  304. }
  305. } else if (insertType === "comma") {
  306. if (this._module || hasSpreadInOptions) {
  307. const dep = new ConstDependency(
  308. `, type: ${this._module ? '"module"' : "undefined"}`,
  309. insertLocation
  310. );
  311. dep.loc = expr.loc;
  312. parser.state.module.addPresentationalDependency(dep);
  313. }
  314. } else if (insertType === "spread") {
  315. const dep1 = new ConstDependency(
  316. "Object.assign({}, ",
  317. insertLocation[0]
  318. );
  319. const dep2 = new ConstDependency(
  320. `, { type: ${this._module ? '"module"' : "undefined"} })`,
  321. insertLocation[1]
  322. );
  323. dep1.loc = expr.loc;
  324. dep2.loc = expr.loc;
  325. parser.state.module.addPresentationalDependency(dep1);
  326. parser.state.module.addPresentationalDependency(dep2);
  327. } else if (insertType === "argument") {
  328. if (this._module) {
  329. const dep = new ConstDependency(
  330. ', { type: "module" }',
  331. insertLocation
  332. );
  333. dep.loc = expr.loc;
  334. parser.state.module.addPresentationalDependency(dep);
  335. }
  336. }
  337. parser.walkExpression(expr.callee);
  338. for (const key of Object.keys(expressions)) {
  339. if (expressions[key]) parser.walkExpression(expressions[key]);
  340. }
  341. for (const prop of otherElements) {
  342. parser.walkProperty(prop);
  343. }
  344. if (insertType === "spread") {
  345. parser.walkExpression(arg2);
  346. }
  347. return true;
  348. };
  349. const processItem = item => {
  350. if (item.endsWith("()")) {
  351. parser.hooks.call
  352. .for(item.slice(0, -2))
  353. .tap("WorkerPlugin", handleNewWorker);
  354. } else {
  355. const match = /^(.+?)(\(\))?\s+from\s+(.+)$/.exec(item);
  356. if (match) {
  357. const ids = match[1].split(".");
  358. const call = match[2];
  359. const source = match[3];
  360. (call ? parser.hooks.call : parser.hooks.new)
  361. .for(harmonySpecifierTag)
  362. .tap("WorkerPlugin", expr => {
  363. const settings = /** @type {HarmonySettings} */ (
  364. parser.currentTagData
  365. );
  366. if (
  367. !settings ||
  368. settings.source !== source ||
  369. !equals(settings.ids, ids)
  370. ) {
  371. return;
  372. }
  373. return handleNewWorker(expr);
  374. });
  375. } else {
  376. parser.hooks.new.for(item).tap("WorkerPlugin", handleNewWorker);
  377. }
  378. }
  379. };
  380. for (const item of options) {
  381. if (item === "...") {
  382. DEFAULT_SYNTAX.forEach(processItem);
  383. } else processItem(item);
  384. }
  385. };
  386. normalModuleFactory.hooks.parser
  387. .for("javascript/auto")
  388. .tap("WorkerPlugin", parserPlugin);
  389. normalModuleFactory.hooks.parser
  390. .for("javascript/esm")
  391. .tap("WorkerPlugin", parserPlugin);
  392. }
  393. );
  394. }
  395. }
  396. module.exports = WorkerPlugin;