JsonpMainTemplatePlugin.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  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. mainTemplate.hooks.jsonpScript.call("", chunk, hash),
  278. "document.head.appendChild(script);"
  279. ]),
  280. "}"
  281. ]),
  282. "}"
  283. ]);
  284. }
  285. );
  286. mainTemplate.hooks.requireEnsure.tap(
  287. {
  288. name: "JsonpMainTemplatePlugin preload",
  289. stage: 10
  290. },
  291. (source, chunk, hash) => {
  292. const chunkMap = chunk.getChildIdsByOrdersMap().preload;
  293. if (!chunkMap || Object.keys(chunkMap).length === 0) return source;
  294. return Template.asString([
  295. source,
  296. "",
  297. "// chunk preloadng for javascript",
  298. "",
  299. `var chunkPreloadMap = ${JSON.stringify(chunkMap, null, "\t")};`,
  300. "",
  301. "var chunkPreloadData = chunkPreloadMap[chunkId];",
  302. "if(chunkPreloadData) {",
  303. Template.indent([
  304. "chunkPreloadData.forEach(function(chunkId) {",
  305. Template.indent([
  306. "if(installedChunks[chunkId] === undefined) {",
  307. Template.indent([
  308. "installedChunks[chunkId] = null;",
  309. mainTemplate.hooks.linkPreload.call("", chunk, hash),
  310. "document.head.appendChild(link);"
  311. ]),
  312. "}"
  313. ]),
  314. "});"
  315. ]),
  316. "}"
  317. ]);
  318. }
  319. );
  320. mainTemplate.hooks.requireExtensions.tap(
  321. "JsonpMainTemplatePlugin",
  322. (source, chunk) => {
  323. if (!needChunkOnDemandLoadingCode(chunk)) return source;
  324. return Template.asString([
  325. source,
  326. "",
  327. "// on error function for async loading",
  328. `${
  329. mainTemplate.requireFn
  330. }.oe = function(err) { console.error(err); throw err; };`
  331. ]);
  332. }
  333. );
  334. mainTemplate.hooks.bootstrap.tap(
  335. "JsonpMainTemplatePlugin",
  336. (source, chunk, hash) => {
  337. if (needChunkLoadingCode(chunk)) {
  338. const withDefer = needEntryDeferringCode(chunk);
  339. const withPrefetch = needPrefetchingCode(chunk);
  340. return Template.asString([
  341. source,
  342. "",
  343. "// install a JSONP callback for chunk loading",
  344. "function webpackJsonpCallback(data) {",
  345. Template.indent([
  346. "var chunkIds = data[0];",
  347. "var moreModules = data[1];",
  348. withDefer ? "var executeModules = data[2];" : "",
  349. withPrefetch ? "var prefetchChunks = data[3] || [];" : "",
  350. '// add "moreModules" to the modules object,',
  351. '// then flag all "chunkIds" as loaded and fire callback',
  352. "var moduleId, chunkId, i = 0, resolves = [];",
  353. "for(;i < chunkIds.length; i++) {",
  354. Template.indent([
  355. "chunkId = chunkIds[i];",
  356. "if(installedChunks[chunkId]) {",
  357. Template.indent("resolves.push(installedChunks[chunkId][0]);"),
  358. "}",
  359. "installedChunks[chunkId] = 0;"
  360. ]),
  361. "}",
  362. "for(moduleId in moreModules) {",
  363. Template.indent([
  364. "if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {",
  365. Template.indent(
  366. mainTemplate.renderAddModule(
  367. hash,
  368. chunk,
  369. "moduleId",
  370. "moreModules[moduleId]"
  371. )
  372. ),
  373. "}"
  374. ]),
  375. "}",
  376. "if(parentJsonpFunction) parentJsonpFunction(data);",
  377. withPrefetch
  378. ? Template.asString([
  379. "// chunk prefetching for javascript",
  380. "prefetchChunks.forEach(function(chunkId) {",
  381. Template.indent([
  382. "if(installedChunks[chunkId] === undefined) {",
  383. Template.indent([
  384. "installedChunks[chunkId] = null;",
  385. mainTemplate.hooks.linkPrefetch.call("", chunk, hash),
  386. "document.head.appendChild(link);"
  387. ]),
  388. "}"
  389. ]),
  390. "});"
  391. ])
  392. : "",
  393. "while(resolves.length) {",
  394. Template.indent("resolves.shift()();"),
  395. "}",
  396. withDefer
  397. ? Template.asString([
  398. "",
  399. "// add entry modules from loaded chunk to deferred list",
  400. "deferredModules.push.apply(deferredModules, executeModules || []);",
  401. "",
  402. "// run deferred modules when all chunks ready",
  403. "return checkDeferredModules();"
  404. ])
  405. : ""
  406. ]),
  407. "};",
  408. withDefer
  409. ? Template.asString([
  410. "function checkDeferredModules() {",
  411. Template.indent([
  412. "var result;",
  413. "for(var i = 0; i < deferredModules.length; i++) {",
  414. Template.indent([
  415. "var deferredModule = deferredModules[i];",
  416. "var fulfilled = true;",
  417. "for(var j = 1; j < deferredModule.length; j++) {",
  418. Template.indent([
  419. "var depId = deferredModule[j];",
  420. "if(installedChunks[depId] !== 0) fulfilled = false;"
  421. ]),
  422. "}",
  423. "if(fulfilled) {",
  424. Template.indent([
  425. "deferredModules.splice(i--, 1);",
  426. "result = " +
  427. mainTemplate.requireFn +
  428. "(" +
  429. mainTemplate.requireFn +
  430. ".s = deferredModule[0]);"
  431. ]),
  432. "}"
  433. ]),
  434. "}",
  435. "return result;"
  436. ]),
  437. "}"
  438. ])
  439. : ""
  440. ]);
  441. }
  442. return source;
  443. }
  444. );
  445. mainTemplate.hooks.beforeStartup.tap(
  446. "JsonpMainTemplatePlugin",
  447. (source, chunk, hash) => {
  448. if (needChunkLoadingCode(chunk)) {
  449. var jsonpFunction = mainTemplate.outputOptions.jsonpFunction;
  450. var globalObject = mainTemplate.outputOptions.globalObject;
  451. return Template.asString([
  452. `var jsonpArray = ${globalObject}[${JSON.stringify(
  453. jsonpFunction
  454. )}] = ${globalObject}[${JSON.stringify(jsonpFunction)}] || [];`,
  455. "var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);",
  456. "jsonpArray.push = webpackJsonpCallback;",
  457. "jsonpArray = jsonpArray.slice();",
  458. "for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);",
  459. "var parentJsonpFunction = oldJsonpFunction;",
  460. "",
  461. source
  462. ]);
  463. }
  464. return source;
  465. }
  466. );
  467. mainTemplate.hooks.beforeStartup.tap(
  468. "JsonpMainTemplatePlugin",
  469. (source, chunk, hash) => {
  470. const prefetchChunks = chunk.getChildIdsByOrders().prefetch;
  471. if (
  472. needChunkLoadingCode(chunk) &&
  473. prefetchChunks &&
  474. prefetchChunks.length
  475. ) {
  476. return Template.asString([
  477. source,
  478. `webpackJsonpCallback([[], {}, 0, ${JSON.stringify(
  479. prefetchChunks
  480. )}]);`
  481. ]);
  482. }
  483. return source;
  484. }
  485. );
  486. mainTemplate.hooks.startup.tap(
  487. "JsonpMainTemplatePlugin",
  488. (source, chunk, hash) => {
  489. if (needEntryDeferringCode(chunk)) {
  490. if (chunk.hasEntryModule()) {
  491. const entries = [chunk.entryModule].filter(Boolean).map(m =>
  492. [m.id].concat(
  493. Array.from(chunk.groupsIterable)[0]
  494. .chunks.filter(c => c !== chunk)
  495. .map(c => c.id)
  496. )
  497. );
  498. return Template.asString([
  499. "// add entry module to deferred list",
  500. `deferredModules.push(${entries
  501. .map(e => JSON.stringify(e))
  502. .join(", ")});`,
  503. "// run deferred modules when ready",
  504. "return checkDeferredModules();"
  505. ]);
  506. } else {
  507. return Template.asString([
  508. "// run deferred modules from other chunks",
  509. "checkDeferredModules();"
  510. ]);
  511. }
  512. }
  513. return source;
  514. }
  515. );
  516. mainTemplate.hooks.hotBootstrap.tap(
  517. "JsonpMainTemplatePlugin",
  518. (source, chunk, hash) => {
  519. const globalObject = mainTemplate.outputOptions.globalObject;
  520. const hotUpdateChunkFilename =
  521. mainTemplate.outputOptions.hotUpdateChunkFilename;
  522. const hotUpdateMainFilename =
  523. mainTemplate.outputOptions.hotUpdateMainFilename;
  524. const crossOriginLoading =
  525. mainTemplate.outputOptions.crossOriginLoading;
  526. const hotUpdateFunction = mainTemplate.outputOptions.hotUpdateFunction;
  527. const currentHotUpdateChunkFilename = mainTemplate.getAssetPath(
  528. JSON.stringify(hotUpdateChunkFilename),
  529. {
  530. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  531. hashWithLength: length =>
  532. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
  533. chunk: {
  534. id: '" + chunkId + "'
  535. }
  536. }
  537. );
  538. const currentHotUpdateMainFilename = mainTemplate.getAssetPath(
  539. JSON.stringify(hotUpdateMainFilename),
  540. {
  541. hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
  542. hashWithLength: length =>
  543. `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`
  544. }
  545. );
  546. const runtimeSource = Template.getFunctionContent(
  547. require("./JsonpMainTemplate.runtime")
  548. )
  549. .replace(/\/\/\$semicolon/g, ";")
  550. .replace(/\$require\$/g, mainTemplate.requireFn)
  551. .replace(
  552. /\$crossOriginLoading\$/g,
  553. crossOriginLoading ? JSON.stringify(crossOriginLoading) : "null"
  554. )
  555. .replace(/\$hotMainFilename\$/g, currentHotUpdateMainFilename)
  556. .replace(/\$hotChunkFilename\$/g, currentHotUpdateChunkFilename)
  557. .replace(/\$hash\$/g, JSON.stringify(hash));
  558. return `${source}
  559. function hotDisposeChunk(chunkId) {
  560. delete installedChunks[chunkId];
  561. }
  562. var parentHotUpdateCallback = ${globalObject}[${JSON.stringify(
  563. hotUpdateFunction
  564. )}];
  565. ${globalObject}[${JSON.stringify(hotUpdateFunction)}] = ${runtimeSource}`;
  566. }
  567. );
  568. mainTemplate.hooks.hash.tap("JsonpMainTemplatePlugin", hash => {
  569. hash.update("jsonp");
  570. hash.update("6");
  571. });
  572. }
  573. }
  574. module.exports = JsonpMainTemplatePlugin;