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

perf(@angular-devkit/build-angular): use esbuild as a CSS optimizer for global styles #21535

Merged
merged 1 commit into from
Aug 10, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading