You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

615 lines
19 KiB

4 years ago
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { SyncWaterfallHook } = require("tapable");
  7. const Template = require("../Template");
  8. class JsonpMainTemplatePlugin {
  9. apply(mainTemplate) {
  10. const needChunkOnDemandLoadingCode = chunk => {
  11. for (const chunkGroup of chunk.groupsIterable) {
  12. if (chunkGroup.getNumberOfChildren() > 0) return true;
  13. }
  14. return false;
  15. };
  16. const needChunkLoadingCode = chunk => {
  17. for (const chunkGroup of chunk.groupsIterable) {
  18. if (chunkGroup.chunks.length > 1) return true;
  19. if (chunkGroup.getNumberOfChildren() > 0) return true;
  20. }
  21. return false;
  22. };
  23. const needEntryDeferringCode = chunk => {
  24. for (const chunkGroup of chunk.groupsIterable) {
  25. if (chunkGroup.chunks.length > 1) return true;
  26. }
  27. return false;
  28. };
  29. const needPrefetchingCode = chunk => {
  30. const allPrefetchChunks = chunk.getChildIdsByOrdersMap(true).prefetch;
  31. return allPrefetchChunks && Object.keys(allPrefetchChunks).length;
  32. };
  33. // TODO webpack 5, no adding to .hooks, use WeakMap and static methods
  34. ["jsonpScript", "linkPreload", "linkPrefetch"].forEach(hook => {
  35. if (!mainTemplate.hooks[hook]) {
  36. mainTemplate.hooks[hook] = new SyncWaterfallHook([
  37. "source",
  38. "chunk",
  39. "hash"
  40. ]);
  41. }
  42. });
  43. const getScriptSrcPath = (hash, chunk, chunkIdExpression) => {
  44. const chunkFilename = mainTemplate.outputOptions.chunkFilename;
  45. const chunkMaps = chunk.getChunkMaps();
  46. return mainTemplate.getAssetPath(JSON.stringify(chunkFilename), {
  47. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  48. hashWithLength: length =>
  49. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
  50. chunk: {
  51. id: `" + ${chunkIdExpression} + "`,
  52. hash: `" + ${JSON.stringify(
  53. chunkMaps.hash
  54. )}[${chunkIdExpression}] + "`,
  55. hashWithLength(length) {
  56. const shortChunkHashMap = Object.create(null);
  57. for (const chunkId of Object.keys(chunkMaps.hash)) {
  58. if (typeof chunkMaps.hash[chunkId] === "string") {
  59. shortChunkHashMap[chunkId] = chunkMaps.hash[chunkId].substr(
  60. 0,
  61. length
  62. );
  63. }
  64. }
  65. return `" + ${JSON.stringify(
  66. shortChunkHashMap
  67. )}[${chunkIdExpression}] + "`;
  68. },
  69. name: `" + (${JSON.stringify(
  70. chunkMaps.name
  71. )}[${chunkIdExpression}]||${chunkIdExpression}) + "`,
  72. contentHash: {
  73. javascript: `" + ${JSON.stringify(
  74. chunkMaps.contentHash.javascript
  75. )}[${chunkIdExpression}] + "`
  76. },
  77. contentHashWithLength: {
  78. javascript: length => {
  79. const shortContentHashMap = {};
  80. const contentHash = chunkMaps.contentHash.javascript;
  81. for (const chunkId of Object.keys(contentHash)) {
  82. if (typeof contentHash[chunkId] === "string") {
  83. shortContentHashMap[chunkId] = contentHash[chunkId].substr(
  84. 0,
  85. length
  86. );
  87. }
  88. }
  89. return `" + ${JSON.stringify(
  90. shortContentHashMap
  91. )}[${chunkIdExpression}] + "`;
  92. }
  93. }
  94. },
  95. contentHashType: "javascript"
  96. });
  97. };
  98. mainTemplate.hooks.localVars.tap(
  99. "JsonpMainTemplatePlugin",
  100. (source, chunk, hash) => {
  101. const extraCode = [];
  102. if (needChunkLoadingCode(chunk)) {
  103. extraCode.push(
  104. "",
  105. "// object to store loaded and loading chunks",
  106. "// undefined = chunk not loaded, null = chunk preloaded/prefetched",
  107. "// Promise = chunk loading, 0 = chunk loaded",
  108. "var installedChunks = {",
  109. Template.indent(
  110. chunk.ids.map(id => `${JSON.stringify(id)}: 0`).join(",\n")
  111. ),
  112. "};",
  113. "",
  114. needEntryDeferringCode(chunk)
  115. ? needPrefetchingCode(chunk)
  116. ? "var deferredModules = [], deferredPrefetch = [];"
  117. : "var deferredModules = [];"
  118. : ""
  119. );
  120. }
  121. if (needChunkOnDemandLoadingCode(chunk)) {
  122. extraCode.push(
  123. "",
  124. "// script path function",
  125. "function jsonpScriptSrc(chunkId) {",
  126. Template.indent([
  127. `return ${mainTemplate.requireFn}.p + ${getScriptSrcPath(
  128. hash,
  129. chunk,
  130. "chunkId"
  131. )}`
  132. ]),
  133. "}"
  134. );
  135. }
  136. if (extraCode.length === 0) return source;
  137. return Template.asString([source, ...extraCode]);
  138. }
  139. );
  140. mainTemplate.hooks.jsonpScript.tap(
  141. "JsonpMainTemplatePlugin",
  142. (_, chunk, hash) => {
  143. const crossOriginLoading =
  144. mainTemplate.outputOptions.crossOriginLoading;
  145. const chunkLoadTimeout = mainTemplate.outputOptions.chunkLoadTimeout;
  146. const jsonpScriptType = mainTemplate.outputOptions.jsonpScriptType;
  147. return Template.asString([
  148. "var script = document.createElement('script');",
  149. "var onScriptComplete;",
  150. jsonpScriptType
  151. ? `script.type = ${JSON.stringify(jsonpScriptType)};`
  152. : "",
  153. "script.charset = 'utf-8';",
  154. `script.timeout = ${chunkLoadTimeout / 1000};`,
  155. `if (${mainTemplate.requireFn}.nc) {`,
  156. Template.indent(
  157. `script.setAttribute("nonce", ${mainTemplate.requireFn}.nc);`
  158. ),
  159. "}",
  160. "script.src = jsonpScriptSrc(chunkId);",
  161. crossOriginLoading
  162. ? Template.asString([
  163. "if (script.src.indexOf(window.location.origin + '/') !== 0) {",
  164. Template.indent(
  165. `script.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  166. ),
  167. "}"
  168. ])
  169. : "",
  170. "// create error before stack unwound to get useful stacktrace later",
  171. "var error = new Error();",
  172. "onScriptComplete = function (event) {",
  173. Template.indent([
  174. "// avoid mem leaks in IE.",
  175. "script.onerror = script.onload = null;",
  176. "clearTimeout(timeout);",
  177. "var chunk = installedChunks[chunkId];",
  178. "if(chunk !== 0) {",
  179. Template.indent([
  180. "if(chunk) {",
  181. Template.indent([
  182. "var errorType = event && (event.type === 'load' ? 'missing' : event.type);",
  183. "var realSrc = event && event.target && event.target.src;",
  184. "error.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';",
  185. "error.name = 'ChunkLoadError';",
  186. "error.type = errorType;",
  187. "error.request = realSrc;",
  188. "chunk[1](error);"
  189. ]),
  190. "}",
  191. "installedChunks[chunkId] = undefined;"
  192. ]),
  193. "}"
  194. ]),
  195. "};",
  196. "var timeout = setTimeout(function(){",
  197. Template.indent([
  198. "onScriptComplete({ type: 'timeout', target: script });"
  199. ]),
  200. `}, ${chunkLoadTimeout});`,
  201. "script.onerror = script.onload = onScriptComplete;"
  202. ]);
  203. }
  204. );
  205. mainTemplate.hooks.linkPreload.tap(
  206. "JsonpMainTemplatePlugin",
  207. (_, chunk, hash) => {
  208. const crossOriginLoading =
  209. mainTemplate.outputOptions.crossOriginLoading;
  210. const jsonpScriptType = mainTemplate.outputOptions.jsonpScriptType;
  211. return Template.asString([
  212. "var link = document.createElement('link');",
  213. jsonpScriptType
  214. ? `link.type = ${JSON.stringify(jsonpScriptType)};`
  215. : "",
  216. "link.charset = 'utf-8';",
  217. `if (${mainTemplate.requireFn}.nc) {`,
  218. Template.indent(
  219. `link.setAttribute("nonce", ${mainTemplate.requireFn}.nc);`
  220. ),
  221. "}",
  222. 'link.rel = "preload";',
  223. 'link.as = "script";',
  224. "link.href = jsonpScriptSrc(chunkId);",
  225. crossOriginLoading
  226. ? Template.asString([
  227. "if (link.href.indexOf(window.location.origin + '/') !== 0) {",
  228. Template.indent(
  229. `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  230. ),
  231. "}"
  232. ])
  233. : ""
  234. ]);
  235. }
  236. );
  237. mainTemplate.hooks.linkPrefetch.tap(
  238. "JsonpMainTemplatePlugin",
  239. (_, chunk, hash) => {
  240. const crossOriginLoading =
  241. mainTemplate.outputOptions.crossOriginLoading;
  242. return Template.asString([
  243. "var link = document.createElement('link');",
  244. crossOriginLoading
  245. ? `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  246. : "",
  247. `if (${mainTemplate.requireFn}.nc) {`,
  248. Template.indent(
  249. `link.setAttribute("nonce", ${mainTemplate.requireFn}.nc);`
  250. ),
  251. "}",
  252. 'link.rel = "prefetch";',
  253. 'link.as = "script";',
  254. "link.href = jsonpScriptSrc(chunkId);"
  255. ]);
  256. }
  257. );
  258. mainTemplate.hooks.requireEnsure.tap(
  259. "JsonpMainTemplatePlugin load",
  260. (source, chunk, hash) => {
  261. return Template.asString([
  262. source,
  263. "",
  264. "// JSONP chunk loading for javascript",
  265. "",
  266. "var installedChunkData = installedChunks[chunkId];",
  267. 'if(installedChunkData !== 0) { // 0 means "already installed".',
  268. Template.indent([
  269. "",
  270. '// a Promise means "currently loading".',
  271. "if(installedChunkData) {",
  272. Template.indent(["promises.push(installedChunkData[2]);"]),
  273. "} else {",
  274. Template.indent([
  275. "// setup Promise in chunk cache",
  276. "var promise = new Promise(function(resolve, reject) {",
  277. Template.indent([
  278. "installedChunkData = installedChunks[chunkId] = [resolve, reject];"
  279. ]),
  280. "});",
  281. "promises.push(installedChunkData[2] = promise);",
  282. "",
  283. "// start chunk loading",
  284. mainTemplate.hooks.jsonpScript.call("", chunk, hash),
  285. "document.head.appendChild(script);"
  286. ]),
  287. "}"
  288. ]),
  289. "}"
  290. ]);
  291. }
  292. );
  293. mainTemplate.hooks.requireEnsure.tap(
  294. {
  295. name: "JsonpMainTemplatePlugin preload",
  296. stage: 10
  297. },
  298. (source, chunk, hash) => {
  299. const chunkMap = chunk.getChildIdsByOrdersMap().preload;
  300. if (!chunkMap || Object.keys(chunkMap).length === 0) return source;
  301. return Template.asString([
  302. source,
  303. "",
  304. "// chunk preloadng for javascript",
  305. "",
  306. `var chunkPreloadMap = ${JSON.stringify(chunkMap, null, "\t")};`,
  307. "",
  308. "var chunkPreloadData = chunkPreloadMap[chunkId];",
  309. "if(chunkPreloadData) {",
  310. Template.indent([
  311. "chunkPreloadData.forEach(function(chunkId) {",
  312. Template.indent([
  313. "if(installedChunks[chunkId] === undefined) {",
  314. Template.indent([
  315. "installedChunks[chunkId] = null;",
  316. mainTemplate.hooks.linkPreload.call("", chunk, hash),
  317. "document.head.appendChild(link);"
  318. ]),
  319. "}"
  320. ]),
  321. "});"
  322. ]),
  323. "}"
  324. ]);
  325. }
  326. );
  327. mainTemplate.hooks.requireExtensions.tap(
  328. "JsonpMainTemplatePlugin",
  329. (source, chunk) => {
  330. if (!needChunkOnDemandLoadingCode(chunk)) return source;
  331. return Template.asString([
  332. source,
  333. "",
  334. "// on error function for async loading",
  335. `${mainTemplate.requireFn}.oe = function(err) { console.error(err); throw err; };`
  336. ]);
  337. }
  338. );
  339. mainTemplate.hooks.bootstrap.tap(
  340. "JsonpMainTemplatePlugin",
  341. (source, chunk, hash) => {
  342. if (needChunkLoadingCode(chunk)) {
  343. const withDefer = needEntryDeferringCode(chunk);
  344. const withPrefetch = needPrefetchingCode(chunk);
  345. return Template.asString([
  346. source,
  347. "",
  348. "// install a JSONP callback for chunk loading",
  349. "function webpackJsonpCallback(data) {",
  350. Template.indent([
  351. "var chunkIds = data[0];",
  352. "var moreModules = data[1];",
  353. withDefer ? "var executeModules = data[2];" : "",
  354. withPrefetch ? "var prefetchChunks = data[3] || [];" : "",
  355. '// add "moreModules" to the modules object,',
  356. '// then flag all "chunkIds" as loaded and fire callback',
  357. "var moduleId, chunkId, i = 0, resolves = [];",
  358. "for(;i < chunkIds.length; i++) {",
  359. Template.indent([
  360. "chunkId = chunkIds[i];",
  361. "if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {",
  362. Template.indent("resolves.push(installedChunks[chunkId][0]);"),
  363. "}",
  364. "installedChunks[chunkId] = 0;"
  365. ]),
  366. "}",
  367. "for(moduleId in moreModules) {",
  368. Template.indent([
  369. "if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {",
  370. Template.indent(
  371. mainTemplate.renderAddModule(
  372. hash,
  373. chunk,
  374. "moduleId",
  375. "moreModules[moduleId]"
  376. )
  377. ),
  378. "}"
  379. ]),
  380. "}",
  381. "if(parentJsonpFunction) parentJsonpFunction(data);",
  382. withPrefetch
  383. ? withDefer
  384. ? "deferredPrefetch.push.apply(deferredPrefetch, prefetchChunks);"
  385. : Template.asString([
  386. "// chunk prefetching for javascript",
  387. "prefetchChunks.forEach(function(chunkId) {",
  388. Template.indent([
  389. "if(installedChunks[chunkId] === undefined) {",
  390. Template.indent([
  391. "installedChunks[chunkId] = null;",
  392. mainTemplate.hooks.linkPrefetch.call("", chunk, hash),
  393. "document.head.appendChild(link);"
  394. ]),
  395. "}"
  396. ]),
  397. "});"
  398. ])
  399. : "",
  400. "while(resolves.length) {",
  401. Template.indent("resolves.shift()();"),
  402. "}",
  403. withDefer
  404. ? Template.asString([
  405. "",
  406. "// add entry modules from loaded chunk to deferred list",
  407. "deferredModules.push.apply(deferredModules, executeModules || []);",
  408. "",
  409. "// run deferred modules when all chunks ready",
  410. "return checkDeferredModules();"
  411. ])
  412. : ""
  413. ]),
  414. "};",
  415. withDefer
  416. ? Template.asString([
  417. "function checkDeferredModules() {",
  418. Template.indent([
  419. "var result;",
  420. "for(var i = 0; i < deferredModules.length; i++) {",
  421. Template.indent([
  422. "var deferredModule = deferredModules[i];",
  423. "var fulfilled = true;",
  424. "for(var j = 1; j < deferredModule.length; j++) {",
  425. Template.indent([
  426. "var depId = deferredModule[j];",
  427. "if(installedChunks[depId] !== 0) fulfilled = false;"
  428. ]),
  429. "}",
  430. "if(fulfilled) {",
  431. Template.indent([
  432. "deferredModules.splice(i--, 1);",
  433. "result = " +
  434. mainTemplate.requireFn +
  435. "(" +
  436. mainTemplate.requireFn +
  437. ".s = deferredModule[0]);"
  438. ]),
  439. "}"
  440. ]),
  441. "}",
  442. withPrefetch
  443. ? Template.asString([
  444. "if(deferredModules.length === 0) {",
  445. Template.indent([
  446. "// chunk prefetching for javascript",
  447. "deferredPrefetch.forEach(function(chunkId) {",
  448. Template.indent([
  449. "if(installedChunks[chunkId] === undefined) {",
  450. Template.indent([
  451. "installedChunks[chunkId] = null;",
  452. mainTemplate.hooks.linkPrefetch.call(
  453. "",
  454. chunk,
  455. hash
  456. ),
  457. "document.head.appendChild(link);"
  458. ]),
  459. "}"
  460. ]),
  461. "});",
  462. "deferredPrefetch.length = 0;"
  463. ]),
  464. "}"
  465. ])
  466. : "",
  467. "return result;"
  468. ]),
  469. "}"
  470. ])
  471. : ""
  472. ]);
  473. }
  474. return source;
  475. }
  476. );
  477. mainTemplate.hooks.beforeStartup.tap(
  478. "JsonpMainTemplatePlugin",
  479. (source, chunk, hash) => {
  480. if (needChunkLoadingCode(chunk)) {
  481. var jsonpFunction = mainTemplate.outputOptions.jsonpFunction;
  482. var globalObject = mainTemplate.outputOptions.globalObject;
  483. return Template.asString([
  484. `var jsonpArray = ${globalObject}[${JSON.stringify(
  485. jsonpFunction
  486. )}] = ${globalObject}[${JSON.stringify(jsonpFunction)}] || [];`,
  487. "var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);",
  488. "jsonpArray.push = webpackJsonpCallback;",
  489. "jsonpArray = jsonpArray.slice();",
  490. "for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);",
  491. "var parentJsonpFunction = oldJsonpFunction;",
  492. "",
  493. source
  494. ]);
  495. }
  496. return source;
  497. }
  498. );
  499. mainTemplate.hooks.afterStartup.tap(
  500. "JsonpMainTemplatePlugin",
  501. (source, chunk, hash) => {
  502. const prefetchChunks = chunk.getChildIdsByOrders().prefetch;
  503. if (
  504. needChunkLoadingCode(chunk) &&
  505. prefetchChunks &&
  506. prefetchChunks.length
  507. ) {
  508. return Template.asString([
  509. source,
  510. `webpackJsonpCallback([[], {}, 0, ${JSON.stringify(
  511. prefetchChunks
  512. )}]);`
  513. ]);
  514. }
  515. return source;
  516. }
  517. );
  518. mainTemplate.hooks.startup.tap(
  519. "JsonpMainTemplatePlugin",
  520. (source, chunk, hash) => {
  521. if (needEntryDeferringCode(chunk)) {
  522. if (chunk.hasEntryModule()) {
  523. const entries = [chunk.entryModule].filter(Boolean).map(m =>
  524. [m.id].concat(
  525. Array.from(chunk.groupsIterable)[0]
  526. .chunks.filter(c => c !== chunk)
  527. .map(c => c.id)
  528. )
  529. );
  530. return Template.asString([
  531. "// add entry module to deferred list",
  532. `deferredModules.push(${entries
  533. .map(e => JSON.stringify(e))
  534. .join(", ")});`,
  535. "// run deferred modules when ready",
  536. "return checkDeferredModules();"
  537. ]);
  538. } else {
  539. return Template.asString([
  540. "// run deferred modules from other chunks",
  541. "checkDeferredModules();"
  542. ]);
  543. }
  544. }
  545. return source;
  546. }
  547. );
  548. mainTemplate.hooks.hotBootstrap.tap(
  549. "JsonpMainTemplatePlugin",
  550. (source, chunk, hash) => {
  551. const globalObject = mainTemplate.outputOptions.globalObject;
  552. const hotUpdateChunkFilename =
  553. mainTemplate.outputOptions.hotUpdateChunkFilename;
  554. const hotUpdateMainFilename =
  555. mainTemplate.outputOptions.hotUpdateMainFilename;
  556. const crossOriginLoading =
  557. mainTemplate.outputOptions.crossOriginLoading;
  558. const hotUpdateFunction = mainTemplate.outputOptions.hotUpdateFunction;
  559. const currentHotUpdateChunkFilename = mainTemplate.getAssetPath(
  560. JSON.stringify(hotUpdateChunkFilename),
  561. {
  562. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  563. hashWithLength: length =>
  564. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
  565. chunk: {
  566. id: '" + chunkId + "'
  567. }
  568. }
  569. );
  570. const currentHotUpdateMainFilename = mainTemplate.getAssetPath(
  571. JSON.stringify(hotUpdateMainFilename),
  572. {
  573. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  574. hashWithLength: length =>
  575. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`
  576. }
  577. );
  578. const runtimeSource = Template.getFunctionContent(
  579. require("./JsonpMainTemplate.runtime")
  580. )
  581. .replace(/\/\/\$semicolon/g, ";")
  582. .replace(/\$require\$/g, mainTemplate.requireFn)
  583. .replace(
  584. /\$crossOriginLoading\$/g,
  585. crossOriginLoading ? JSON.stringify(crossOriginLoading) : "null"
  586. )
  587. .replace(/\$hotMainFilename\$/g, currentHotUpdateMainFilename)
  588. .replace(/\$hotChunkFilename\$/g, currentHotUpdateChunkFilename)
  589. .replace(/\$hash\$/g, JSON.stringify(hash));
  590. return `${source}
  591. function hotDisposeChunk(chunkId) {
  592. delete installedChunks[chunkId];
  593. }
  594. var parentHotUpdateCallback = ${globalObject}[${JSON.stringify(
  595. hotUpdateFunction
  596. )}];
  597. ${globalObject}[${JSON.stringify(hotUpdateFunction)}] = ${runtimeSource}`;
  598. }
  599. );
  600. mainTemplate.hooks.hash.tap("JsonpMainTemplatePlugin", hash => {
  601. hash.update("jsonp");
  602. hash.update("6");
  603. });
  604. }
  605. }
  606. module.exports = JsonpMainTemplatePlugin;