Skip to content

Commit 9901de7

Browse files
feat(css): add CSS module support (css/experimental)
1 parent b55f8fc commit 9901de7

20 files changed

+936
-0
lines changed

bin/webpack.js

100644100755
File mode changed.

lib/Compilation.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ class Compilation extends Tapable {
220220
this.requestShortener
221221
);
222222
this.moduleTemplates = {
223+
// CSS
224+
css: new ModuleTemplate(this.outputOptions),
223225
javascript: new ModuleTemplate(this.runtimeTemplate),
224226
webassembly: new ModuleTemplate(this.runtimeTemplate)
225227
};

lib/WebpackOptionsApply.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const OptionsApply = require("./OptionsApply");
99
const JavascriptModulesPlugin = require("./JavascriptModulesPlugin");
1010
const JsonModulesPlugin = require("./JsonModulesPlugin");
1111
const WebAssemblyModulesPlugin = require("./WebAssemblyModulesPlugin");
12+
// CSS
13+
const CSSModulesPlugin = require("./css/CSSModulesPlugin");
14+
const CSSDependencyPlugin = require("./css/CSSDependency");
1215

1316
const LoaderTargetPlugin = require("./LoaderTargetPlugin");
1417
const FunctionModulePlugin = require("./FunctionModulePlugin");
@@ -270,6 +273,9 @@ class WebpackOptionsApply extends OptionsApply {
270273
new JavascriptModulesPlugin().apply(compiler);
271274
new JsonModulesPlugin().apply(compiler);
272275
new WebAssemblyModulesPlugin().apply(compiler);
276+
// CSS
277+
new CSSModulesPlugin().apply(compiler);
278+
new CSSDependencyPlugin().apply(compiler);
273279

274280
new EntryOptionPlugin().apply(compiler);
275281
compiler.hooks.entryOption.call(options.context, options.entry);

lib/WebpackOptionsDefaulter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ class WebpackOptionsDefaulter extends OptionsDefaulter {
6767
{
6868
test: /\.wasm$/i,
6969
type: "webassembly/experimental"
70+
},
71+
{
72+
test: /\.css$/i,
73+
type: "css/experimental",
74+
resolve: {
75+
mainFields: ["style"]
76+
}
7077
}
7178
]);
7279

@@ -83,6 +90,8 @@ class WebpackOptionsDefaulter extends OptionsDefaulter {
8390
});
8491

8592
this.set("output.filename", "[name].js");
93+
// CSS
94+
this.set("output.cssFilename", "[name].css");
8695
this.set("output.chunkFilename", "make", options => {
8796
const filename = options.output.filename;
8897
if (typeof filename === "function") return filename;

lib/css/CSSDependency.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const {
2+
CSSURLDependency,
3+
CSSImportDependency,
4+
CSSExportDependency
5+
} = require("./dependencies");
6+
7+
class CSSDependencyPlugin {
8+
constructor(options) {
9+
this.plugin = "CSSDependencyPlugin";
10+
this.options = options;
11+
}
12+
13+
apply(compiler) {
14+
const { plugin } = this;
15+
const { compilation } = compiler.hooks;
16+
17+
compilation.tap(plugin, (compilation, { normalModuleFactory }) => {
18+
const { dependencyFactories, dependencyTemplates } = compilation;
19+
20+
dependencyFactories.set(CSSURLDependency, normalModuleFactory);
21+
dependencyFactories.set(CSSImportDependency, normalModuleFactory);
22+
dependencyFactories.set(CSSExportDependency, normalModuleFactory);
23+
24+
dependencyTemplates.set(
25+
CSSURLDependency,
26+
new CSSURLDependency.Template()
27+
);
28+
29+
dependencyTemplates.set(
30+
CSSImportDependency,
31+
new CSSImportDependency.Template()
32+
);
33+
34+
dependencyTemplates.set(
35+
CSSExportDependency,
36+
new CSSExportDependency.Template()
37+
);
38+
});
39+
}
40+
}
41+
42+
module.exports = CSSDependencyPlugin;

lib/css/CSSGenerator.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { RawSource } = require("webpack-sources");
2+
3+
class CSSGenerator {
4+
generate(module, dependencyTemplates) {
5+
const source = module.originalSource();
6+
7+
if (!source) {
8+
return new RawSource("throw new Error('No source available');");
9+
}
10+
11+
for (const dependency of module.dependencies) {
12+
this.sourceDependency(source, dependency, dependencyTemplates);
13+
}
14+
15+
return source;
16+
}
17+
18+
sourceDependency(source, dependency, dependencyTemplates) {
19+
const template = dependencyTemplates.get(dependency.constructor);
20+
21+
if (!template) {
22+
throw new Error(
23+
"No template for dependency: " + dependency.constructor.name
24+
);
25+
}
26+
27+
return template.apply(source, dependency);
28+
}
29+
}
30+
31+
module.exports = CSSGenerator;

lib/css/CSSModulesPlugin.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const CSSParser = require("./CSSParser");
2+
const CSSGenerator = require("./CSSGenerator");
3+
const CSSTemplate = require("./CSSTemplate");
4+
5+
const { ConcatSource } = require("webpack-sources");
6+
7+
class CSSModulesPlugin {
8+
constructor() {
9+
this.plugin = {
10+
name: "CSSModulesPlugin"
11+
};
12+
}
13+
14+
apply(compiler) {
15+
const { plugin } = this;
16+
const { compilation } = compiler.hooks;
17+
18+
compilation.tap(plugin, (compilation, { normalModuleFactory }) => {
19+
const { createParser, createGenerator } = normalModuleFactory.hooks;
20+
21+
createParser.for("css/experimental").tap(plugin, options => {
22+
return new CSSParser(options);
23+
});
24+
25+
createGenerator.for("css/experimental").tap(plugin, options => {
26+
return new CSSGenerator(options);
27+
});
28+
29+
const { chunkTemplate } = compilation;
30+
31+
chunkTemplate.hooks.renderManifest.tap(plugin, (result, options) => {
32+
const { chunk, moduleTemplates, dependencyTemplates } = options;
33+
34+
const filenameTemplate = options.outputOptions.cssFilename;
35+
36+
result.push({
37+
render: () =>
38+
this.renderCSS(
39+
chunkTemplate,
40+
chunk,
41+
moduleTemplates.css,
42+
dependencyTemplates
43+
),
44+
filenameTemplate,
45+
pathOptions: {
46+
chunk
47+
},
48+
identifier: `CSSChunk (${chunk.id})`,
49+
hash: chunk.hash
50+
});
51+
52+
return result;
53+
});
54+
});
55+
}
56+
57+
renderCSSModules(module, moduleTemplate, dependencyTemplates) {
58+
return moduleTemplate.render(module, dependencyTemplates, {});
59+
}
60+
61+
renderCSS(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
62+
const { modules } = chunkTemplate.hooks;
63+
64+
const sources = CSSTemplate.renderChunk(
65+
chunk,
66+
module => module.type.startsWith("css"),
67+
moduleTemplate,
68+
dependencyTemplates
69+
);
70+
71+
const result = modules.call(
72+
sources,
73+
chunk,
74+
moduleTemplate,
75+
dependencyTemplates
76+
);
77+
78+
chunk.rendered = true;
79+
80+
return new ConcatSource(result);
81+
}
82+
}
83+
84+
module.exports = CSSModulesPlugin;

lib/css/CSSParser.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
const postcss = require("postcss");
2+
const { imports, urls } = require("./parser/plugins");
3+
const { selectors } = require("./parser/plugins/modules");
4+
5+
const {
6+
CSSURLDependency,
7+
CSSImportDependency,
8+
CSSExportDependency
9+
} = require("./dependencies");
10+
11+
const { OriginalSource, SourceMapSource } = require("webpack-sources");
12+
13+
class CSSParser {
14+
constructor(options = {}) {
15+
this.postcss = options;
16+
}
17+
18+
parse(source, state, cb) {
19+
const plugins = [
20+
urls({
21+
url: true
22+
}),
23+
imports({
24+
import: true
25+
}),
26+
selectors()
27+
].concat(this.postcss.plugins || []);
28+
29+
const options = Object.assign(
30+
{
31+
to: state.module.resource,
32+
map: {
33+
inline: false,
34+
annotation: false
35+
},
36+
from: state.module.resource
37+
},
38+
this.postcss.options
39+
);
40+
41+
postcss(plugins)
42+
.process(source, options)
43+
.then(({ root, css, map, messages }) => {
44+
state.module._ast = root;
45+
state.module._source = map
46+
? new SourceMapSource(css, state.module.resource, map)
47+
: new OriginalSource(css, state.module.resource);
48+
49+
return messages.filter(msg => msg.type.includes("dependency")).reduce(
50+
(done, dep) =>
51+
new Promise((resolve, reject) => {
52+
if (dep.name.includes("CSS__URL")) {
53+
const dependency = new CSSURLDependency(dep.url, dep.name);
54+
55+
state.module.addDependency(dependency, err => {
56+
if (err) reject(err);
57+
58+
resolve();
59+
});
60+
}
61+
62+
if (dep.name.includes("CSS__IMPORT")) {
63+
const dependency = new CSSImportDependency(
64+
dep.import,
65+
dep.name
66+
);
67+
68+
state.module.addDependency(dependency, err => {
69+
if (err) reject(err);
70+
71+
resolve();
72+
});
73+
}
74+
75+
if (dep.name.includes("CSS__EXPORT")) {
76+
const dependency = new CSSExportDependency(
77+
dep.export(),
78+
dep.name
79+
);
80+
81+
state.module.addDependency(dependency, err => {
82+
if (err) reject(err);
83+
84+
resolve();
85+
});
86+
}
87+
88+
resolve();
89+
}),
90+
Promise.resolve()
91+
);
92+
})
93+
.then(() => cb(null, state))
94+
.catch(err => cb(err));
95+
}
96+
}
97+
98+
module.exports = CSSParser;

lib/css/CSSTemplate.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const { ConcatSource } = require("webpack-sources");
2+
3+
class CSSTemplate {
4+
static renderChunk(
5+
chunk,
6+
filterFn,
7+
moduleTemplate,
8+
dependencyTemplates,
9+
prefix
10+
) {
11+
// if(!prefix) prefix = "";
12+
13+
const result = new ConcatSource();
14+
15+
const modules = chunk.getModules().filter(filterFn);
16+
const removedModules = chunk.removedModules;
17+
18+
const sources = modules.map(module => {
19+
return {
20+
id: module.id,
21+
source: moduleTemplate.render(module, dependencyTemplates, { chunk })
22+
};
23+
});
24+
25+
if (removedModules && removedModules.length > 0) {
26+
for (const id of removedModules) {
27+
sources.push({
28+
id,
29+
source: "/* CSS Module removed */"
30+
});
31+
}
32+
}
33+
34+
sources.sort().reduceRight((result, module, idx) => {
35+
result.add(`\n/* ${module.id} */\n`);
36+
result.add(module.source);
37+
38+
return result;
39+
}, result);
40+
41+
result.add("\n" /* + prefix */);
42+
43+
return result;
44+
}
45+
}
46+
47+
module.exports = CSSTemplate;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const NullDependency = require("../../dependencies/NullDependency");
2+
3+
class CSSExportDependency extends NullDependency {
4+
constructor(exports) {
5+
super();
6+
7+
this.exports = exports;
8+
}
9+
10+
get type() {
11+
return "css exports";
12+
}
13+
14+
getExports() {
15+
return {
16+
exports: this.exports
17+
};
18+
}
19+
}
20+
21+
CSSExportDependency.Template = class CSSExportDependencyTemplate {
22+
apply(source, dependency) {
23+
return source;
24+
}
25+
};
26+
27+
module.exports = CSSExportDependency;

0 commit comments

Comments
 (0)