UNPKG

13.7 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const { ConcatSource } = require("webpack-sources");
9const HotUpdateChunk = require("../HotUpdateChunk");
10const RuntimeGlobals = require("../RuntimeGlobals");
11const SelfModuleFactory = require("../SelfModuleFactory");
12const CssExportDependency = require("../dependencies/CssExportDependency");
13const CssImportDependency = require("../dependencies/CssImportDependency");
14const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
15const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
16const CssUrlDependency = require("../dependencies/CssUrlDependency");
17const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
18const { compareModulesByIdentifier } = require("../util/comparators");
19const createSchemaValidation = require("../util/create-schema-validation");
20const createHash = require("../util/createHash");
21const memoize = require("../util/memoize");
22const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
23const CssExportsGenerator = require("./CssExportsGenerator");
24const CssGenerator = require("./CssGenerator");
25const CssParser = require("./CssParser");
26
27/** @typedef {import("webpack-sources").Source} Source */
28/** @typedef {import("../../declarations/WebpackOptions").CssExperimentOptions} CssExperimentOptions */
29/** @typedef {import("../Chunk")} Chunk */
30/** @typedef {import("../Compiler")} Compiler */
31/** @typedef {import("../Module")} Module */
32
33const getCssLoadingRuntimeModule = memoize(() =>
34 require("./CssLoadingRuntimeModule")
35);
36
37const getSchema = name => {
38 const { definitions } = require("../../schemas/WebpackOptions.json");
39 return {
40 definitions,
41 oneOf: [{ $ref: `#/definitions/${name}` }]
42 };
43};
44
45const validateGeneratorOptions = createSchemaValidation(
46 require("../../schemas/plugins/css/CssGeneratorOptions.check.js"),
47 () => getSchema("CssGeneratorOptions"),
48 {
49 name: "Css Modules Plugin",
50 baseDataPath: "parser"
51 }
52);
53const validateParserOptions = createSchemaValidation(
54 require("../../schemas/plugins/css/CssParserOptions.check.js"),
55 () => getSchema("CssParserOptions"),
56 {
57 name: "Css Modules Plugin",
58 baseDataPath: "parser"
59 }
60);
61
62const escapeCss = (str, omitOptionalUnderscore) => {
63 const escaped = `${str}`.replace(
64 // cspell:word uffff
65 /[^a-zA-Z0-9_\u0081-\uffff-]/g,
66 s => `\\${s}`
67 );
68 return !omitOptionalUnderscore && /^(?!--)[0-9_-]/.test(escaped)
69 ? `_${escaped}`
70 : escaped;
71};
72
73const plugin = "CssModulesPlugin";
74
75class CssModulesPlugin {
76 /**
77 * @param {CssExperimentOptions} options options
78 */
79 constructor({ exportsOnly = false }) {
80 this._exportsOnly = exportsOnly;
81 }
82 /**
83 * Apply the plugin
84 * @param {Compiler} compiler the compiler instance
85 * @returns {void}
86 */
87 apply(compiler) {
88 compiler.hooks.compilation.tap(
89 plugin,
90 (compilation, { normalModuleFactory }) => {
91 const selfFactory = new SelfModuleFactory(compilation.moduleGraph);
92 compilation.dependencyFactories.set(
93 CssUrlDependency,
94 normalModuleFactory
95 );
96 compilation.dependencyTemplates.set(
97 CssUrlDependency,
98 new CssUrlDependency.Template()
99 );
100 compilation.dependencyTemplates.set(
101 CssLocalIdentifierDependency,
102 new CssLocalIdentifierDependency.Template()
103 );
104 compilation.dependencyFactories.set(
105 CssSelfLocalIdentifierDependency,
106 selfFactory
107 );
108 compilation.dependencyTemplates.set(
109 CssSelfLocalIdentifierDependency,
110 new CssSelfLocalIdentifierDependency.Template()
111 );
112 compilation.dependencyTemplates.set(
113 CssExportDependency,
114 new CssExportDependency.Template()
115 );
116 compilation.dependencyFactories.set(
117 CssImportDependency,
118 normalModuleFactory
119 );
120 compilation.dependencyTemplates.set(
121 CssImportDependency,
122 new CssImportDependency.Template()
123 );
124 compilation.dependencyTemplates.set(
125 StaticExportsDependency,
126 new StaticExportsDependency.Template()
127 );
128 normalModuleFactory.hooks.createParser
129 .for("css")
130 .tap(plugin, parserOptions => {
131 validateParserOptions(parserOptions);
132 return new CssParser();
133 });
134 normalModuleFactory.hooks.createParser
135 .for("css/global")
136 .tap(plugin, parserOptions => {
137 validateParserOptions(parserOptions);
138 return new CssParser({
139 allowPseudoBlocks: false,
140 allowModeSwitch: false
141 });
142 });
143 normalModuleFactory.hooks.createParser
144 .for("css/module")
145 .tap(plugin, parserOptions => {
146 validateParserOptions(parserOptions);
147 return new CssParser({
148 defaultMode: "local"
149 });
150 });
151 normalModuleFactory.hooks.createGenerator
152 .for("css")
153 .tap(plugin, generatorOptions => {
154 validateGeneratorOptions(generatorOptions);
155 return this._exportsOnly
156 ? new CssExportsGenerator()
157 : new CssGenerator();
158 });
159 normalModuleFactory.hooks.createGenerator
160 .for("css/global")
161 .tap(plugin, generatorOptions => {
162 validateGeneratorOptions(generatorOptions);
163 return this._exportsOnly
164 ? new CssExportsGenerator()
165 : new CssGenerator();
166 });
167 normalModuleFactory.hooks.createGenerator
168 .for("css/module")
169 .tap(plugin, generatorOptions => {
170 validateGeneratorOptions(generatorOptions);
171 return this._exportsOnly
172 ? new CssExportsGenerator()
173 : new CssGenerator();
174 });
175 const orderedCssModulesPerChunk = new WeakMap();
176 compilation.hooks.afterCodeGeneration.tap("CssModulesPlugin", () => {
177 const { chunkGraph } = compilation;
178 for (const chunk of compilation.chunks) {
179 if (CssModulesPlugin.chunkHasCss(chunk, chunkGraph)) {
180 orderedCssModulesPerChunk.set(
181 chunk,
182 this.getOrderedChunkCssModules(chunk, chunkGraph, compilation)
183 );
184 }
185 }
186 });
187 compilation.hooks.contentHash.tap("CssModulesPlugin", chunk => {
188 const {
189 chunkGraph,
190 outputOptions: {
191 hashSalt,
192 hashDigest,
193 hashDigestLength,
194 hashFunction
195 }
196 } = compilation;
197 const modules = orderedCssModulesPerChunk.get(chunk);
198 if (modules === undefined) return;
199 const hash = createHash(hashFunction);
200 if (hashSalt) hash.update(hashSalt);
201 for (const module of modules) {
202 hash.update(chunkGraph.getModuleHash(module, chunk.runtime));
203 }
204 const digest = /** @type {string} */ (hash.digest(hashDigest));
205 chunk.contentHash.css = nonNumericOnlyHash(digest, hashDigestLength);
206 });
207 compilation.hooks.renderManifest.tap(plugin, (result, options) => {
208 const { chunkGraph } = compilation;
209 const { hash, chunk, codeGenerationResults } = options;
210
211 if (chunk instanceof HotUpdateChunk) return result;
212
213 const modules = orderedCssModulesPerChunk.get(chunk);
214 if (modules !== undefined) {
215 result.push({
216 render: () =>
217 this.renderChunk({
218 chunk,
219 chunkGraph,
220 codeGenerationResults,
221 uniqueName: compilation.outputOptions.uniqueName,
222 modules
223 }),
224 filenameTemplate: CssModulesPlugin.getChunkFilenameTemplate(
225 chunk,
226 compilation.outputOptions
227 ),
228 pathOptions: {
229 hash,
230 runtime: chunk.runtime,
231 chunk,
232 contentHashType: "css"
233 },
234 identifier: `css${chunk.id}`,
235 hash: chunk.contentHash.css
236 });
237 }
238 return result;
239 });
240 const enabledChunks = new WeakSet();
241 const handler = (chunk, set) => {
242 if (enabledChunks.has(chunk)) {
243 return;
244 }
245 enabledChunks.add(chunk);
246
247 set.add(RuntimeGlobals.publicPath);
248 set.add(RuntimeGlobals.getChunkCssFilename);
249 set.add(RuntimeGlobals.hasOwnProperty);
250 set.add(RuntimeGlobals.moduleFactoriesAddOnly);
251 set.add(RuntimeGlobals.makeNamespaceObject);
252
253 const CssLoadingRuntimeModule = getCssLoadingRuntimeModule();
254 compilation.addRuntimeModule(chunk, new CssLoadingRuntimeModule(set));
255 };
256 compilation.hooks.runtimeRequirementInTree
257 .for(RuntimeGlobals.hasCssModules)
258 .tap(plugin, handler);
259 compilation.hooks.runtimeRequirementInTree
260 .for(RuntimeGlobals.ensureChunkHandlers)
261 .tap(plugin, handler);
262 compilation.hooks.runtimeRequirementInTree
263 .for(RuntimeGlobals.hmrDownloadUpdateHandlers)
264 .tap(plugin, handler);
265 }
266 );
267 }
268
269 getModulesInOrder(chunk, modules, compilation) {
270 if (!modules) return [];
271
272 const modulesList = [...modules];
273
274 // Get ordered list of modules per chunk group
275 // Lists are in reverse order to allow to use Array.pop()
276 const modulesByChunkGroup = Array.from(chunk.groupsIterable, chunkGroup => {
277 const sortedModules = modulesList
278 .map(module => {
279 return {
280 module,
281 index: chunkGroup.getModulePostOrderIndex(module)
282 };
283 })
284 .filter(item => item.index !== undefined)
285 .sort((a, b) => b.index - a.index)
286 .map(item => item.module);
287
288 return { list: sortedModules, set: new Set(sortedModules) };
289 });
290
291 if (modulesByChunkGroup.length === 1)
292 return modulesByChunkGroup[0].list.reverse();
293
294 const compareModuleLists = ({ list: a }, { list: b }) => {
295 if (a.length === 0) {
296 return b.length === 0 ? 0 : 1;
297 } else {
298 if (b.length === 0) return -1;
299 return compareModulesByIdentifier(a[a.length - 1], b[b.length - 1]);
300 }
301 };
302
303 modulesByChunkGroup.sort(compareModuleLists);
304
305 const finalModules = [];
306
307 for (;;) {
308 const failedModules = new Set();
309 const list = modulesByChunkGroup[0].list;
310 if (list.length === 0) {
311 // done, everything empty
312 break;
313 }
314 let selectedModule = list[list.length - 1];
315 let hasFailed = undefined;
316 outer: for (;;) {
317 for (const { list, set } of modulesByChunkGroup) {
318 if (list.length === 0) continue;
319 const lastModule = list[list.length - 1];
320 if (lastModule === selectedModule) continue;
321 if (!set.has(selectedModule)) continue;
322 failedModules.add(selectedModule);
323 if (failedModules.has(lastModule)) {
324 // There is a conflict, try other alternatives
325 hasFailed = lastModule;
326 continue;
327 }
328 selectedModule = lastModule;
329 hasFailed = false;
330 continue outer; // restart
331 }
332 break;
333 }
334 if (hasFailed) {
335 // There is a not resolve-able conflict with the selectedModule
336 if (compilation) {
337 // TODO print better warning
338 compilation.warnings.push(
339 new Error(
340 `chunk ${
341 chunk.name || chunk.id
342 }\nConflicting order between ${hasFailed.readableIdentifier(
343 compilation.requestShortener
344 )} and ${selectedModule.readableIdentifier(
345 compilation.requestShortener
346 )}`
347 )
348 );
349 }
350 selectedModule = hasFailed;
351 }
352 // Insert the selected module into the final modules list
353 finalModules.push(selectedModule);
354 // Remove the selected module from all lists
355 for (const { list, set } of modulesByChunkGroup) {
356 const lastModule = list[list.length - 1];
357 if (lastModule === selectedModule) list.pop();
358 else if (hasFailed && set.has(selectedModule)) {
359 const idx = list.indexOf(selectedModule);
360 if (idx >= 0) list.splice(idx, 1);
361 }
362 }
363 modulesByChunkGroup.sort(compareModuleLists);
364 }
365 return finalModules;
366 }
367
368 getOrderedChunkCssModules(chunk, chunkGraph, compilation) {
369 return [
370 ...this.getModulesInOrder(
371 chunk,
372 chunkGraph.getOrderedChunkModulesIterableBySourceType(
373 chunk,
374 "css-import",
375 compareModulesByIdentifier
376 ),
377 compilation
378 ),
379 ...this.getModulesInOrder(
380 chunk,
381 chunkGraph.getOrderedChunkModulesIterableBySourceType(
382 chunk,
383 "css",
384 compareModulesByIdentifier
385 ),
386 compilation
387 )
388 ];
389 }
390
391 renderChunk({
392 uniqueName,
393 chunk,
394 chunkGraph,
395 codeGenerationResults,
396 modules
397 }) {
398 const source = new ConcatSource();
399 const metaData = [];
400 for (const module of modules) {
401 try {
402 const codeGenResult = codeGenerationResults.get(module, chunk.runtime);
403
404 const s =
405 codeGenResult.sources.get("css") ||
406 codeGenResult.sources.get("css-import");
407 if (s) {
408 source.add(s);
409 source.add("\n");
410 }
411 const exports =
412 codeGenResult.data && codeGenResult.data.get("css-exports");
413 const moduleId = chunkGraph.getModuleId(module) + "";
414 metaData.push(
415 `${
416 exports
417 ? Array.from(exports, ([n, v]) => {
418 const shortcutValue = `${
419 uniqueName ? uniqueName + "-" : ""
420 }${moduleId}-${n}`;
421 return v === shortcutValue
422 ? `${escapeCss(n)}/`
423 : v === "--" + shortcutValue
424 ? `${escapeCss(n)}%`
425 : `${escapeCss(n)}(${escapeCss(v)})`;
426 }).join("")
427 : ""
428 }${escapeCss(moduleId)}`
429 );
430 } catch (e) {
431 e.message += `\nduring rendering of css ${module.identifier()}`;
432 throw e;
433 }
434 }
435 source.add(
436 `head{--webpack-${escapeCss(
437 (uniqueName ? uniqueName + "-" : "") + chunk.id,
438 true
439 )}:${metaData.join(",")};}`
440 );
441 return source;
442 }
443
444 static getChunkFilenameTemplate(chunk, outputOptions) {
445 if (chunk.cssFilenameTemplate) {
446 return chunk.cssFilenameTemplate;
447 } else if (chunk.canBeInitial()) {
448 return outputOptions.cssFilename;
449 } else {
450 return outputOptions.cssChunkFilename;
451 }
452 }
453
454 static chunkHasCss(chunk, chunkGraph) {
455 return (
456 !!chunkGraph.getChunkModulesIterableBySourceType(chunk, "css") ||
457 !!chunkGraph.getChunkModulesIterableBySourceType(chunk, "css-import")
458 );
459 }
460}
461
462module.exports = CssModulesPlugin;