Skip to content

Commit

Permalink
perf(@angular-devkit/build-angular): use esbuild as a CSS optimizer f…
Browse files Browse the repository at this point in the history
…or 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.
  • Loading branch information
alan-agius4 committed Aug 10, 2021
1 parent 8954d11 commit cb7d156
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 575 deletions.
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -143,7 +143,6 @@
"core-js": "3.16.1",
"critters": "0.0.10",
"css-loader": "6.2.0",
"css-minimizer-webpack-plugin": "3.0.2",
"debug": "^4.1.1",
"esbuild": "0.12.19",
"eslint": "7.32.0",
Expand Down
1 change: 0 additions & 1 deletion packages/angular_devkit/build_angular/BUILD.bazel
Expand Up @@ -141,7 +141,6 @@ ts_library(
"@npm//core-js",
"@npm//critters",
"@npm//css-loader",
"@npm//css-minimizer-webpack-plugin",
"@npm//esbuild",
"@npm//find-cache-dir",
"@npm//glob",
Expand Down
1 change: 0 additions & 1 deletion packages/angular_devkit/build_angular/package.json
Expand Up @@ -32,7 +32,6 @@
"core-js": "3.16.1",
"critters": "0.0.10",
"css-loader": "6.2.0",
"css-minimizer-webpack-plugin": "3.0.2",
"esbuild": "0.12.19",
"find-cache-dir": "3.3.1",
"glob": "7.1.7",
Expand Down
Expand Up @@ -317,7 +317,7 @@ describe('Browser Builder styles', () => {

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

it('supports autoprefixer grid comments in SCSS with optimization true', async () => {
Expand Down
Expand Up @@ -13,13 +13,13 @@ import { ExtraEntryPoint } from '../../builders/browser/schema';
import { SassWorkerImplementation } from '../../sass/sass-service';
import { BuildBrowserFeatures } from '../../utils/build-browser-features';
import { WebpackConfigOptions } from '../../utils/build-options';
import { maxWorkers } from '../../utils/environment-options';
import {
AnyComponentStyleBudgetChecker,
PostcssCliResources,
RemoveHashPlugin,
SuppressExtractedTextChunksWebpackPlugin,
} from '../plugins';
import { CssOptimizerPlugin } from '../plugins/css-optimizer-plugin';
import {
assetNameTemplateFactory,
getOutputHashFormat,
Expand Down Expand Up @@ -261,71 +261,6 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio
},
];

const extraMinimizers = [];
if (buildOptions.optimization.styles.minify) {
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const esbuild = require('esbuild') as typeof import('esbuild');

const cssnanoOptions = {
preset: [
'default',
{
// Disable SVG optimizations, as this can cause optimizations which are not compatible in all browsers.
svgo: false,
// Disable `calc` optimizations, due to several issues. #16910, #16875, #17890
calc: false,
// 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
cssDeclarationSorter: false,
// Workaround for Critters as it doesn't work when `@media all {}` is minified to `@media {}`.
// TODO: Remove once they move to postcss.
minifyParams: !buildOptions.optimization.styles.inlineCritical,
},
],
};

const globalBundlesRegExp = new RegExp(
`^(${Object.keys(entryPoints).join('|')})(\.[0-9a-f]{20})?.css$`,
);

extraMinimizers.push(
// Component styles use esbuild which is faster and generates smaller files on average.
// esbuild does not yet support style sourcemaps but component style sourcemaps are not
// supported by the CLI when style minify is enabled.
new CssMinimizerPlugin({
// Component styles retain their original file name
test: /\.(?:css|scss|sass|less|styl)$/,
exclude: globalBundlesRegExp,
parallel: false,
minify: async (data: string) => {
const [[sourcefile, input]] = Object.entries(data);
const { code, warnings } = await esbuild.transform(input, {
loader: 'css',
minify: true,
sourcefile,
});

return {
code,
warnings:
warnings.length > 0
? await esbuild.formatMessages(warnings, { kind: 'warning' })
: [],
};
},
}),
// Global styles use cssnano since sourcemap support is required even when minify
// is enabled. Once esbuild supports style sourcemaps this can be changed.
// esbuild stylesheet source map support issue: https://github.com/evanw/esbuild/issues/519
new CssMinimizerPlugin({
test: /\.css$/,
include: globalBundlesRegExp,
parallel: maxWorkers,
minify: [CssMinimizerPlugin.cssnanoMinify],
minimizerOptions: cssnanoOptions,
}),
);
}

const styleLanguages: {
extensions: string[];
use: webpack.RuleSetUseItem[];
Expand Down Expand Up @@ -460,7 +395,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio
})),
},
optimization: {
minimizer: extraMinimizers,
minimizer: buildOptions.optimization.styles.minify ? [new CssOptimizerPlugin()] : undefined,
},
plugins: extraPlugins,
};
Expand Down
@@ -0,0 +1,113 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { Message, formatMessages, transform } from 'esbuild';
import type { Compilation, Compiler, sources } from 'webpack';
import { addWarning } from '../../utils/webpack-diagnostics';
/**
* The name of the plugin provided to Webpack when tapping Webpack compiler hooks.
*/
const PLUGIN_NAME = 'angular-css-optimizer';

/**
* A Webpack plugin that provides CSS optimization capabilities.
*
* The plugin uses both `esbuild` to provide both fast and highly-optimized
* code output.
*/
export class CssOptimizerPlugin {
constructor() {}

apply(compiler: Compiler) {
const { OriginalSource, SourceMapSource } = compiler.webpack.sources;

compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.processAssets.tapPromise(
{
name: PLUGIN_NAME,
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
},
async (compilationAssets) => {
const cache = compilation.options.cache && compilation.getCache(PLUGIN_NAME);

for (const assetName of Object.keys(compilationAssets)) {
if (!/\.(?:css|scss|sass|less|styl)$/.test(assetName)) {
continue;
}

const asset = compilation.getAsset(assetName);
// Skip assets that have already been optimized or are verbatim copies (project assets)
if (!asset || asset.info.minimized || asset.info.copied) {
continue;
}

const { source: styleAssetSource, name } = asset;
let cacheItem;

if (cache) {
const eTag = cache.getLazyHashedEtag(styleAssetSource);
cacheItem = cache.getItemCache(name, eTag);
const cachedOutput = await cacheItem.getPromise<
{ source: sources.Source; warnings: Message[] } | undefined
>();

if (cachedOutput) {
await this.addWarnings(compilation, cachedOutput.warnings);
compilation.updateAsset(name, cachedOutput.source, {
minimized: true,
});
continue;
}
}

const { source, map: inputMap } = styleAssetSource.sourceAndMap();
let sourceMapLine;
if (inputMap) {
// esbuild will automatically remap the sourcemap if provided
sourceMapLine = `\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(
JSON.stringify(inputMap),
).toString('base64')} */`;
}

const input = typeof source === 'string' ? source : source.toString();
const { code, warnings, map } = await transform(
sourceMapLine ? input + sourceMapLine : input,
{
loader: 'css',
legalComments: 'inline',
minify: true,
sourcemap: !!inputMap && 'external',
sourcefile: asset.name,
},
);

await this.addWarnings(compilation, warnings);

const optimizedAsset = map
? new SourceMapSource(code, name, map)
: new OriginalSource(code, name);
compilation.updateAsset(name, optimizedAsset, { minimized: true });

await cacheItem?.storePromise({
source: optimizedAsset,
warnings,
});
}
},
);
});
}

private async addWarnings(compilation: Compilation, warnings: Message[]) {
if (warnings.length > 0) {
for (const warning of await formatMessages(warnings, { kind: 'warning' })) {
addWarning(compilation, warning);
}
}
}
}
2 changes: 1 addition & 1 deletion tests/legacy-cli/e2e/tests/third-party/bootstrap.ts
Expand Up @@ -41,7 +41,7 @@ export default function () {
),
)
.then(() => expectFileToMatch('dist/test-project/scripts.js', 'jQuery'))
.then(() => expectFileToMatch('dist/test-project/styles.css', '* Bootstrap'))
.then(() => expectFileToMatch('dist/test-project/styles.css', ':root'))
.then(() =>
expectFileToMatch(
'dist/test-project/index.html',
Expand Down

0 comments on commit cb7d156

Please sign in to comment.