AssetModulesPlugin.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Yuta Hiroto @hiroppy
  4. */
  5. "use strict";
  6. const { RawSource } = require("webpack-sources");
  7. const HotUpdateChunk = require("../HotUpdateChunk");
  8. const {
  9. ASSET_URL_TYPE,
  10. JAVASCRIPT_TYPE
  11. } = require("../ModuleSourceTypeConstants");
  12. const {
  13. ASSET_MODULE_TYPE,
  14. ASSET_MODULE_TYPE_BYTES,
  15. ASSET_MODULE_TYPE_INLINE,
  16. ASSET_MODULE_TYPE_RESOURCE,
  17. ASSET_MODULE_TYPE_SOURCE,
  18. ASSET_MODULE_TYPE_WEBMANIFEST
  19. } = require("../ModuleTypeConstants");
  20. const { compareModulesByFullName } = require("../util/comparators");
  21. const createHash = require("../util/createHash");
  22. const { getUndoPath } = require("../util/identifier");
  23. const lazyModule = require("../util/lazyModule");
  24. const preloadModuleType = require("../util/preloadModuleType");
  25. const { PUBLIC_PATH_AUTO } = require("../util/publicPathPlaceholder");
  26. /** @import { Source } from "webpack-sources" */
  27. /** @import { Schema } from "schema-utils" */
  28. /**
  29. * @import {
  30. * AssetGeneratorDataUrl,
  31. * AssetModuleOutputPath,
  32. * RawPublicPath,
  33. * AssetModuleFilename
  34. * } from "../../declarations/WebpackOptions"
  35. */
  36. /**
  37. * @import {
  38. * AssetInfo,
  39. * ExecuteModuleObject,
  40. * WebpackRequire
  41. * } from "../Compilation"
  42. */
  43. /** @import Compiler from "../Compiler" */
  44. /** @import { AssetModuleBuildInfo } from "./AssetModule" */
  45. /** @import { CodeGenerationResult } from "../Module" */
  46. /** @import NormalModule from "../NormalModule" */
  47. /**
  48. * Returns definition.
  49. * @param {string} name name of definitions
  50. * @returns {Schema} definition
  51. */
  52. const getSchema = (name) => {
  53. const { definitions } =
  54. /** @type {EXPECTED_ANY} */
  55. (require("../../schemas/WebpackOptions.json"));
  56. return {
  57. definitions,
  58. oneOf: [{ $ref: `#/definitions/${name}` }]
  59. };
  60. };
  61. const generatorValidationOptions = {
  62. name: "Asset Modules Plugin",
  63. baseDataPath: "generator"
  64. };
  65. const getAssetGenerator = lazyModule(() => require("./AssetGenerator"));
  66. const getAssetParser = lazyModule(() => require("./AssetParser"));
  67. const getAssetSourceParser = lazyModule(() => require("./AssetSourceParser"));
  68. const getAssetBytesParser = lazyModule(() => require("./AssetBytesParser"));
  69. const getAssetSourceGenerator = lazyModule(() =>
  70. require("./AssetSourceGenerator")
  71. );
  72. const getAssetBytesGenerator = lazyModule(() =>
  73. require("./AssetBytesGenerator")
  74. );
  75. const getAssetModule = lazyModule(() => require("./AssetModule"));
  76. const getWebManifestParser = lazyModule(() => require("./WebManifestParser"));
  77. const getWebManifestGenerator = lazyModule(() =>
  78. require("./WebManifestGenerator")
  79. );
  80. const type = ASSET_MODULE_TYPE;
  81. // Source type produced by `WebManifestGenerator` (its emitted, rewritten JSON).
  82. const WEBMANIFEST_SOURCE_TYPE = "webmanifest";
  83. const PLUGIN_NAME = "AssetModulesPlugin";
  84. /**
  85. * Represents the asset modules plugin runtime component.
  86. * @typedef {object} AssetModulesPluginOptions
  87. * @property {boolean=} sideEffectFree
  88. */
  89. class AssetModulesPlugin {
  90. /**
  91. * Creates an instance of AssetModulesPlugin.
  92. * @param {AssetModulesPluginOptions} options options
  93. */
  94. constructor(options) {
  95. /** @type {AssetModulesPluginOptions} */
  96. this.options = options;
  97. }
  98. /**
  99. * Applies the plugin by registering its hooks on the compiler.
  100. * @param {Compiler} compiler the compiler instance
  101. * @returns {void}
  102. */
  103. apply(compiler) {
  104. compiler.hooks.compilation.tap(
  105. PLUGIN_NAME,
  106. (compilation, { normalModuleFactory }) => {
  107. preloadModuleType(normalModuleFactory, PLUGIN_NAME, [
  108. [
  109. ASSET_MODULE_TYPE,
  110. [getAssetModule, getAssetParser, getAssetGenerator]
  111. ],
  112. [
  113. ASSET_MODULE_TYPE_INLINE,
  114. [getAssetModule, getAssetParser, getAssetGenerator]
  115. ],
  116. [
  117. ASSET_MODULE_TYPE_RESOURCE,
  118. [getAssetModule, getAssetParser, getAssetGenerator]
  119. ],
  120. [
  121. ASSET_MODULE_TYPE_SOURCE,
  122. [getAssetModule, getAssetSourceParser, getAssetSourceGenerator]
  123. ],
  124. [
  125. ASSET_MODULE_TYPE_BYTES,
  126. [getAssetModule, getAssetBytesParser, getAssetBytesGenerator]
  127. ],
  128. [
  129. ASSET_MODULE_TYPE_WEBMANIFEST,
  130. [getWebManifestParser, getWebManifestGenerator]
  131. ]
  132. ]);
  133. for (const type of [
  134. ASSET_MODULE_TYPE,
  135. ASSET_MODULE_TYPE_BYTES,
  136. ASSET_MODULE_TYPE_INLINE,
  137. ASSET_MODULE_TYPE_RESOURCE,
  138. ASSET_MODULE_TYPE_SOURCE
  139. ]) {
  140. normalModuleFactory.hooks.createModuleClass
  141. .for(type)
  142. .tap(PLUGIN_NAME, (createData, _resolveData) => {
  143. const AssetModule = getAssetModule.loaded();
  144. return new AssetModule(createData, this.options.sideEffectFree);
  145. });
  146. }
  147. normalModuleFactory.hooks.createParser
  148. .for(ASSET_MODULE_TYPE)
  149. .tap(PLUGIN_NAME, (parserOptions) => {
  150. compiler.validate(
  151. () => getSchema("AssetParserOptions"),
  152. parserOptions,
  153. {
  154. name: "Asset Modules Plugin",
  155. baseDataPath: "parser"
  156. },
  157. (options) =>
  158. require("../../schemas/plugins/asset/AssetParserOptions.check")(
  159. options
  160. )
  161. );
  162. let dataUrlCondition = parserOptions.dataUrlCondition;
  163. if (!dataUrlCondition || typeof dataUrlCondition === "object") {
  164. dataUrlCondition = {
  165. maxSize: 8096,
  166. ...dataUrlCondition
  167. };
  168. }
  169. const AssetParser = getAssetParser.loaded();
  170. return new AssetParser(dataUrlCondition);
  171. });
  172. normalModuleFactory.hooks.createParser
  173. .for(ASSET_MODULE_TYPE_INLINE)
  174. .tap(PLUGIN_NAME, (_parserOptions) => {
  175. const AssetParser = getAssetParser.loaded();
  176. return new AssetParser(true);
  177. });
  178. normalModuleFactory.hooks.createParser
  179. .for(ASSET_MODULE_TYPE_RESOURCE)
  180. .tap(PLUGIN_NAME, (_parserOptions) => {
  181. const AssetParser = getAssetParser.loaded();
  182. return new AssetParser(false);
  183. });
  184. normalModuleFactory.hooks.createParser
  185. .for(ASSET_MODULE_TYPE_SOURCE)
  186. .tap(PLUGIN_NAME, (_parserOptions) => {
  187. const AssetSourceParser = getAssetSourceParser.loaded();
  188. return new AssetSourceParser();
  189. });
  190. normalModuleFactory.hooks.createParser
  191. .for(ASSET_MODULE_TYPE_BYTES)
  192. .tap(PLUGIN_NAME, (_parserOptions) => {
  193. const AssetBytesParser = getAssetBytesParser.loaded();
  194. return new AssetBytesParser();
  195. });
  196. for (const type of [
  197. ASSET_MODULE_TYPE,
  198. ASSET_MODULE_TYPE_INLINE,
  199. ASSET_MODULE_TYPE_RESOURCE
  200. ]) {
  201. normalModuleFactory.hooks.createGenerator
  202. .for(type)
  203. .tap(PLUGIN_NAME, (generatorOptions) => {
  204. switch (type) {
  205. case ASSET_MODULE_TYPE: {
  206. compiler.validate(
  207. () => getSchema("AssetGeneratorOptions"),
  208. generatorOptions,
  209. generatorValidationOptions,
  210. (options) =>
  211. require("../../schemas/plugins/asset/AssetGeneratorOptions.check")(
  212. options
  213. )
  214. );
  215. break;
  216. }
  217. case ASSET_MODULE_TYPE_RESOURCE: {
  218. compiler.validate(
  219. () => getSchema("AssetResourceGeneratorOptions"),
  220. generatorOptions,
  221. generatorValidationOptions,
  222. (options) =>
  223. require("../../schemas/plugins/asset/AssetResourceGeneratorOptions.check")(
  224. options
  225. )
  226. );
  227. break;
  228. }
  229. case ASSET_MODULE_TYPE_INLINE: {
  230. compiler.validate(
  231. () => getSchema("AssetInlineGeneratorOptions"),
  232. generatorOptions,
  233. generatorValidationOptions,
  234. (options) =>
  235. require("../../schemas/plugins/asset/AssetInlineGeneratorOptions.check")(
  236. options
  237. )
  238. );
  239. break;
  240. }
  241. }
  242. /** @type {undefined | AssetGeneratorDataUrl} */
  243. let dataUrl;
  244. if (type !== ASSET_MODULE_TYPE_RESOURCE) {
  245. dataUrl = generatorOptions.dataUrl;
  246. if (!dataUrl || typeof dataUrl === "object") {
  247. dataUrl = {
  248. encoding: undefined,
  249. mimetype: undefined,
  250. ...dataUrl
  251. };
  252. }
  253. }
  254. /** @type {undefined | AssetModuleFilename} */
  255. let filename;
  256. /** @type {undefined | RawPublicPath} */
  257. let publicPath;
  258. /** @type {undefined | AssetModuleOutputPath} */
  259. let outputPath;
  260. if (type !== ASSET_MODULE_TYPE_INLINE) {
  261. filename = generatorOptions.filename;
  262. publicPath = generatorOptions.publicPath;
  263. outputPath = generatorOptions.outputPath;
  264. }
  265. const AssetGenerator = getAssetGenerator.loaded();
  266. return new AssetGenerator(
  267. compilation.moduleGraph,
  268. dataUrl,
  269. filename,
  270. publicPath,
  271. outputPath,
  272. generatorOptions.emit !== false,
  273. compilation
  274. );
  275. });
  276. }
  277. normalModuleFactory.hooks.createGenerator
  278. .for(ASSET_MODULE_TYPE_SOURCE)
  279. .tap(PLUGIN_NAME, () => {
  280. const AssetSourceGenerator = getAssetSourceGenerator.loaded();
  281. return new AssetSourceGenerator(compilation.moduleGraph);
  282. });
  283. normalModuleFactory.hooks.createGenerator
  284. .for(ASSET_MODULE_TYPE_BYTES)
  285. .tap(PLUGIN_NAME, () => {
  286. const AssetBytesGenerator = getAssetBytesGenerator.loaded();
  287. return new AssetBytesGenerator(compilation.moduleGraph);
  288. });
  289. compilation.hooks.renderManifest.tap(PLUGIN_NAME, (result, options) => {
  290. const { chunkGraph } = compilation;
  291. const { chunk, codeGenerationResults, runtimeTemplate } = options;
  292. const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType(
  293. chunk,
  294. ASSET_MODULE_TYPE,
  295. compareModulesByFullName(compilation.compiler)
  296. );
  297. if (modules) {
  298. for (const module of modules) {
  299. try {
  300. const codeGenResult = codeGenerationResults.get(
  301. module,
  302. chunk.runtime
  303. );
  304. const buildInfo = /** @type {AssetModuleBuildInfo} */ (
  305. module.buildInfo
  306. );
  307. const data =
  308. /** @type {NonNullable<CodeGenerationResult["data"]>} */
  309. (codeGenResult.data);
  310. const errored = module.getNumberOfErrors() > 0;
  311. /** @type {string} */
  312. let entryFilename;
  313. /** @type {AssetInfo} */
  314. let entryInfo;
  315. /** @type {string} */
  316. let entryHash;
  317. if (errored) {
  318. const erroredModule = /** @type {NormalModule} */ (module);
  319. const AssetGenerator = getAssetGenerator.loaded();
  320. const [fullContentHash, contentHash] =
  321. AssetGenerator.getFullContentHash(
  322. erroredModule,
  323. runtimeTemplate
  324. );
  325. const { filename, assetInfo } =
  326. AssetGenerator.getFilenameWithInfo(
  327. erroredModule,
  328. {
  329. filename:
  330. erroredModule.generatorOptions &&
  331. erroredModule.generatorOptions.filename,
  332. outputPath:
  333. erroredModule.generatorOptions &&
  334. erroredModule.generatorOptions.outputPath
  335. },
  336. {
  337. runtime: chunk.runtime,
  338. runtimeTemplate,
  339. chunkGraph
  340. },
  341. contentHash,
  342. fullContentHash
  343. );
  344. entryFilename = filename;
  345. entryInfo = assetInfo;
  346. entryHash = fullContentHash;
  347. } else {
  348. entryFilename =
  349. /** @type {string} */
  350. (buildInfo.filename || data.get("filename"));
  351. entryInfo =
  352. /** @type {AssetInfo} */
  353. (buildInfo.assetInfo || data.get("assetInfo"));
  354. entryHash =
  355. /** @type {string} */
  356. (buildInfo.fullContentHash || data.get("fullContentHash"));
  357. }
  358. result.push({
  359. render: () =>
  360. /** @type {Source} */ (codeGenResult.sources.get(type)),
  361. filename: entryFilename,
  362. info: entryInfo,
  363. auxiliary: true,
  364. identifier: `assetModule${chunkGraph.getModuleId(module)}`,
  365. hash: entryHash
  366. });
  367. } catch (err) {
  368. /** @type {Error} */ (err).message +=
  369. `\nduring rendering of asset ${module.identifier()}`;
  370. throw err;
  371. }
  372. }
  373. }
  374. return result;
  375. });
  376. // A Web App Manifest (`asset/webmanifest`, gated on `experiments.html`)
  377. // parses/emits like an asset, but its icon URLs are rewritten in place
  378. // and the emitted file needs its own per-file public-path resolution.
  379. if (compilation.options.experiments.html) {
  380. normalModuleFactory.hooks.createParser
  381. .for(ASSET_MODULE_TYPE_WEBMANIFEST)
  382. .tap(PLUGIN_NAME, () => {
  383. const WebManifestParser = getWebManifestParser.loaded();
  384. return new WebManifestParser();
  385. });
  386. normalModuleFactory.hooks.createGenerator
  387. .for(ASSET_MODULE_TYPE_WEBMANIFEST)
  388. .tap(PLUGIN_NAME, () => {
  389. const WebManifestGenerator = getWebManifestGenerator.loaded();
  390. return new WebManifestGenerator(compilation.moduleGraph);
  391. });
  392. compilation.hooks.renderManifest.tap(
  393. PLUGIN_NAME,
  394. (result, options) => {
  395. const { chunk, codeGenerationResults } = options;
  396. if (chunk instanceof HotUpdateChunk) return result;
  397. const { chunkGraph, outputOptions } = compilation;
  398. const modules =
  399. chunkGraph.getOrderedChunkModulesIterableBySourceType(
  400. chunk,
  401. WEBMANIFEST_SOURCE_TYPE,
  402. compareModulesByFullName(compilation.compiler)
  403. );
  404. if (!modules) return result;
  405. for (const module of modules) {
  406. const codeGenResult = codeGenerationResults.get(
  407. module,
  408. chunk.runtime
  409. );
  410. const source = codeGenResult.sources.get(
  411. WEBMANIFEST_SOURCE_TYPE
  412. );
  413. if (!source) continue;
  414. const filename = /** @type {string} */ (
  415. codeGenResult.data && codeGenResult.data.get("filename")
  416. );
  417. if (!filename) continue;
  418. // Icon URLs resolve relative to the manifest's own location.
  419. const undoPath = getUndoPath(
  420. filename,
  421. /** @type {string} */ (outputOptions.path),
  422. false
  423. );
  424. const content = source
  425. .source()
  426. .toString()
  427. .split(PUBLIC_PATH_AUTO)
  428. .join(undoPath);
  429. const hash = createHash(outputOptions.hashFunction || "md4");
  430. hash.update(content);
  431. const contentHash = /** @type {string} */ (
  432. hash.digest(outputOptions.hashDigest || "hex")
  433. ).slice(0, outputOptions.hashDigestLength || 20);
  434. result.push({
  435. render: () => new RawSource(content),
  436. filename,
  437. info: { immutable: true },
  438. auxiliary: true,
  439. identifier: `webManifestModule${chunkGraph.getModuleId(
  440. module
  441. )}|${filename}`,
  442. hash: contentHash
  443. });
  444. }
  445. return result;
  446. }
  447. );
  448. }
  449. compilation.hooks.prepareModuleExecution.tap(
  450. PLUGIN_NAME,
  451. (options, context) => {
  452. const { codeGenerationResult } = options;
  453. const source = codeGenerationResult.sources.get(ASSET_MODULE_TYPE);
  454. if (source === undefined) return;
  455. const data =
  456. /** @type {NonNullable<CodeGenerationResult["data"]>} */
  457. (codeGenerationResult.data);
  458. context.assets.set(
  459. /** @type {string} */
  460. (data.get("filename")),
  461. {
  462. source,
  463. info: data.get("assetInfo")
  464. }
  465. );
  466. }
  467. );
  468. compilation.hooks.executeModule.tap(PLUGIN_NAME, (options, context) => {
  469. const { codeGenerationResult } = options;
  470. // A wrapper-less asset (`asset-url` only) would yield `{}` when
  471. // required at build time — emulate the wrapper's exports instead.
  472. if (!options.module.type.startsWith(ASSET_MODULE_TYPE)) return;
  473. if (codeGenerationResult.sources.has(JAVASCRIPT_TYPE)) return;
  474. const data = codeGenerationResult.data;
  475. if (!data) return;
  476. const moduleObject =
  477. /** @type {ExecuteModuleObject} */
  478. (options.moduleObject);
  479. // `__webpack_require__.p` honors the `importModule` publicPath
  480. // override (`URLDependency` requests it for wrapper-less assets).
  481. const publicPath =
  482. /** @type {WebpackRequire} */
  483. (context.__webpack_require__).p;
  484. const filename = data.get("filename");
  485. if (typeof filename === "string" && typeof publicPath === "string") {
  486. moduleObject.exports = publicPath + filename;
  487. return;
  488. }
  489. // data: uris (and assets without a runtime publicPath) resolve
  490. // chunk-independently — use the baked `asset-url`.
  491. const url = data.get("url");
  492. if (url && typeof url[ASSET_URL_TYPE] === "string") {
  493. moduleObject.exports = url[ASSET_URL_TYPE];
  494. }
  495. });
  496. }
  497. );
  498. }
  499. }
  500. module.exports = AssetModulesPlugin;