JsonpMainTemplatePlugin.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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) ? "var deferredModules = [];" : ""
  115. );
  116. }
  117. if (needChunkOnDemandLoadingCode(chunk)) {
  118. extraCode.push(
  119. "",
  120. "// script path function",
  121. "function jsonpScriptSrc(chunkId) {",
  122. Template.indent([
  123. `return ${mainTemplate.requireFn}.p + ${getScriptSrcPath(
  124. hash,
  125. chunk,
  126. "chunkId"
  127. )}`
  128. ]),
  129. "}"
  130. );
  131. }
  132. if (extraCode.length === 0) return source;
  133. return Template.asString([source, ...extraCode]);
  134. }
  135. );
  136. mainTemplate.hooks.jsonpScript.tap(
  137. "JsonpMainTemplatePlugin",
  138. (_, chunk, hash) => {
  139. const crossOriginLoading =
  140. mainTemplate.outputOptions.crossOriginLoading;
  141. const chunkLoadTimeout = mainTemplate.outputOptions.chunkLoadTimeout;
  142. const jsonpScriptType = mainTemplate.outputOptions.jsonpScriptType;
  143. return Template.asString([
  144. "var script = document.createElement('script');",
  145. "var onScriptComplete;",
  146. jsonpScriptType
  147. ? `script.type = ${JSON.stringify(jsonpScriptType)};`
  148. : "",
  149. "script.charset = 'utf-8';",
  150. `script.timeout = ${chunkLoadTimeout / 1000};`,
  151. `if (${mainTemplate.requireFn}.nc) {`,
  152. Template.indent(
  153. `script.setAttribute("nonce", ${mainTemplate.requireFn}.nc);`
  154. ),
  155. "}",
  156. "script.src = jsonpScriptSrc(chunkId);",
  157. crossOriginLoading
  158. ? Template.asString([
  159. "if (script.src.indexOf(window.location.origin + '/') !== 0) {",
  160. Template.indent(
  161. `script.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  162. ),
  163. "}"
  164. ])
  165. : "",
  166. "onScriptComplete = function (event) {",
  167. Template.indent([
  168. "// avoid mem leaks in IE.",
  169. "script.onerror = script.onload = null;",
  170. "clearTimeout(timeout);",
  171. "var chunk = installedChunks[chunkId];",
  172. "if(chunk !== 0) {",
  173. Template.indent([
  174. "if(chunk) {",
  175. Template.indent([
  176. "var errorType = event && (event.type === 'load' ? 'missing' : event.type);",
  177. "var realSrc = event && event.target && event.target.src;",
  178. "var error = new Error('Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')');",
  179. "error.type = errorType;",
  180. "error.request = realSrc;",
  181. "chunk[1](error);"
  182. ]),
  183. "}",
  184. "installedChunks[chunkId] = undefined;"
  185. ]),
  186. "}"
  187. ]),
  188. "};",
  189. "var timeout = setTimeout(function(){",
  190. Template.indent([
  191. "onScriptComplete({ type: 'timeout', target: script });"
  192. ]),
  193. `}, ${chunkLoadTimeout});`,
  194. "script.onerror = script.onload = onScriptComplete;"
  195. ]);
  196. }
  197. );
  198. mainTemplate.hooks.linkPreload.tap(
  199. "JsonpMainTemplatePlugin",
  200. (_, chunk, hash) => {
  201. const crossOriginLoading =
  202. mainTemplate.outputOptions.crossOriginLoading;
  203. const jsonpScriptType = mainTemplate.outputOptions.jsonpScriptType;
  204. return Template.asString([
  205. "var link = document.createElement('link');",
  206. jsonpScriptType
  207. ? `link.type = ${JSON.stringify(jsonpScriptType)};`
  208. : "",
  209. "link.charset = 'utf-8';",
  210. `if (${mainTemplate.requireFn}.nc) {`,
  211. Template.indent(
  212. `link.setAttribute("nonce", ${mainTemplate.requireFn}.nc);`
  213. ),
  214. "}",
  215. 'link.rel = "preload";',
  216. 'link.as = "script";',
  217. "link.href = jsonpScriptSrc(chunkId);",
  218. crossOriginLoading
  219. ? Template.asString([
  220. "if (link.href.indexOf(window.location.origin + '/') !== 0) {",
  221. Template.indent(
  222. `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  223. ),
  224. "}"
  225. ])
  226. : ""
  227. ]);
  228. }
  229. );
  230. mainTemplate.hooks.linkPrefetch.tap(
  231. "JsonpMainTemplatePlugin",
  232. (_, chunk, hash) => {
  233. const crossOriginLoading =
  234. mainTemplate.outputOptions.crossOriginLoading;
  235. return Template.asString([
  236. "var link = document.createElement('link');",
  237. crossOriginLoading
  238. ? `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  239. : "",
  240. `if (${mainTemplate.requireFn}.nc) {`,
  241. Template.indent(
  242. `link.setAttribute("nonce", ${mainTemplate.requireFn}.nc);`
  243. ),
  244. "}",
  245. 'link.rel = "prefetch";',
  246. 'link.as = "script";',
  247. "link.href = jsonpScriptSrc(chunkId);"
  248. ]);
  249. }
  250. );
  251. mainTemplate.hooks.requireEnsure.tap(
  252. "JsonpMainTemplatePlugin load",
  253. (source, chunk, hash) => {
  254. return Template.asString([
  255. source,
  256. "",
  257. "// JSONP chunk loading for javascript",
  258. "",
  259. "var installedChunkData = installedChunks[chunkId];",
  260. 'if(installedChunkData !== 0) { // 0 means "already installed".',
  261. Template.indent([
  262. "",
  263. '// a Promise means "currently loading".',
  264. "if(installedChunkData) {",
  265. Template.indent(["promises.push(installedChunkData[2]);"]),
  266. "} else {",
  267. Template.indent([
  268. "// setup Promise in chunk cache",
  269. "var promise = new Promise(function(resolve, reject) {",
  270. Template.indent([
  271. "installedChunkData = installedChunks[chunkId] = [resolve, reject];"
  272. ]),
  273. "});",
  274. "promises.push(installedChunkData[2] = promise);",
  275. "",
  276. "// start chunk loading",
  277. "var head = document.getElementsByTagName('head')[0];",
  278. mainTemplate.hooks.jsonpScript.call("", chunk, hash),
  279. "head.appendChild(script);"
  280. ]),
  281. "}"
  282. ]),
  283. "}"
  284. ]);
  285. }
  286. );
  287. mainTemplate.hooks.requireEnsure.tap(
  288. {
  289. name: "JsonpMainTemplatePlugin preload",
  290. stage: 10
  291. },
  292. (source, chunk, hash) => {
  293. const chunkMap = chunk.getChildIdsByOrdersMap().preload;
  294. if (!chunkMap || Object.keys(chunkMap).length === 0) return source;
  295. return Template.asString([
  296. source,
  297. "",
  298. "// chunk preloadng for javascript",
  299. "",
  300. `var chunkPreloadMap = ${JSON.stringify(chunkMap, null, "\t")};`,
  301. "",
  302. "var chunkPreloadData = chunkPreloadMap[chunkId];",
  303. "if(chunkPreloadData) {",
  304. Template.indent([
  305. "var head = document.getElementsByTagName('head')[0];",
  306. "chunkPreloadData.forEach(function(chunkId) {",
  307. Template.indent([
  308. "if(installedChunks[chunkId] === undefined) {",
  309. Template.indent([
  310. "installedChunks[chunkId] = null;",
  311. mainTemplate.hooks.linkPreload.call("", chunk, hash),
  312. "head.appendChild(link);"
  313. ]),
  314. "}"
  315. ]),
  316. "});"
  317. ]),
  318. "}"
  319. ]);
  320. }
  321. );
  322. mainTemplate.hooks.requireExtensions.tap(
  323. "JsonpMainTemplatePlugin",
  324. (source, chunk) => {
  325. if (!needChunkOnDemandLoadingCode(chunk)) return source;
  326. return Template.asString([
  327. source,
  328. "",
  329. "// on error function for async loading",
  330. `${
  331. mainTemplate.requireFn
  332. }.oe = function(err) { console.error(err); throw err; };`
  333. ]);
  334. }
  335. );
  336. mainTemplate.hooks.bootstrap.tap(
  337. "JsonpMainTemplatePlugin",
  338. (source, chunk, hash) => {
  339. if (needChunkLoadingCode(chunk)) {
  340. const withDefer = needEntryDeferringCode(chunk);
  341. const withPrefetch = needPrefetchingCode(chunk);
  342. return Template.asString([
  343. source,
  344. "",
  345. "// install a JSONP callback for chunk loading",
  346. "function webpackJsonpCallback(data) {",
  347. Template.indent([
  348. "var chunkIds = data[0];",
  349. "var moreModules = data[1];",
  350. withDefer ? "var executeModules = data[2];" : "",
  351. withPrefetch ? "var prefetchChunks = data[3] || [];" : "",
  352. '// add "moreModules" to the modules object,',
  353. '// then flag all "chunkIds" as loaded and fire callback',
  354. "var moduleId, chunkId, i = 0, resolves = [];",
  355. "for(;i < chunkIds.length; i++) {",
  356. Template.indent([
  357. "chunkId = chunkIds[i];",
  358. "if(installedChunks[chunkId]) {",
  359. Template.indent("resolves.push(installedChunks[chunkId][0]);"),
  360. "}",
  361. "installedChunks[chunkId] = 0;"
  362. ]),
  363. "}",
  364. "for(moduleId in moreModules) {",
  365. Template.indent([
  366. "if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {",
  367. Template.indent(
  368. mainTemplate.renderAddModule(
  369. hash,
  370. chunk,
  371. "moduleId",
  372. "moreModules[moduleId]"
  373. )
  374. ),
  375. "}"
  376. ]),
  377. "}",
  378. "if(parentJsonpFunction) parentJsonpFunction(data);",
  379. withPrefetch
  380. ? Template.asString([
  381. "// chunk prefetching for javascript",
  382. "var head = document.getElementsByTagName('head')[0];",
  383. "prefetchChunks.forEach(function(chunkId) {",
  384. Template.indent([
  385. "if(installedChunks[chunkId] === undefined) {",
  386. Template.indent([
  387. "installedChunks[chunkId] = null;",
  388. mainTemplate.hooks.linkPrefetch.call("", chunk, hash),
  389. "head.appendChild(link);"
  390. ]),
  391. "}"
  392. ]),
  393. "});"
  394. ])
  395. : "",
  396. "while(resolves.length) {",
  397. Template.indent("resolves.shift()();"),
  398. "}",
  399. withDefer
  400. ? Template.asString([
  401. "",
  402. "// add entry modules from loaded chunk to deferred list",
  403. "deferredModules.push.apply(deferredModules, executeModules || []);",
  404. "",
  405. "// run deferred modules when all chunks ready",
  406. "return checkDeferredModules();"
  407. ])
  408. : ""
  409. ]),
  410. "};",
  411. withDefer
  412. ? Template.asString([
  413. "function checkDeferredModules() {",
  414. Template.indent([
  415. "var result;",
  416. "for(var i = 0; i < deferredModules.length; i++) {",
  417. Template.indent([
  418. "var deferredModule = deferredModules[i];",
  419. "var fulfilled = true;",
  420. "for(var j = 1; j < deferredModule.length; j++) {",
  421. Template.indent([
  422. "var depId = deferredModule[j];",
  423. "if(installedChunks[depId] !== 0) fulfilled = false;"
  424. ]),
  425. "}",
  426. "if(fulfilled) {",
  427. Template.indent([
  428. "deferredModules.splice(i--, 1);",
  429. "result = " +
  430. mainTemplate.requireFn +
  431. "(" +
  432. mainTemplate.requireFn +
  433. ".s = deferredModule[0]);"
  434. ]),
  435. "}"
  436. ]),
  437. "}",
  438. "return result;"
  439. ]),
  440. "}"
  441. ])
  442. : ""
  443. ]);
  444. }
  445. return source;
  446. }
  447. );
  448. mainTemplate.hooks.beforeStartup.tap(
  449. "JsonpMainTemplatePlugin",
  450. (source, chunk, hash) => {
  451. if (needChunkLoadingCode(chunk)) {
  452. var jsonpFunction = mainTemplate.outputOptions.jsonpFunction;
  453. var globalObject = mainTemplate.outputOptions.globalObject;
  454. return Template.asString([
  455. `var jsonpArray = ${globalObject}[${JSON.stringify(
  456. jsonpFunction
  457. )}] = ${globalObject}[${JSON.stringify(jsonpFunction)}] || [];`,
  458. "var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);",
  459. "jsonpArray.push = webpackJsonpCallback;",
  460. "jsonpArray = jsonpArray.slice();",
  461. "for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);",
  462. "var parentJsonpFunction = oldJsonpFunction;",
  463. "",
  464. source
  465. ]);
  466. }
  467. return source;
  468. }
  469. );
  470. mainTemplate.hooks.beforeStartup.tap(
  471. "JsonpMainTemplatePlugin",
  472. (source, chunk, hash) => {
  473. const prefetchChunks = chunk.getChildIdsByOrders().prefetch;
  474. if (
  475. needChunkLoadingCode(chunk) &&
  476. prefetchChunks &&
  477. prefetchChunks.length
  478. ) {
  479. return Template.asString([
  480. source,
  481. `webpackJsonpCallback([[], {}, 0, ${JSON.stringify(
  482. prefetchChunks
  483. )}]);`
  484. ]);
  485. }
  486. return source;
  487. }
  488. );
  489. mainTemplate.hooks.startup.tap(
  490. "JsonpMainTemplatePlugin",
  491. (source, chunk, hash) => {
  492. if (needEntryDeferringCode(chunk)) {
  493. if (chunk.hasEntryModule()) {
  494. const entries = [chunk.entryModule].filter(Boolean).map(m =>
  495. [m.id].concat(
  496. Array.from(chunk.groupsIterable)[0]
  497. .chunks.filter(c => c !== chunk)
  498. .map(c => c.id)
  499. )
  500. );
  501. return Template.asString([
  502. "// add entry module to deferred list",
  503. `deferredModules.push(${entries
  504. .map(e => JSON.stringify(e))
  505. .join(", ")});`,
  506. "// run deferred modules when ready",
  507. "return checkDeferredModules();"
  508. ]);
  509. } else {
  510. return Template.asString([
  511. "// run deferred modules from other chunks",
  512. "checkDeferredModules();"
  513. ]);
  514. }
  515. }
  516. return source;
  517. }
  518. );
  519. mainTemplate.hooks.hotBootstrap.tap(
  520. "JsonpMainTemplatePlugin",
  521. (source, chunk, hash) => {
  522. const globalObject = mainTemplate.outputOptions.globalObject;
  523. const hotUpdateChunkFilename =
  524. mainTemplate.outputOptions.hotUpdateChunkFilename;
  525. const hotUpdateMainFilename =
  526. mainTemplate.outputOptions.hotUpdateMainFilename;
  527. const crossOriginLoading =
  528. mainTemplate.outputOptions.crossOriginLoading;
  529. const hotUpdateFunction = mainTemplate.outputOptions.hotUpdateFunction;
  530. const currentHotUpdateChunkFilename = mainTemplate.getAssetPath(
  531. JSON.stringify(hotUpdateChunkFilename),
  532. {
  533. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  534. hashWithLength: length =>
  535. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
  536. chunk: {
  537. id: '" + chunkId + "'
  538. }
  539. }
  540. );
  541. const currentHotUpdateMainFilename = mainTemplate.getAssetPath(
  542. JSON.stringify(hotUpdateMainFilename),
  543. {
  544. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  545. hashWithLength: length =>
  546. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`
  547. }
  548. );
  549. const runtimeSource = Template.getFunctionContent(
  550. require("./JsonpMainTemplate.runtime")
  551. )
  552. .replace(/\/\/\$semicolon/g, ";")
  553. .replace(/\$require\$/g, mainTemplate.requireFn)
  554. .replace(
  555. /\$crossOriginLoading\$/g,
  556. crossOriginLoading
  557. ? `script.crossOrigin = ${JSON.stringify(crossOriginLoading)}`
  558. : ""
  559. )
  560. .replace(/\$hotMainFilename\$/g, currentHotUpdateMainFilename)
  561. .replace(/\$hotChunkFilename\$/g, currentHotUpdateChunkFilename)
  562. .replace(/\$hash\$/g, JSON.stringify(hash));
  563. return `${source}
  564. function hotDisposeChunk(chunkId) {
  565. delete installedChunks[chunkId];
  566. }
  567. var parentHotUpdateCallback = ${globalObject}[${JSON.stringify(
  568. hotUpdateFunction
  569. )}];
  570. ${globalObject}[${JSON.stringify(hotUpdateFunction)}] = ${runtimeSource}`;
  571. }
  572. );
  573. mainTemplate.hooks.hash.tap("JsonpMainTemplatePlugin", hash => {
  574. hash.update("jsonp");
  575. hash.update("5");
  576. hash.update(`${mainTemplate.outputOptions.globalObject}`);
  577. hash.update(`${mainTemplate.outputOptions.chunkFilename}`);
  578. hash.update(`${mainTemplate.outputOptions.jsonpFunction}`);
  579. hash.update(`${mainTemplate.outputOptions.hotUpdateFunction}`);
  580. });
  581. }
  582. }
  583. module.exports = JsonpMainTemplatePlugin;