Skip to content

Commit bdf4d83

Browse files
committed
add basic css modules support
1 parent 453e5ca commit bdf4d83

8 files changed

Lines changed: 243 additions & 16 deletions

File tree

‎lib/css/CssModulesPlugin.js‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const HotUpdateChunk = require("../HotUpdateChunk");
1010
const RuntimeGlobals = require("../RuntimeGlobals");
1111
const CssExportDependency = require("../dependencies/CssExportDependency");
1212
const CssImportDependency = require("../dependencies/CssImportDependency");
13+
const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
1314
const CssUrlDependency = require("../dependencies/CssUrlDependency");
1415
const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
1516
const {
@@ -61,7 +62,7 @@ const escapeCssIdentifierPart = (str, useOptionalUnderscore) => {
6162
/[^a-zA-Z0-9_\u0081-\uffff-]/g,
6263
s => `\\${s}`
6364
);
64-
return useOptionalUnderscore && /^[0-9_]/.test(escaped)
65+
return useOptionalUnderscore && /^[0-9_-]/.test(escaped)
6566
? `_${escaped}`
6667
: escaped;
6768
};
@@ -86,6 +87,10 @@ class CssModulesPlugin {
8687
CssUrlDependency,
8788
new CssUrlDependency.Template()
8889
);
90+
compilation.dependencyTemplates.set(
91+
CssLocalIdentifierDependency,
92+
new CssLocalIdentifierDependency.Template()
93+
);
8994
compilation.dependencyTemplates.set(
9095
CssExportDependency,
9196
new CssExportDependency.Template()
@@ -112,13 +117,18 @@ class CssModulesPlugin {
112117
.for("css/global")
113118
.tap(plugin, parserOptions => {
114119
validateParserOptions(parserOptions);
115-
return new CssParser({ allowPseudoBlocks: false });
120+
return new CssParser({
121+
allowPseudoBlocks: false,
122+
allowModeSwitch: false
123+
});
116124
});
117125
normalModuleFactory.hooks.createParser
118126
.for("css/module")
119127
.tap(plugin, parserOptions => {
120128
validateParserOptions(parserOptions);
121-
return new CssParser({ allowPseudoBlocks: true });
129+
return new CssParser({
130+
defaultMode: "local"
131+
});
122132
});
123133
normalModuleFactory.hooks.createGenerator
124134
.for("css")

‎lib/css/CssParser.js‎

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Parser = require("../Parser");
99
const ConstDependency = require("../dependencies/ConstDependency");
1010
const CssExportDependency = require("../dependencies/CssExportDependency");
1111
const CssImportDependency = require("../dependencies/CssImportDependency");
12+
const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
1213
const CssUrlDependency = require("../dependencies/CssUrlDependency");
1314
const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
1415
const walkCssTokens = require("./walkCssTokens");
@@ -95,9 +96,15 @@ const explainMode = mode => {
9596
};
9697

9798
class CssParser extends Parser {
98-
constructor({ allowPseudoBlocks = true } = {}) {
99+
constructor({
100+
allowPseudoBlocks = true,
101+
allowModeSwitch = true,
102+
defaultMode = "global"
103+
} = {}) {
99104
super();
100105
this.allowPseudoBlocks = allowPseudoBlocks;
106+
this.allowModeSwitch = allowModeSwitch;
107+
this.defaultMode = defaultMode;
101108
}
102109

103110
/**
@@ -340,6 +347,27 @@ class CssParser extends Parser {
340347
}
341348
return end;
342349
},
350+
class: (input, start, end) => {
351+
switch (mode) {
352+
case CSS_MODE_TOP_LEVEL: {
353+
if (
354+
modeData === "local" ||
355+
(this.defaultMode === "local" && modeData === undefined)
356+
) {
357+
const dep = new CssLocalIdentifierDependency(
358+
input.slice(start + 1, end),
359+
[start + 1, end]
360+
);
361+
const { line: sl, column: sc } = locConverter.get(start);
362+
const { line: el, column: ec } = locConverter.get(end);
363+
dep.setLoc(sl, sc, el, ec);
364+
module.addDependency(dep);
365+
}
366+
break;
367+
}
368+
}
369+
return end;
370+
},
343371
leftParenthesis: (input, start, end) => {
344372
return end;
345373
},
@@ -349,24 +377,30 @@ class CssParser extends Parser {
349377
pseudoClass: (input, start, end) => {
350378
switch (mode) {
351379
case CSS_MODE_TOP_LEVEL: {
352-
if (this.allowPseudoBlocks) {
353-
const name = input.slice(start, end);
354-
if (name === ":global") {
355-
modeData = ":global";
356-
} else if (name === ":local") {
357-
modeData = ":local";
358-
} else if (name === ":export") {
359-
const pos = parseExports(input, end);
360-
const dep = new ConstDependency("", [start, pos]);
361-
module.addPresentationalDependency(dep);
362-
return pos;
363-
}
380+
const name = input.slice(start, end);
381+
if (this.allowModeSwitch && name === ":global") {
382+
modeData = "global";
383+
} else if (this.allowModeSwitch && name === ":local") {
384+
modeData = "local";
385+
} else if (this.allowPseudoBlocks && name === ":export") {
386+
const pos = parseExports(input, end);
387+
const dep = new ConstDependency("", [start, pos]);
388+
module.addPresentationalDependency(dep);
389+
return pos;
364390
}
365391
break;
366392
}
367393
}
368394
return end;
369395
},
396+
pseudoFunction: (input, start, end) => {
397+
switch (mode) {
398+
case CSS_MODE_TOP_LEVEL: {
399+
break;
400+
}
401+
}
402+
return end;
403+
},
370404
comma: (input, start, end) => {
371405
switch (mode) {
372406
case CSS_MODE_TOP_LEVEL:
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Ivan Kopeykin @vankop
4+
*/
5+
6+
"use strict";
7+
8+
const makeSerializable = require("../util/makeSerializable");
9+
const NullDependency = require("./NullDependency");
10+
11+
/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
12+
/** @typedef {import("../Dependency")} Dependency */
13+
/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
14+
/** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
15+
/** @typedef {import("../ModuleGraph")} ModuleGraph */
16+
17+
class CssLocalIdentifierDependency extends NullDependency {
18+
/**
19+
* @param {string} name name
20+
* @param {[number, number]} range range
21+
*/
22+
constructor(name, range) {
23+
super();
24+
this.name = name;
25+
this.range = range;
26+
}
27+
28+
get type() {
29+
return "css local identifier";
30+
}
31+
32+
/**
33+
* Returns the exported names
34+
* @param {ModuleGraph} moduleGraph module graph
35+
* @returns {ExportsSpec | undefined} export names
36+
*/
37+
getExports(moduleGraph) {
38+
const name = this.name;
39+
return {
40+
exports: [
41+
{
42+
name,
43+
canMangle: true
44+
}
45+
],
46+
dependencies: undefined
47+
};
48+
}
49+
50+
serialize(context) {
51+
const { write } = context;
52+
write(this.name);
53+
write(this.range);
54+
super.serialize(context);
55+
}
56+
57+
deserialize(context) {
58+
const { read } = context;
59+
this.name = read();
60+
this.range = read();
61+
super.deserialize(context);
62+
}
63+
}
64+
65+
const escapeCssIdentifier = str => {
66+
const escaped = `${str}`.replace(
67+
// cspell:word uffff
68+
/[^a-zA-Z0-9_\u0081-\uffff-]/g,
69+
s => `\\${s}`
70+
);
71+
return /^[0-9-]/.test(escaped) ? `_${escaped}` : escaped;
72+
};
73+
74+
CssLocalIdentifierDependency.Template = class CssLocalIdentifierDependencyTemplate extends (
75+
NullDependency.Template
76+
) {
77+
/**
78+
* @param {Dependency} dependency the dependency for which the template should be applied
79+
* @param {ReplaceSource} source the current replace source which can be modified
80+
* @param {DependencyTemplateContext} templateContext the context object
81+
* @returns {void}
82+
*/
83+
apply(
84+
dependency,
85+
source,
86+
{ module, moduleGraph, chunkGraph, runtime, cssExports }
87+
) {
88+
const dep = /** @type {CssLocalIdentifierDependency} */ (dependency);
89+
const used = moduleGraph
90+
.getExportInfo(module, dep.name)
91+
.getUsedName(dep.name, runtime);
92+
const moduleId = chunkGraph.getModuleId(module);
93+
const identifier = escapeCssIdentifier((used || "_") + "_" + moduleId);
94+
source.replace(dep.range[0], dep.range[1] - 1, identifier);
95+
if (used) cssExports.set(used, identifier);
96+
}
97+
};
98+
99+
makeSerializable(
100+
CssLocalIdentifierDependency,
101+
"webpack/lib/dependencies/CssLocalIdentifierDependency"
102+
);
103+
104+
module.exports = CssLocalIdentifierDependency;

‎lib/util/internalSerializables.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ module.exports = {
6969
require("../dependencies/CriticalDependencyWarning"),
7070
"dependencies/CssImportDependency": () =>
7171
require("../dependencies/CssImportDependency"),
72+
"dependencies/CssLocalIdentifierDependency": () =>
73+
require("../dependencies/CssLocalIdentifierDependency"),
7274
"dependencies/CssExportDependency": () =>
7375
require("../dependencies/CssExportDependency"),
7476
"dependencies/CssUrlDependency": () =>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const prod = process.env.NODE_ENV === "production";
2+
3+
it("should allow to create css modules", done => {
4+
prod
5+
? __non_webpack_require__("./249.bundle1.js")
6+
: __non_webpack_require__("./use-style_js.bundle0.js");
7+
import("./use-style.js").then(({ default: x }) => {
8+
try {
9+
expect(x).toEqual({
10+
class: prod ? "S_491" : "class_\\.\\/style\\.module\\.css",
11+
local: prod
12+
? "Zw_491 yl_491 J__491 gc_491"
13+
: "local1_\\.\\/style\\.module\\.css local2_\\.\\/style\\.module\\.css local3_\\.\\/style\\.module\\.css local4_\\.\\/style\\.module\\.css"
14+
});
15+
} catch (e) {
16+
return done(e);
17+
}
18+
done();
19+
}, done);
20+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.class {
2+
color: red;
3+
}
4+
5+
.local1,
6+
.local2 :global .global,
7+
.local3 {
8+
color: green;
9+
}
10+
11+
:global .global :local .local4 {
12+
color: yellow;
13+
}
14+
15+
/* .local5:global(.global).local6 {
16+
color: blue;
17+
} */
18+
19+
/* :global(:global(:local(.nested1)).nested2).nested3 {
20+
color: pink;
21+
} */
22+
23+
/* @keyframes localkeyframes {
24+
0% {
25+
}
26+
100% {
27+
}
28+
}
29+
30+
.animation {
31+
animation-name: localkeyframes;
32+
animation: 3s ease-in 1s 2 reverse both paused localkeyframes, localkeyframes;
33+
} */
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as style from "./style.module.css";
2+
import { local1, local2, local3, local4 } from "./style.module.css";
3+
4+
export default {
5+
class: style.class,
6+
local: `${local1} ${local2} ${local3} ${local4}`
7+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @type {import("../../../../").Configuration[]} */
2+
module.exports = [
3+
{
4+
target: "web",
5+
mode: "development",
6+
experiments: {
7+
css: true
8+
}
9+
},
10+
{
11+
target: "web",
12+
mode: "production",
13+
experiments: {
14+
css: true
15+
}
16+
}
17+
];

0 commit comments

Comments
 (0)