UNPKG

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