UNPKG

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