feat: `@value` in CSS modules

This commit is contained in:
alexander.akait 2024-11-07 19:32:00 +03:00
parent 90bf8dd833
commit bc6ef39a9e
10 changed files with 1229 additions and 70 deletions

View File

@ -27,6 +27,7 @@ const SelfModuleFactory = require("../SelfModuleFactory");
const WebpackError = require("../WebpackError"); const WebpackError = require("../WebpackError");
const CssIcssExportDependency = require("../dependencies/CssIcssExportDependency"); const CssIcssExportDependency = require("../dependencies/CssIcssExportDependency");
const CssIcssImportDependency = require("../dependencies/CssIcssImportDependency"); const CssIcssImportDependency = require("../dependencies/CssIcssImportDependency");
const CssIcssSymbolDependency = require("../dependencies/CssIcssSymbolDependency");
const CssImportDependency = require("../dependencies/CssImportDependency"); const CssImportDependency = require("../dependencies/CssImportDependency");
const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency"); const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency"); const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
@ -248,6 +249,14 @@ class CssModulesPlugin {
(compilation, { normalModuleFactory }) => { (compilation, { normalModuleFactory }) => {
const hooks = CssModulesPlugin.getCompilationHooks(compilation); const hooks = CssModulesPlugin.getCompilationHooks(compilation);
const selfFactory = new SelfModuleFactory(compilation.moduleGraph); const selfFactory = new SelfModuleFactory(compilation.moduleGraph);
compilation.dependencyFactories.set(
CssImportDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
CssImportDependency,
new CssImportDependency.Template()
);
compilation.dependencyFactories.set( compilation.dependencyFactories.set(
CssUrlDependency, CssUrlDependency,
normalModuleFactory normalModuleFactory
@ -280,13 +289,9 @@ class CssModulesPlugin {
CssIcssExportDependency, CssIcssExportDependency,
new CssIcssExportDependency.Template() new CssIcssExportDependency.Template()
); );
compilation.dependencyFactories.set(
CssImportDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set( compilation.dependencyTemplates.set(
CssImportDependency, CssIcssSymbolDependency,
new CssImportDependency.Template() new CssIcssSymbolDependency.Template()
); );
compilation.dependencyTemplates.set( compilation.dependencyTemplates.set(
StaticExportsDependency, StaticExportsDependency,

View File

@ -15,6 +15,7 @@ const WebpackError = require("../WebpackError");
const ConstDependency = require("../dependencies/ConstDependency"); const ConstDependency = require("../dependencies/ConstDependency");
const CssIcssExportDependency = require("../dependencies/CssIcssExportDependency"); const CssIcssExportDependency = require("../dependencies/CssIcssExportDependency");
const CssIcssImportDependency = require("../dependencies/CssIcssImportDependency"); const CssIcssImportDependency = require("../dependencies/CssIcssImportDependency");
const CssIcssSymbolDependency = require("../dependencies/CssIcssSymbolDependency");
const CssImportDependency = require("../dependencies/CssImportDependency"); const CssImportDependency = require("../dependencies/CssImportDependency");
const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency"); const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency"); const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
@ -245,8 +246,8 @@ class CssParser extends Parser {
let lastIdentifier; let lastIdentifier;
/** @type {Set<string>} */ /** @type {Set<string>} */
const declaredCssVariables = new Set(); const declaredCssVariables = new Set();
/** @type {Map<string, { path: string, name: string }>} */ /** @type {Map<string, { path?: string, value: string }>} */
const icssImportMap = new Map(); const icssDefinitions = new Map();
/** /**
* @param {string} input input * @param {string} input input
@ -365,9 +366,9 @@ class CssParser extends Parser {
*/ */
const createDep = (name, value, start, end) => { const createDep = (name, value, start, end) => {
if (type === 0) { if (type === 0) {
icssImportMap.set(name, { icssDefinitions.set(name, {
path: /** @type {string} */ (importPath), path: /** @type {string} */ (importPath),
name: value value
}); });
} else if (type === 1) { } else if (type === 1) {
const dep = new CssIcssExportDependency(name, value); const dep = new CssIcssExportDependency(name, value);
@ -785,7 +786,61 @@ class CssParser extends Parser {
} }
default: { default: {
if (isModules) { if (isModules) {
if (OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)) { if (name === "@value") {
const semi = eatUntilSemi(input, end);
const atRuleEnd = semi + 1;
const params = input.slice(end, semi);
let [tokens, from] = params.split(/\s*from\s*/);
if (from) {
const aliases = tokens
.replace(/\/\*((?!\*\/).*?)\*\//g, " ")
.trim()
.replace(/^\(|\)$/g, "")
.split(/\s*,\s*/);
from = from.replace(/\/\*((?!\*\/).*?)\*\//g, "");
for (const alias of aliases) {
const [name, aliasName] = alias.split(/\s*as\s*/);
const isExplicitImport = from[0] === "'" || from[0] === '"';
if (!isExplicitImport) {
return atRuleEnd;
}
icssDefinitions.set(aliasName || name, {
value: name,
path: from.slice(1, -1)
});
}
} else {
const alias = tokens.trim();
let [name, value] = alias.includes(":")
? alias.split(":")
: alias.split(" ");
if (value && !/^\s+$/.test(value)) {
value = value.trim();
}
icssDefinitions.set(name, { value });
const dep = new CssIcssExportDependency(name, value);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
const dep = new ConstDependency("", [start, atRuleEnd]);
module.addPresentationalDependency(dep);
return atRuleEnd;
} else if (
OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name) &&
isLocalMode()
) {
const ident = walkCssTokens.eatIdentSequenceOrString( const ident = walkCssTokens.eatIdentSequenceOrString(
input, input,
end end
@ -795,7 +850,6 @@ class CssParser extends Parser {
ident[2] === true ident[2] === true
? input.slice(ident[0], ident[1]) ? input.slice(ident[0], ident[1])
: input.slice(ident[0] + 1, ident[1] - 1); : input.slice(ident[0] + 1, ident[1] - 1);
if (isLocalMode()) {
const { line: sl, column: sc } = locConverter.get(ident[0]); const { line: sl, column: sc } = locConverter.get(ident[0]);
const { line: el, column: ec } = locConverter.get(ident[1]); const { line: el, column: ec } = locConverter.get(ident[1]);
const dep = new CssLocalIdentifierDependency(name, [ const dep = new CssLocalIdentifierDependency(name, [
@ -804,16 +858,14 @@ class CssParser extends Parser {
]); ]);
dep.setLoc(sl, sc, el, ec); dep.setLoc(sl, sc, el, ec);
module.addDependency(dep); module.addDependency(dep);
}
return ident[1]; return ident[1];
} else if (name === "@property") { } else if (name === "@property" && isLocalMode()) {
const ident = walkCssTokens.eatIdentSequence(input, end); const ident = walkCssTokens.eatIdentSequence(input, end);
if (!ident) return end; if (!ident) return end;
let name = input.slice(ident[0], ident[1]); let name = input.slice(ident[0], ident[1]);
if (!name.startsWith("--")) return end; if (!name.startsWith("--")) return end;
name = name.slice(2); name = name.slice(2);
declaredCssVariables.add(name); declaredCssVariables.add(name);
if (isLocalMode()) {
const { line: sl, column: sc } = locConverter.get(ident[0]); const { line: sl, column: sc } = locConverter.get(ident[0]);
const { line: el, column: ec } = locConverter.get(ident[1]); const { line: el, column: ec } = locConverter.get(ident[1]);
const dep = new CssLocalIdentifierDependency( const dep = new CssLocalIdentifierDependency(
@ -823,10 +875,8 @@ class CssParser extends Parser {
); );
dep.setLoc(sl, sc, el, ec); dep.setLoc(sl, sc, el, ec);
module.addDependency(dep); module.addDependency(dep);
}
return ident[1]; return ident[1];
} else if (isModules && name === "@scope") { } else if (name === "@scope") {
modeData = isLocalMode() ? "local" : "global";
isNextRulePrelude = true; isNextRulePrelude = true;
return end; return end;
} }
@ -851,14 +901,12 @@ class CssParser extends Parser {
}, },
identifier: (input, start, end) => { identifier: (input, start, end) => {
if (isModules) { if (isModules) {
switch (scope) { if (icssDefinitions.has(input.slice(start, end))) {
case CSS_MODE_IN_BLOCK: { const name = input.slice(start, end);
if (icssImportMap.has(input.slice(start, end))) { const { path, value } = icssDefinitions.get(name);
const { path, name } = icssImportMap.get(
input.slice(start, end)
);
const dep = new CssIcssImportDependency(path, name, [ if (path) {
const dep = new CssIcssImportDependency(path, value, [
start, start,
end - 1 end - 1
]); ]);
@ -866,10 +914,22 @@ class CssParser extends Parser {
const { line: el, column: ec } = locConverter.get(end - 1); const { line: el, column: ec } = locConverter.get(end - 1);
dep.setLoc(sl, sc, el, ec); dep.setLoc(sl, sc, el, ec);
module.addDependency(dep); module.addDependency(dep);
} else {
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(end);
const dep = new CssIcssSymbolDependency(name, value, [
start,
end
]);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
return end; return end;
} }
switch (scope) {
case CSS_MODE_IN_BLOCK: {
if (isLocalMode()) { if (isLocalMode()) {
// Handle only top level values and not inside functions // Handle only top level values and not inside functions
if (inAnimationProperty && balanced.length === 0) { if (inAnimationProperty && balanced.length === 0) {

View File

@ -32,6 +32,7 @@ class CssIcssExportDependency extends NullDependency {
super(); super();
this.name = name; this.name = name;
this.value = value; this.value = value;
this._hashUpdate = undefined;
} }
get type() { get type() {
@ -78,6 +79,7 @@ class CssIcssExportDependency extends NullDependency {
* @returns {void} * @returns {void}
*/ */
updateHash(hash, { chunkGraph }) { updateHash(hash, { chunkGraph }) {
if (this._hashUpdate === undefined) {
const module = const module =
/** @type {CssModule} */ /** @type {CssModule} */
(chunkGraph.moduleGraph.getParentModule(this)); (chunkGraph.moduleGraph.getParentModule(this));
@ -88,8 +90,10 @@ class CssIcssExportDependency extends NullDependency {
this.name, this.name,
generator.convention generator.convention
); );
this._hashUpdate = JSON.stringify(names);
}
hash.update("exportsConvention"); hash.update("exportsConvention");
hash.update(JSON.stringify(names)); hash.update(this._hashUpdate);
} }
/** /**

View File

@ -0,0 +1,131 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Alexander Akait @alexander-akait
*/
"use strict";
const makeSerializable = require("../util/makeSerializable");
const NullDependency = require("./NullDependency");
/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
/** @typedef {import("../Dependency").ReferencedExport} ReferencedExport */
/** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../css/CssParser").Range} Range */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
class CssIcssSymbolDependency extends NullDependency {
/**
* @param {string} name name
* @param {string} value value
* @param {Range} range range
*/
constructor(name, value, range) {
super();
this.name = name;
this.value = value;
this.range = range;
this._hashUpdate = undefined;
}
get type() {
return "css @value identifier";
}
get category() {
return "self";
}
/**
* Update the hash
* @param {Hash} hash hash to be updated
* @param {UpdateHashContext} context context
* @returns {void}
*/
updateHash(hash, context) {
if (this._hashUpdate === undefined) {
const hashUpdate = `${this.range}|${this.name}|${this.value}`;
this._hashUpdate = hashUpdate;
}
hash.update(this._hashUpdate);
}
/**
* Returns the exported names
* @param {ModuleGraph} moduleGraph module graph
* @returns {ExportsSpec | undefined} export names
*/
getExports(moduleGraph) {
return {
exports: [
{
name: this.name,
canMangle: true
}
],
dependencies: undefined
};
}
/**
* Returns list of exports referenced by this dependency
* @param {ModuleGraph} moduleGraph module graph
* @param {RuntimeSpec} runtime the runtime for which the module is analysed
* @returns {(string[] | ReferencedExport)[]} referenced exports
*/
getReferencedExports(moduleGraph, runtime) {
return [[this.name]];
}
/**
* @param {ObjectSerializerContext} context context
*/
serialize(context) {
const { write } = context;
write(this.name);
write(this.range);
super.serialize(context);
}
/**
* @param {ObjectDeserializerContext} context context
*/
deserialize(context) {
const { read } = context;
this.name = read();
this.range = read();
super.deserialize(context);
}
}
CssIcssSymbolDependency.Template = class CssValueAtRuleDependencyTemplate extends (
NullDependency.Template
) {
/**
* @param {Dependency} dependency the dependency for which the template should be applied
* @param {ReplaceSource} source the current replace source which can be modified
* @param {DependencyTemplateContext} templateContext the context object
* @returns {void}
*/
apply(dependency, source, { cssExportsData }) {
const dep = /** @type {CssIcssSymbolDependency} */ (dependency);
source.replace(dep.range[0], dep.range[1] - 1, dep.value);
cssExportsData.exports.set(dep.name, dep.value);
}
};
makeSerializable(
CssIcssSymbolDependency,
"webpack/lib/dependencies/CssIcssSymbolDependency"
);
module.exports = CssIcssSymbolDependency;

View File

@ -83,6 +83,8 @@ module.exports = {
require("../dependencies/CssIcssExportDependency"), require("../dependencies/CssIcssExportDependency"),
"dependencies/CssUrlDependency": () => "dependencies/CssUrlDependency": () =>
require("../dependencies/CssUrlDependency"), require("../dependencies/CssUrlDependency"),
"dependencies/CssIcssSymbolDependency": () =>
require("../dependencies/CssIcssSymbolDependency"),
"dependencies/DelegatedSourceDependency": () => "dependencies/DelegatedSourceDependency": () =>
require("../dependencies/DelegatedSourceDependency"), require("../dependencies/DelegatedSourceDependency"),
"dependencies/DllEntryDependency": () => "dependencies/DllEntryDependency": () =>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,221 @@
@value red blue;
.value-in-class {
color: red;
}
@value v-comment-broken:;
@value v-comment:/* comment */;
@value small: (max-width: 599px);
@media small {
abbr:hover {
color: limegreen;
transition-duration: 1s;
}
}
@value blue-v1: red;
.foo { color: blue-v1; }
@value blue-v2: red;
.foo {
&.bar { color: blue-v2; }
}
@value blue-v3: red;
.foo {
@media (min-width: 1024px) {
&.bar { color: blue-v3; }
}
}
@value blue-v4: red;
.foo {
@media (min-width: 1024px) {
&.bar {
@media (min-width: 1024px) {
color: blue-v4;
}
}
}
}
@value test-t: 40px;
@value test_q: 36px;
.foo { height: test-t; height: test_q; }
@value colorValue: red;
.colorValue {
color: colorValue;
}
@value colorValue-v1: red;
#colorValue-v1 {
color: colorValue-v1;
}
@value colorValue-v2: red;
.colorValue-v2 > .colorValue-v2 {
color: colorValue-v2;
}
@value colorValue-v3: .red;
colorValue-v3 {
color: colorValue-v3;
}
@value red-v2 from "./colors.module.css";
.export {
color: red-v2;
}
@value blue as green from "./colors.module.css";
.foo { color: green; }
@value blue, green-v2 from "./colors.module.css";
.foo { color: red; }
.bar { color: green-v2 }
@value red-v3 from colors;
@value colors: "./colors.module.css";
.foo { color: red-v3; }
@value colors: "./colors.module.css";
@value red-v4 from colors;
.foo { color: red-v4; }
@value aaa: red;
@value bbb: aaa;
.a { color: bbb; }
@value base: 10px;
@value large: calc(base * 2);
.a { margin: large; }
@value a from "./colors.module.css";
@value b from "./colors.module.css";
.a { content: a b; }
@value --red from "./colors.module.css";
.foo { color: --red; }
@value named: red;
@value 3char #0f0;
@value 6char #00ff00;
@value rgba rgba(34, 12, 64, 0.3);
@value hsla hsla(220, 13.0%, 18.0%, 1);
.foo {
color: named;
background-color: 3char;
border-top-color: 6char;
border-bottom-color: rgba;
outline-color: hsla;
}
@value (blue-i, red-i) from "./colors.module.css";
.foo { color: red-i; }
.bar { color: blue-i }
@value coolShadow: 0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14) ;
.foo { box-shadow: coolShadow; }
@value func: color(red lightness(50%));
.foo { color: func; }
@value v-color: red;
:root { --color: v-color; }
@value v-empty: ;
:root { --color:v-empty; }
@value v-empty-v2: ;
:root { --color:v-empty-v2; }
@value v-empty-v3: /* comment */;
:root { --color:v-empty-v3; }
@value override: blue;
@value override: red;
.override {
color: override;
}
@value (blue as my-name) from "./colors.module.css";
@value (blue as my-name-again, red) from "./colors.module.css";
.class {
color: my-name;
color: my-name-again;
color: red;
}
@value/* test */blue-v1/* test */:/* test */red/* test */;
.color {
color: blue-v1;
}
@value coolShadow-v2 : 0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14) ;
.foo { box-shadow: coolShadow-v2; }
@value /* test */ coolShadow-v3 /* test */ : 0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14) ;
.foo { box-shadow: coolShadow-v3; }
@value /* test */ coolShadow-v4 /* test */ 0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14) ;
.foo { box-shadow: coolShadow-v4; }
@value/* test */coolShadow-v5/* test */0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14);
.foo { box-shadow: coolShadow-v5; }
@value/* test */coolShadow-v6/* test */:0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14);
.foo { box-shadow: coolShadow-v6; }
@value/* test */coolShadow-v7/* test */:/* test */0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14);
.foo { box-shadow: coolShadow-v7; }
@value /* test */ test-v1 /* test */ from /* test */ "./colors.module.css" /* test */;
.foo { color: test-v1; }
@value/* test */test-v2/* test */from/* test */"./colors.module.css"/* test */;
.foo { color: test-v2; }
@value/* test */(/* test */blue/* test */as/* test */my-name-q/* test */)/* test */from/* test */"./colors.module.css"/* test */;
.foo { color: my-name-q; }

View File

@ -0,0 +1,11 @@
@value red blue;
@value red-i: blue;
@value blue red;
@value blue-i: red;
@value a: "test-a";
@value b: "test-b";
@value --red: var(--color);
@value test-v1: blue;
@value test-v2: blue;
@value red-v2: blue;
@value green-v2: yellow;

View File

@ -1,3 +1,5 @@
@import "at-rule-value.module.css";
.class { .class {
color: red; color: red;
} }

View File

@ -6,7 +6,7 @@ import { UsedClassName } from "./identifiers.module.css";
// To prevent analysis export // To prevent analysis export
const isNotACSSModule = typeof notACssModule["c" + "lass"] === "undefined"; const isNotACSSModule = typeof notACssModule["c" + "lass"] === "undefined";
const hasOwnProperty = (obj, p) => Object.hasOwnProperty.call(obj, p) const hasOwnProperty = (obj, p) => Object.hasOwnProperty.call(obj, p);
export default { export default {
global: style.global, global: style.global,