Skip to content

Commit cb7d156

Browse files
committed
perf(@angular-devkit/build-angular): use esbuild as a CSS optimizer for global styles
Esbuild now support CSS sourcemaps which now makes it possible to be used to optimize global CSS. With this change we also reduce the amount of dependencies by removing `css-minimizer-webpack-plugin` which brings in a number of transitive depedencies which we no longer use.
1 parent 8954d11 commit cb7d156

File tree

8 files changed

+124
-575
lines changed

8 files changed

+124
-575
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@
143143
"core-js": "3.16.1",
144144
"critters": "0.0.10",
145145
"css-loader": "6.2.0",
146-
"css-minimizer-webpack-plugin": "3.0.2",
147146
"debug": "^4.1.1",
148147
"esbuild": "0.12.19",
149148
"eslint": "7.32.0",

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ ts_library(
141141
"@npm//core-js",
142142
"@npm//critters",
143143
"@npm//css-loader",
144-
"@npm//css-minimizer-webpack-plugin",
145144
"@npm//esbuild",
146145
"@npm//find-cache-dir",
147146
"@npm//glob",

packages/angular_devkit/build_angular/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"core-js": "3.16.1",
3333
"critters": "0.0.10",
3434
"css-loader": "6.2.0",
35-
"css-minimizer-webpack-plugin": "3.0.2",
3635
"esbuild": "0.12.19",
3736
"find-cache-dir": "3.3.1",
3837
"glob": "7.1.7",

packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ describe('Browser Builder styles', () => {
317317

318318
const overrides = { optimization: true };
319319
const { files } = await browserBuild(architect, host, target, overrides);
320-
expect(await files['styles.css']).toContain('/*! important-comment */div{flex:1}');
320+
expect(await files['styles.css']).not.toContain('/*! important-comment */');
321321
});
322322

323323
it('supports autoprefixer grid comments in SCSS with optimization true', async () => {

packages/angular_devkit/build_angular/src/webpack/configs/styles.ts

Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import { ExtraEntryPoint } from '../../builders/browser/schema';
1313
import { SassWorkerImplementation } from '../../sass/sass-service';
1414
import { BuildBrowserFeatures } from '../../utils/build-browser-features';
1515
import { WebpackConfigOptions } from '../../utils/build-options';
16-
import { maxWorkers } from '../../utils/environment-options';
1716
import {
1817
AnyComponentStyleBudgetChecker,
1918
PostcssCliResources,
2019
RemoveHashPlugin,
2120
SuppressExtractedTextChunksWebpackPlugin,
2221
} from '../plugins';
22+
import { CssOptimizerPlugin } from '../plugins/css-optimizer-plugin';
2323
import {
2424
assetNameTemplateFactory,
2525
getOutputHashFormat,
@@ -261,71 +261,6 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio
261261
},
262262
];
263263

264-
const extraMinimizers = [];
265-
if (buildOptions.optimization.styles.minify) {
266-
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
267-
const esbuild = require('esbuild') as typeof import('esbuild');
268-
269-
const cssnanoOptions = {
270-
preset: [
271-
'default',
272-
{
273-
// Disable SVG optimizations, as this can cause optimizations which are not compatible in all browsers.
274-
svgo: false,
275-
// Disable `calc` optimizations, due to several issues. #16910, #16875, #17890
276-
calc: false,
277-
// Disable CSS rules sorted due to several issues #20693, https://github.com/ionic-team/ionic-framework/issues/23266 and https://github.com/cssnano/cssnano/issues/1054
278-
cssDeclarationSorter: false,
279-
// Workaround for Critters as it doesn't work when `@media all {}` is minified to `@media {}`.
280-
// TODO: Remove once they move to postcss.
281-
minifyParams: !buildOptions.optimization.styles.inlineCritical,
282-
},
283-
],
284-
};
285-
286-
const globalBundlesRegExp = new RegExp(
287-
`^(${Object.keys(entryPoints).join('|')})(\.[0-9a-f]{20})?.css$`,
288-
);
289-
290-
extraMinimizers.push(
291-
// Component styles use esbuild which is faster and generates smaller files on average.
292-
// esbuild does not yet support style sourcemaps but component style sourcemaps are not
293-
// supported by the CLI when style minify is enabled.
294-
new CssMinimizerPlugin({
295-
// Component styles retain their original file name
296-
test: /\.(?:css|scss|sass|less|styl)$/,
297-
exclude: globalBundlesRegExp,
298-
parallel: false,
299-
minify: async (data: string) => {
300-
const [[sourcefile, input]] = Object.entries(data);
301-
const { code, warnings } = await esbuild.transform(input, {
302-
loader: 'css',
303-
minify: true,
304-
sourcefile,
305-
});
306-
307-
return {
308-
code,
309-
warnings:
310-
warnings.length > 0
311-
? await esbuild.formatMessages(warnings, { kind: 'warning' })
312-
: [],
313-
};
314-
},
315-
}),
316-
// Global styles use cssnano since sourcemap support is required even when minify
317-
// is enabled. Once esbuild supports style sourcemaps this can be changed.
318-
// esbuild stylesheet source map support issue: https://github.com/evanw/esbuild/issues/519
319-
new CssMinimizerPlugin({
320-
test: /\.css$/,
321-
include: globalBundlesRegExp,
322-
parallel: maxWorkers,
323-
minify: [CssMinimizerPlugin.cssnanoMinify],
324-
minimizerOptions: cssnanoOptions,
325-
}),
326-
);
327-
}
328-
329264
const styleLanguages: {
330265
extensions: string[];
331266
use: webpack.RuleSetUseItem[];
@@ -460,7 +395,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio
460395
})),
461396
},
462397
optimization: {
463-
minimizer: extraMinimizers,
398+
minimizer: buildOptions.optimization.styles.minify ? [new CssOptimizerPlugin()] : undefined,
464399
},
465400
plugins: extraPlugins,
466401
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { Message, formatMessages, transform } from 'esbuild';
10+
import type { Compilation, Compiler, sources } from 'webpack';
11+
import { addWarning } from '../../utils/webpack-diagnostics';
12+
/**
13+
* The name of the plugin provided to Webpack when tapping Webpack compiler hooks.
14+
*/
15+
const PLUGIN_NAME = 'angular-css-optimizer';
16+
17+
/**
18+
* A Webpack plugin that provides CSS optimization capabilities.
19+
*
20+
* The plugin uses both `esbuild` to provide both fast and highly-optimized
21+
* code output.
22+
*/
23+
export class CssOptimizerPlugin {
24+
constructor() {}
25+
26+
apply(compiler: Compiler) {
27+
const { OriginalSource, SourceMapSource } = compiler.webpack.sources;
28+
29+
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
30+
compilation.hooks.processAssets.tapPromise(
31+
{
32+
name: PLUGIN_NAME,
33+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
34+
},
35+
async (compilationAssets) => {
36+
const cache = compilation.options.cache && compilation.getCache(PLUGIN_NAME);
37+
38+
for (const assetName of Object.keys(compilationAssets)) {
39+
if (!/\.(?:css|scss|sass|less|styl)$/.test(assetName)) {
40+
continue;
41+
}
42+
43+
const asset = compilation.getAsset(assetName);
44+
// Skip assets that have already been optimized or are verbatim copies (project assets)
45+
if (!asset || asset.info.minimized || asset.info.copied) {
46+
continue;
47+
}
48+
49+
const { source: styleAssetSource, name } = asset;
50+
let cacheItem;
51+
52+
if (cache) {
53+
const eTag = cache.getLazyHashedEtag(styleAssetSource);
54+
cacheItem = cache.getItemCache(name, eTag);
55+
const cachedOutput = await cacheItem.getPromise<
56+
{ source: sources.Source; warnings: Message[] } | undefined
57+
>();
58+
59+
if (cachedOutput) {
60+
await this.addWarnings(compilation, cachedOutput.warnings);
61+
compilation.updateAsset(name, cachedOutput.source, {
62+
minimized: true,
63+
});
64+
continue;
65+
}
66+
}
67+
68+
const { source, map: inputMap } = styleAssetSource.sourceAndMap();
69+
let sourceMapLine;
70+
if (inputMap) {
71+
// esbuild will automatically remap the sourcemap if provided
72+
sourceMapLine = `\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(
73+
JSON.stringify(inputMap),
74+
).toString('base64')} */`;
75+
}
76+
77+
const input = typeof source === 'string' ? source : source.toString();
78+
const { code, warnings, map } = await transform(
79+
sourceMapLine ? input + sourceMapLine : input,
80+
{
81+
loader: 'css',
82+
legalComments: 'inline',
83+
minify: true,
84+
sourcemap: !!inputMap && 'external',
85+
sourcefile: asset.name,
86+
},
87+
);
88+
89+
await this.addWarnings(compilation, warnings);
90+
91+
const optimizedAsset = map
92+
? new SourceMapSource(code, name, map)
93+
: new OriginalSource(code, name);
94+
compilation.updateAsset(name, optimizedAsset, { minimized: true });
95+
96+
await cacheItem?.storePromise({
97+
source: optimizedAsset,
98+
warnings,
99+
});
100+
}
101+
},
102+
);
103+
});
104+
}
105+
106+
private async addWarnings(compilation: Compilation, warnings: Message[]) {
107+
if (warnings.length > 0) {
108+
for (const warning of await formatMessages(warnings, { kind: 'warning' })) {
109+
addWarning(compilation, warning);
110+
}
111+
}
112+
}
113+
}

tests/legacy-cli/e2e/tests/third-party/bootstrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default function () {
4141
),
4242
)
4343
.then(() => expectFileToMatch('dist/test-project/scripts.js', 'jQuery'))
44-
.then(() => expectFileToMatch('dist/test-project/styles.css', '* Bootstrap'))
44+
.then(() => expectFileToMatch('dist/test-project/styles.css', ':root'))
4545
.then(() =>
4646
expectFileToMatch(
4747
'dist/test-project/index.html',

0 commit comments

Comments
 (0)