Skip to content

Commit

Permalink
Use lightningcss to implement CSS packager (#8492)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Jan 8, 2024
1 parent e92fe64 commit cd447dc
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 76 deletions.
17 changes: 16 additions & 1 deletion packages/core/integration-tests/test/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ describe('css', () => {
let css = await outputFS.readFile(path.join(distDir, '/index.css'), 'utf8');
assert(css.includes('.local'));
assert(css.includes('.other'));
assert(/@media print {\s*.other/.test(css));
assert(
/@media print {\s*\.local(.|\n)*\.other(.|\n)*}(.|\n)*\.index/.test(css),
);
assert(css.includes('.index'));
});

Expand Down Expand Up @@ -510,4 +512,17 @@ describe('css', () => {
let res = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
assert(res.includes('.foo.bar'));
});

it('should support @layer', async function () {
let b = await bundle(path.join(__dirname, '/integration/css-layer/a.css'), {
mode: 'production',
});

let res = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
assert(
res.includes(
'@layer b.c{.c{color:#ff0}}@layer b{.b{color:#00f}}.a{color:red}',
),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "b.css" layer(b);

.a {
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "c.css" layer(c);

.b {
color: blue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.c {
color: yellow;
}
3 changes: 1 addition & 2 deletions packages/core/integration-tests/test/sourcemaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -830,12 +830,11 @@ describe('sourcemaps', function () {
// This should actually just be `./integration/scss-sourcemap-imports/with_url.scss`
// but this is a small bug in the extend utility of the source-map library
assert.deepEqual(mapData.sources, [
'integration/scss-sourcemap-imports/style.scss',
'integration/scss-sourcemap-imports/with_url.scss',
]);

let input = await inputFS.readFile(
path.join(path.dirname(filename), map.sourceRoot, map.sources[1]),
path.join(path.dirname(filename), map.sourceRoot, map.sources[0]),
'utf-8',
);

Expand Down
8 changes: 7 additions & 1 deletion packages/optimizers/css/src/CSSOptimizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import SourceMap from '@parcel/source-map';
import {Optimizer} from '@parcel/plugin';
import {
// $FlowFixMe - init for browser build.
import init, {
transform,
transformStyleAttribute,
browserslistToTargets,
Expand Down Expand Up @@ -155,6 +156,11 @@ Parcel\'s default CSS minifer changed from cssnano to lightningcss, but a "cssna
}
}

// $FlowFixMe
if (process.browser) {
await init();
}

let result = transform({
filename: bundle.name,
code,
Expand Down
5 changes: 5 additions & 0 deletions packages/packagers/css/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@
"@parcel/plugin": "2.11.0",
"@parcel/source-map": "^2.1.1",
"@parcel/utils": "2.11.0",
"lightningcss": "^1.22.1",
"nullthrows": "^1.1.1"
},
"devDependencies": {
"lightningcss-wasm": "^1.22.1",
"postcss": "^8.4.5"
},
"browser": {
"lightningcss": "lightningcss-wasm"
}
}
202 changes: 133 additions & 69 deletions packages/packagers/css/src/CSSPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
import type {Root} from 'postcss';
import type {Asset, Dependency} from '@parcel/types';
import typeof PostCSS from 'postcss';
// $FlowFixMe - init for browser build.
import init, {bundleAsync} from 'lightningcss';

import path from 'path';
import invariant from 'assert';
import nullthrows from 'nullthrows';
import SourceMap from '@parcel/source-map';
import {Packager} from '@parcel/plugin';
import {convertSourceLocationToHighlight} from '@parcel/diagnostic';
import {
PromiseQueue,
countLines,
replaceInlineReferences,
replaceURLReferences,
} from '@parcel/utils';

import nullthrows from 'nullthrows';

export default (new Packager({
async package({
bundle,
Expand All @@ -26,38 +26,68 @@ export default (new Packager({
logger,
options,
}) {
// Inline style attributes are parsed differently from full CSS files.
if (bundle.bundleBehavior === 'inline') {
let entry = bundle.getMainEntry();
if (entry?.meta.type === 'attr') {
return replaceReferences(
bundle,
bundleGraph,
await entry.getCode(),
await entry.getMap(),
getInlineBundleContents,
);
}
}

let queue = new PromiseQueue({
maxConcurrent: 32,
});
let hoistedImports = [];
let assetsByPlaceholder = new Map();
let entry = null;
let entryContents = '';

bundle.traverse({
enter: (node, context) => {
if (node.type === 'asset' && !context) {
// If there is only one entry, we'll use it directly.
// Otherwise, we'll create a fake bundle entry with @import rules for each root asset.
if (entry == null) {
entry = node.value.id;
} else {
entry = bundle.id;
}

assetsByPlaceholder.set(node.value.id, node.value);
entryContents += `@import "${node.value.id}";\n`;
}
return true;
},
exit: node => {
if (node.type === 'dependency') {
let resolved = bundleGraph.getResolvedAsset(node.value, bundle);

// Hoist unresolved external dependencies (i.e. http: imports)
if (
node.value.priority === 'sync' &&
!bundleGraph.isDependencySkipped(node.value) &&
!bundleGraph.getResolvedAsset(node.value, bundle)
!resolved
) {
hoistedImports.push(node.value.specifier);
}
return;
}

let asset = node.value;

// Figure out which media types this asset was imported with.
// We only want to import the asset once, so group them all together.
let media = [];
for (let dep of bundleGraph.getIncomingDependencies(asset)) {
if (!dep.meta.media) {
// Asset was imported without a media type. Don't wrap in @media.
media.length = 0;
break;
if (resolved && bundle.hasAsset(resolved)) {
assetsByPlaceholder.set(
node.value.meta.placeholder ?? node.value.specifier,
resolved,
);
}
media.push(dep.meta.media);

return;
}

let asset = node.value;
queue.add(() => {
if (
!asset.symbols.isCleared &&
Expand All @@ -71,7 +101,6 @@ export default (new Packager({
bundleGraph,
bundle,
asset,
media,
);
} else {
return Promise.all([
Expand Down Expand Up @@ -106,77 +135,117 @@ export default (new Packager({
}
}

if (media.length) {
return `@media ${media.join(', ')} {\n${css}\n}\n`;
}

return css;
}),
bundle.env.sourceMap && asset.getMapBuffer(),
bundle.env.sourceMap ? asset.getMap() : null,
]);
}
});
},
});

let outputs = await queue.run();
let contents = '';
let outputs = new Map(
(await queue.run()).map(([asset, code, map]) => [asset, [code, map]]),
);
let map = new SourceMap(options.projectRoot);
let lineOffset = 0;

for (let url of hoistedImports) {
contents += `@import "${url}";\n`;
lineOffset++;
// $FlowFixMe
if (process.browser) {
await init();
}

for (let [asset, code, mapBuffer] of outputs) {
contents += code + '\n';
if (bundle.env.sourceMap) {
if (mapBuffer) {
map.addBuffer(mapBuffer, lineOffset);
} else {
map.addEmptyMap(
path
.relative(options.projectRoot, asset.filePath)
.replace(/\\+/g, '/'),
code,
lineOffset,
);
}
let res = await bundleAsync({
filename: nullthrows(entry),
sourceMap: !!bundle.env.sourceMap,
resolver: {
resolve(specifier) {
return specifier;
},
async read(file) {
if (file === bundle.id) {
return entryContents;
}

lineOffset += countLines(code);
}
}
let asset = assetsByPlaceholder.get(file);
if (!asset) {
return '';
}
let [code, map] = nullthrows(outputs.get(asset));
if (map) {
let sm = await map.stringify({format: 'inline'});
invariant(typeof sm === 'string');
code += `\n/*# sourceMappingURL=${sm} */`;
}
return code;
},
},
});

if (bundle.env.sourceMap) {
let contents = res.code.toString();

if (res.map) {
let vlqMap = JSON.parse(res.map.toString());
map.addVLQMap(vlqMap);
let reference = await getSourceMapReference(map);
if (reference != null) {
contents += '/*# sourceMappingURL=' + reference + ' */\n';
}
}

({contents, map} = replaceURLReferences({
bundle,
bundleGraph,
contents,
map,
getReplacement: escapeString,
}));
// Prepend hoisted external imports.
if (hoistedImports.length > 0) {
let lineOffset = 0;
let hoistedCode = '';
for (let url of hoistedImports) {
hoistedCode += `@import "${url}";\n`;
lineOffset++;
}

return replaceInlineReferences({
if (bundle.env.sourceMap) {
map.offsetLines(1, lineOffset);
}

contents = hoistedCode + contents;
}

return replaceReferences(
bundle,
bundleGraph,
contents,
getInlineBundleContents,
getInlineReplacement: (dep, inlineType, contents) => ({
from: getSpecifier(dep),
to: escapeString(contents),
}),
map,
});
getInlineBundleContents,
);
},
}): Packager);

function replaceReferences(
bundle,
bundleGraph,
contents,
map,
getInlineBundleContents,
) {
({contents, map} = replaceURLReferences({
bundle,
bundleGraph,
contents,
map,
getReplacement: escapeString,
}));

return replaceInlineReferences({
bundle,
bundleGraph,
contents,
getInlineBundleContents,
getInlineReplacement: (dep, inlineType, contents) => ({
from: getSpecifier(dep),
to: escapeString(contents),
}),
map,
});
}

export function getSpecifier(dep: Dependency): string {
if (typeof dep.meta.placeholder === 'string') {
return dep.meta.placeholder;
Expand All @@ -195,8 +264,7 @@ async function processCSSModule(
bundleGraph,
bundle,
asset,
media,
): Promise<[Asset, string, ?Buffer]> {
): Promise<[Asset, string, ?SourceMap]> {
let postcss: PostCSS = await options.packageManager.require(
'postcss',
options.projectRoot + '/index',
Expand Down Expand Up @@ -276,11 +344,7 @@ async function processCSSModule(
sourceMap.addVLQMap(map.toJSON());
}

if (media.length) {
content = `@media ${media.join(', ')} {\n${content}\n}\n`;
}

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

function escapeDashedIdent(name) {
Expand Down

0 comments on commit cd447dc

Please sign in to comment.