Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for scoping variables in CSS modules #8122

Merged
merged 7 commits into from May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/core/integration-tests/test/css-modules.js
Expand Up @@ -621,4 +621,52 @@ describe('css modules', () => {
);
assert(contents.includes('.index {'));
});

it('should support global css modules via boolean config', async function () {
let b = await bundle(
path.join(__dirname, '/integration/css-modules-global/a/index.js'),
{mode: 'production'},
);
let res = await run(b);
assert.deepEqual(res, 'C-gzXq_foo');

let contents = await outputFS.readFile(
b.getBundles().find(b => b.type === 'css').filePath,
'utf8',
);
assert(contents.includes('.C-gzXq_foo'));
assert(contents.includes('.x'));
});

it('should support global css modules via object config', async function () {
let b = await bundle(
path.join(__dirname, '/integration/css-modules-global/b/index.js'),
{mode: 'production'},
);
let res = await run(b);
assert.deepEqual(res, 'C-gzXq_foo');
let contents = await outputFS.readFile(
b.getBundles().find(b => b.type === 'css').filePath,
'utf8',
);
assert(contents.includes('.C-gzXq_foo'));
assert(contents.includes('.x'));
});

it('should optimize away unused variables when dashedIdents option is used', async function () {
let b = await bundle(
path.join(__dirname, '/integration/css-modules-vars/index.js'),
{mode: 'production'},
);
let contents = await outputFS.readFile(
b.getBundles().find(b => b.type === 'css').filePath,
'utf8',
);
assert.equal(
contents.split('\n')[0],
':root{--wGsoEa_color:red;--wGsoEa_font:Helvetica;--wGsoEa_theme-sizes-1\\/12:2;--wGsoEa_from-js:purple}body{font:var(--wGsoEa_font)}._4fY2uG_foo{color:var(--wGsoEa_color);width:var(--wGsoEa_theme-sizes-1\\/12);height:var(--height)}',
);
let res = await run(b);
assert.deepEqual(res, ['_4fY2uG_foo', '--wGsoEa_from-js']);
});
});
@@ -0,0 +1,3 @@
.foo {
color: red;
}
@@ -0,0 +1,4 @@
var foo = require('./index.css');
require('test/index.css');

output = foo.foo;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,5 @@
{
"@parcel/transformer-css": {
"cssModules": true
}
}
@@ -0,0 +1,3 @@
.foo {
color: red;
}
@@ -0,0 +1,4 @@
var foo = require('./index.css');
require('test/index.css');

output = foo.foo;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,7 @@
{
"@parcel/transformer-css": {
"cssModules": {
"global": true
}
}
}
@@ -0,0 +1,4 @@
var foo = require('./index.module.css');
var vars = require('./vars.module.css');

output = [foo.foo, vars['--from-js']];
@@ -0,0 +1,5 @@
.foo {
color: var(--color from "./vars.module.css");
width: var(--theme-sizes-1\/12 from "./vars.module.css");
height: var(--height from global);
}
@@ -0,0 +1,7 @@
{
"@parcel/transformer-css": {
"cssModules": {
"dashedIdents": true
}
}
}
@@ -0,0 +1,11 @@
:root {
--color: red;
--font: Helvetica;
--theme-sizes-1\/12: 2;
--from-js: purple;
--unused: green;
}

body {
font: var(--font);
}
4 changes: 2 additions & 2 deletions packages/optimizers/css/package.json
Expand Up @@ -20,12 +20,12 @@
"parcel": "^2.5.0"
},
"dependencies": {
"@parcel/css": "^1.8.2",
"@parcel/css": "^1.9.0",
"@parcel/diagnostic": "2.5.0",
"@parcel/plugin": "2.5.0",
"@parcel/source-map": "^2.0.0",
"@parcel/utils": "2.5.0",
"browserslist": "^4.6.6",
"nullthrows": "^1.1.1"
}
}
}
55 changes: 55 additions & 0 deletions packages/packagers/css/src/CSSPackager.js
Expand Up @@ -75,6 +75,35 @@ export default (new Packager({
return Promise.all([
asset,
asset.getCode().then((css: string) => {
// Replace CSS variable references with resolved symbols.
if (asset.meta.hasReferences) {
let replacements = new Map();
for (let dep of asset.getDependencies()) {
for (let [exported, {local}] of dep.symbols) {
let resolved = bundleGraph.getResolvedAsset(dep, bundle);
if (resolved) {
let resolution = bundleGraph.getSymbolResolution(
resolved,
exported,
bundle,
);
if (resolution.symbol) {
replacements.set(local, resolution.symbol);
}
}
}
}
if (replacements.size > 0) {
let regex = new RegExp(
[...replacements.keys()].join('|'),
'g',
);
css = css.replace(regex, m =>
escapeDashedIdent(replacements.get(m) || m),
);
}
}

if (media.length) {
return `@media ${media.join(', ')} {\n${css}\n}\n`;
}
Expand Down Expand Up @@ -251,3 +280,29 @@ async function processCSSModule(

return [asset, content, sourceMap?.toBuffer()];
}

function escapeDashedIdent(name) {
// https://drafts.csswg.org/cssom/#serialize-an-identifier
let res = '';
for (let c of name) {
let code = c.codePointAt(0);
if (code === 0) {
res += '\ufffd';
} else if ((code >= 0x1 && code <= 0x1f) || code === 0x7f) {
res += '\\' + code.toString(16) + ' ';
} else if (
(code >= 48 /* '0' */ && code <= 57) /* '9' */ ||
(code >= 65 /* 'A' */ && code <= 90) /* 'Z' */ ||
(code >= 97 /* 'a' */ && code <= 122) /* 'z' */ ||
code === 95 /* '_' */ ||
code === 45 /* '-' */ ||
code & 128 // non-ascii
) {
res += c;
} else {
res += '\\' + c;
}
}

return res;
}
4 changes: 2 additions & 2 deletions packages/transformers/css/package.json
Expand Up @@ -20,12 +20,12 @@
"parcel": "^2.5.0"
},
"dependencies": {
"@parcel/css": "^1.8.2",
"@parcel/css": "^1.9.0",
"@parcel/diagnostic": "2.5.0",
"@parcel/plugin": "2.5.0",
"@parcel/source-map": "^2.0.0",
"@parcel/utils": "2.5.0",
"browserslist": "^4.6.6",
"nullthrows": "^1.1.1"
}
}
}
42 changes: 37 additions & 5 deletions packages/transformers/css/src/CSSTransformer.js
Expand Up @@ -49,14 +49,30 @@ export default (new Transformer({
targets,
});
} else {
let cssModules = false;
if (
asset.meta.type !== 'tag' &&
asset.meta.cssModulesCompiled == null
) {
let cssModulesConfig = config?.cssModules;
if (
(asset.isSource &&
(typeof cssModulesConfig === 'boolean' ||
cssModulesConfig?.global)) ||
/\.module\./.test(asset.filePath)
) {
if (cssModulesConfig?.dashedIdents && !asset.isSource) {
cssModulesConfig.dashedIdents = false;
}

cssModules = cssModulesConfig ?? true;
}
}

res = transform({
filename: path.relative(options.projectRoot, asset.filePath),
code,
cssModules:
asset.meta.type !== 'tag' &&
(config?.cssModules ??
(asset.meta.cssModulesCompiled == null &&
/\.module\./.test(asset.filePath))),
cssModules,
analyzeDependencies: asset.meta.hasDependencies !== false,
sourceMap: !!asset.env.sourceMap,
drafts: config?.drafts,
Expand Down Expand Up @@ -212,6 +228,22 @@ export default (new Transformer({
}
}

if (res.references != null) {
let references = res.references;
for (let symbol in references) {
let reference = references[symbol];
asset.addDependency({
specifier: reference.specifier,
specifierType: 'esm',
symbols: new Map([
[reference.name, {local: symbol, isWeak: false, loc: null}],
]),
});

asset.meta.hasReferences = true;
}
}

assets.push({
type: 'js',
content: depjs + js,
Expand Down