Skip to content

Commit

Permalink
Support for scoping variables in CSS modules (#8122)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed May 25, 2022
1 parent 33562b6 commit 03cc465
Show file tree
Hide file tree
Showing 23 changed files with 253 additions and 53 deletions.
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
}
}
Empty file.
@@ -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
}
}
}
Empty file.
@@ -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);
}
Empty file.
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

0 comments on commit 03cc465

Please sign in to comment.