loader.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. "use strict";
  2. const path = require("path");
  3. const schema = require("./loader-options.json");
  4. const {
  5. ABSOLUTE_PUBLIC_PATH,
  6. AUTO_PUBLIC_PATH,
  7. BASE_URI,
  8. SINGLE_DOT_PATH_SEGMENT,
  9. evalModuleCode,
  10. findModuleById,
  11. stringifyLocal,
  12. stringifyRequest
  13. } = require("./utils");
  14. const MiniCssExtractPlugin = require("./index");
  15. /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
  16. /** @typedef {import("webpack").Compiler} Compiler */
  17. /** @typedef {import("webpack").Compilation} Compilation */
  18. /** @typedef {import("webpack").Chunk} Chunk */
  19. /** @typedef {import("webpack").Module} Module */
  20. /** @typedef {import("webpack").sources.Source} Source */
  21. /** @typedef {import("webpack").AssetInfo} AssetInfo */
  22. /** @typedef {import("webpack").NormalModule} NormalModule */
  23. /** @typedef {import("./index.js").LoaderOptions} LoaderOptions */
  24. // eslint-disable-next-line jsdoc/no-restricted-syntax
  25. /** @typedef {{[key: string]: string | Function }} Locals */
  26. // eslint-disable-next-line jsdoc/no-restricted-syntax
  27. /** @typedef {any} EXPECTED_ANY */
  28. /**
  29. * @typedef {object} Dependency
  30. * @property {string} identifier identifier
  31. * @property {string | null} context context
  32. * @property {Buffer} content content
  33. * @property {string=} media media
  34. * @property {string=} supports supports
  35. * @property {string=} layer layer
  36. * @property {Buffer=} sourceMap source map
  37. */
  38. /**
  39. * @param {string} code code
  40. * @param {{ loaderContext: import("webpack").LoaderContext<LoaderOptions>, options: LoaderOptions, locals: Locals | undefined }} context context
  41. * @returns {string} code and HMR code
  42. */
  43. function hotLoader(code, context) {
  44. const localsJsonString = JSON.stringify(JSON.stringify(context.locals));
  45. return `${code}
  46. if(module.hot) {
  47. (function() {
  48. var localsJsonString = ${localsJsonString};
  49. // ${Date.now()}
  50. var cssReload = require(${stringifyRequest(context.loaderContext, path.join(__dirname, "hmr/hotModuleReplacement.js"))})(module.id, ${JSON.stringify(context.options)});
  51. // only invalidate when locals change
  52. if (
  53. module.hot.data &&
  54. module.hot.data.value &&
  55. module.hot.data.value !== localsJsonString
  56. ) {
  57. module.hot.invalidate();
  58. } else {
  59. module.hot.accept();
  60. }
  61. module.hot.dispose(function(data) {
  62. data.value = localsJsonString;
  63. cssReload();
  64. });
  65. })();
  66. }
  67. `;
  68. }
  69. /**
  70. * @this {import("webpack").LoaderContext<LoaderOptions>}
  71. * @param {string} request request
  72. */
  73. function pitch(request) {
  74. if (this._compiler && this._compiler.options && this._compiler.options.experiments && this._compiler.options.experiments.css && this._module && (this._module.type === "css" || this._module.type === "css/auto" || this._module.type === "css/global" || this._module.type === "css/module")) {
  75. this.emitWarning(new Error('You can\'t use `experiments.css` (`experiments.futureDefaults` enable built-in CSS support by default) and `mini-css-extract-plugin` together, please set `experiments.css` to `false` or set `{ type: "javascript/auto" }` for rules with `mini-css-extract-plugin` in your webpack config (now `mini-css-extract-plugin` does nothing).'));
  76. return;
  77. }
  78. const options = this.getOptions(/** @type {Schema} */schema);
  79. const emit = typeof options.emit !== "undefined" ? options.emit : true;
  80. const callback = this.async();
  81. const optionsFromPlugin =
  82. // @ts-expect-error
  83. this[MiniCssExtractPlugin.pluginSymbol];
  84. if (!optionsFromPlugin) {
  85. callback(new Error("You forgot to add 'mini-css-extract-plugin' plugin (i.e. `{ plugins: [new MiniCssExtractPlugin()] }`), please read https://github.com/webpack-contrib/mini-css-extract-plugin#getting-started"));
  86. return;
  87. }
  88. const {
  89. webpack
  90. } = /** @type {Compiler} */this._compiler;
  91. /**
  92. * @param {EXPECTED_ANY} originalExports original exports
  93. * @param {Compilation=} compilation compilation
  94. * @param {{ [name: string]: Source }=} assets assets
  95. * @param {Map<string, AssetInfo>=} assetsInfo assets info
  96. * @returns {void}
  97. */
  98. const handleExports = (originalExports, compilation, assets, assetsInfo) => {
  99. /** @type {Locals | undefined} */
  100. let locals;
  101. let namedExport;
  102. const esModule = typeof options.esModule !== "undefined" ? options.esModule : true;
  103. /**
  104. * @param {Dependency[] | [null, object][]} dependencies dependencies
  105. */
  106. const addDependencies = dependencies => {
  107. // eslint-disable-next-line no-eq-null, eqeqeq
  108. if (!Array.isArray(dependencies) && dependencies != null) {
  109. throw new Error(`Exported value was not extracted as an array: ${JSON.stringify(dependencies)}`);
  110. }
  111. const identifierCountMap = new Map();
  112. let lastDep;
  113. for (const dependency of dependencies) {
  114. if (!(/** @type {Dependency} */dependency.identifier) || !emit) {
  115. continue;
  116. }
  117. const count = identifierCountMap.get(/** @type {Dependency} */dependency.identifier) || 0;
  118. const CssDependency = MiniCssExtractPlugin.getCssDependency(webpack);
  119. /** @type {NormalModule} */
  120. this._module.addDependency(lastDep = new CssDependency(/** @type {Dependency} */
  121. dependency, /** @type {Dependency} */
  122. dependency.context, count));
  123. identifierCountMap.set(/** @type {Dependency} */
  124. dependency.identifier, count + 1);
  125. }
  126. if (lastDep && assets) {
  127. lastDep.assets = assets;
  128. lastDep.assetsInfo = assetsInfo;
  129. }
  130. };
  131. try {
  132. const exports = originalExports.__esModule ? originalExports.default : originalExports;
  133. namedExport = originalExports.__esModule && (!originalExports.default || !("locals" in originalExports.default));
  134. if (namedExport) {
  135. for (const key of Object.keys(originalExports)) {
  136. if (key !== "default") {
  137. if (!locals) {
  138. locals = {};
  139. }
  140. /** @type {Locals} */
  141. locals[key] = originalExports[key];
  142. }
  143. }
  144. } else {
  145. locals = exports && exports.locals;
  146. }
  147. /** @type {Dependency[] | [null, Record<string, string>][]} */
  148. let dependencies;
  149. if (!Array.isArray(exports)) {
  150. dependencies = [[null, exports]];
  151. } else {
  152. dependencies = exports.map(([id, content, media, sourceMap, supports, layer]) => {
  153. let identifier = id;
  154. let context;
  155. if (compilation) {
  156. const module = /** @type {Module} */
  157. findModuleById(compilation, id);
  158. identifier = module.identifier();
  159. ({
  160. context
  161. } = module);
  162. } else {
  163. // TODO check if this context is used somewhere
  164. context = this.rootContext;
  165. }
  166. return {
  167. identifier,
  168. context,
  169. content: Buffer.from(content),
  170. media,
  171. supports,
  172. layer,
  173. sourceMap: sourceMap ? Buffer.from(JSON.stringify(sourceMap)) : undefined
  174. };
  175. });
  176. }
  177. addDependencies(dependencies);
  178. } catch (err) {
  179. callback(/** @type {Error} */err);
  180. return;
  181. }
  182. const result = function makeResult() {
  183. const defaultExport = typeof options.defaultExport !== "undefined" ? options.defaultExport : false;
  184. if (locals) {
  185. if (namedExport) {
  186. const identifiers = [...function* generateIdentifiers() {
  187. let identifierId = 0;
  188. for (const key of Object.keys(locals)) {
  189. identifierId += 1;
  190. yield [`_${identifierId.toString(16)}`, key];
  191. }
  192. }()];
  193. const localsString = identifiers.map(([id, key]) => `\nvar ${id} = ${stringifyLocal(/** @type {Locals} */locals[key])};`).join("");
  194. const exportsString = `export { ${identifiers.map(([id, key]) => `${id} as ${JSON.stringify(key)}`).join(", ")} }`;
  195. return defaultExport ? `${localsString}\n${exportsString}\nexport default { ${identifiers.map(([id, key]) => `${JSON.stringify(key)}: ${id}`).join(", ")} }\n` : `${localsString}\n${exportsString}\n`;
  196. }
  197. return `\n${esModule ? "export default" : "module.exports = "} ${JSON.stringify(locals)};`;
  198. } else if (esModule) {
  199. return defaultExport ? "\nexport {};export default {};" : "\nexport {};";
  200. }
  201. return "";
  202. }();
  203. let resultSource = `// extracted by ${MiniCssExtractPlugin.pluginName}`;
  204. // only attempt hotreloading if the css is actually used for something other than hash values
  205. resultSource += this.hot && emit ? hotLoader(result, {
  206. loaderContext: this,
  207. options,
  208. locals
  209. }) : result;
  210. callback(null, resultSource);
  211. };
  212. let {
  213. publicPath
  214. } = /** @type {Compilation} */
  215. this._compilation.outputOptions;
  216. if (typeof options.publicPath === "string") {
  217. publicPath = options.publicPath;
  218. } else if (typeof options.publicPath === "function") {
  219. publicPath = options.publicPath(this.resourcePath, this.rootContext);
  220. }
  221. if (publicPath === "auto") {
  222. publicPath = AUTO_PUBLIC_PATH;
  223. }
  224. if (typeof optionsFromPlugin.experimentalUseImportModule === "undefined" && typeof this.importModule === "function" || optionsFromPlugin.experimentalUseImportModule) {
  225. if (!this.importModule) {
  226. callback(new Error("You are using 'experimentalUseImportModule' but 'this.importModule' is not available in loader context. You need to have at least webpack 5.33.2."));
  227. return;
  228. }
  229. let publicPathForExtract;
  230. if (typeof publicPath === "string") {
  231. const isAbsolutePublicPath = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/.test(publicPath);
  232. publicPathForExtract = isAbsolutePublicPath ? publicPath : `${ABSOLUTE_PUBLIC_PATH}${publicPath.replace(/\./g, SINGLE_DOT_PATH_SEGMENT)}`;
  233. } else {
  234. publicPathForExtract = publicPath;
  235. }
  236. this.importModule(`${this.resourcePath}.webpack[javascript/auto]!=!!!${request}`, {
  237. layer: options.layer,
  238. publicPath: (/** @type {string} */publicPathForExtract),
  239. baseUri: `${BASE_URI}/`
  240. },
  241. /**
  242. * @param {Error | null | undefined} error error
  243. * @param {object} exports exports
  244. */
  245. (error, exports) => {
  246. if (error) {
  247. callback(error);
  248. return;
  249. }
  250. handleExports(exports);
  251. });
  252. return;
  253. }
  254. const loaders = this.loaders.slice(this.loaderIndex + 1);
  255. this.addDependency(this.resourcePath);
  256. const childFilename = "*";
  257. const outputOptions = {
  258. filename: childFilename,
  259. publicPath
  260. };
  261. const childCompiler = /** @type {Compilation} */
  262. this._compilation.createChildCompiler(`${MiniCssExtractPlugin.pluginName} ${request}`, outputOptions);
  263. // The templates are compiled and executed by NodeJS - similar to server side rendering
  264. // Unfortunately this causes issues as some loaders require an absolute URL to support ES Modules
  265. // The following config enables relative URL support for the child compiler
  266. childCompiler.options.module = {
  267. ...childCompiler.options.module
  268. };
  269. childCompiler.options.module.parser = {
  270. ...childCompiler.options.module.parser
  271. };
  272. childCompiler.options.module.parser.javascript = {
  273. ...childCompiler.options.module.parser.javascript,
  274. url: "relative"
  275. };
  276. const {
  277. NodeTemplatePlugin
  278. } = webpack.node;
  279. const {
  280. NodeTargetPlugin
  281. } = webpack.node;
  282. new NodeTemplatePlugin().apply(childCompiler);
  283. new NodeTargetPlugin().apply(childCompiler);
  284. const {
  285. EntryOptionPlugin
  286. } = webpack;
  287. const {
  288. library: {
  289. EnableLibraryPlugin
  290. }
  291. } = webpack;
  292. new EnableLibraryPlugin("commonjs2").apply(childCompiler);
  293. EntryOptionPlugin.applyEntryOption(childCompiler, this.context, {
  294. child: {
  295. library: {
  296. type: "commonjs2"
  297. },
  298. import: [`!!${request}`]
  299. }
  300. });
  301. const {
  302. LimitChunkCountPlugin
  303. } = webpack.optimize;
  304. new LimitChunkCountPlugin({
  305. maxChunks: 1
  306. }).apply(childCompiler);
  307. const {
  308. NormalModule
  309. } = webpack;
  310. childCompiler.hooks.thisCompilation.tap(`${MiniCssExtractPlugin.pluginName} loader`,
  311. /**
  312. * @param {Compilation} compilation compilation
  313. */
  314. compilation => {
  315. const normalModuleHook = NormalModule.getCompilationHooks(compilation).loader;
  316. normalModuleHook.tap(`${MiniCssExtractPlugin.pluginName} loader`, (loaderContext, module) => {
  317. if (module.request === request) {
  318. module.loaders = loaders.map(loader => ({
  319. type: null,
  320. loader: loader.path,
  321. options: loader.options,
  322. ident: loader.ident
  323. }));
  324. }
  325. });
  326. });
  327. /** @type {string | Buffer} */
  328. let source;
  329. childCompiler.hooks.compilation.tap(MiniCssExtractPlugin.pluginName,
  330. /**
  331. * @param {Compilation} compilation compilation
  332. */
  333. compilation => {
  334. compilation.hooks.processAssets.tap(MiniCssExtractPlugin.pluginName, () => {
  335. source = compilation.assets[childFilename] && compilation.assets[childFilename].source();
  336. // Remove all chunk assets
  337. for (const chunk of compilation.chunks) {
  338. for (const file of chunk.files) {
  339. compilation.deleteAsset(file);
  340. }
  341. }
  342. });
  343. });
  344. childCompiler.runAsChild((error, entries, compilation_) => {
  345. if (error) {
  346. callback(error);
  347. return;
  348. }
  349. const compilation = /** @type {Compilation} */compilation_;
  350. if (compilation.errors.length > 0) {
  351. callback(compilation.errors[0]);
  352. return;
  353. }
  354. /** @type {{ [name: string]: Source }} */
  355. const assets = Object.create(null);
  356. /** @type {Map<string, AssetInfo>} */
  357. const assetsInfo = new Map();
  358. for (const asset of compilation.getAssets()) {
  359. assets[asset.name] = asset.source;
  360. assetsInfo.set(asset.name, asset.info);
  361. }
  362. for (const dep of compilation.fileDependencies) {
  363. this.addDependency(dep);
  364. }
  365. for (const dep of compilation.contextDependencies) {
  366. this.addContextDependency(dep);
  367. }
  368. if (!source) {
  369. callback(new Error("Didn't get a result from child compiler"));
  370. return;
  371. }
  372. let originalExports;
  373. try {
  374. originalExports = evalModuleCode(this, source, request);
  375. } catch (err) {
  376. callback(/** @type {Error} */err);
  377. return;
  378. }
  379. handleExports(originalExports, compilation, assets, assetsInfo);
  380. });
  381. }
  382. /**
  383. * @this {import("webpack").LoaderContext<LoaderOptions>}
  384. * @param {string} content content
  385. * @returns {string | undefined} the original content
  386. */
  387. function loader(content) {
  388. if (this._compiler && this._compiler.options && this._compiler.options.experiments && this._compiler.options.experiments.css && this._module && (this._module.type === "css" || this._module.type === "css/auto" || this._module.type === "css/global" || this._module.type === "css/module")) {
  389. return content;
  390. }
  391. }
  392. module.exports = loader;
  393. module.exports.hotLoader = hotLoader;
  394. module.exports.pitch = pitch;