Skip to content

Commit f04859d

Browse files
clydinangular-robot[bot]
authored andcommitted
feat(@angular-devkit/build-angular): initial autoprefixer support for CSS in esbuild builder
When using the experimental esbuild-based browser application builder, CSS stylesheets will now be processed by the postcss autoprefixer plugin. The autoprefixer plugin will only be used if the browsers provided by browserslist require prefixes to be added. This avoids unnecessary stylesheet parsing and processing if no additional prefixes are needed. Currently, only CSS stylesheets are processed. Preprocessor support including Sass and Less will be added in a future change.
1 parent b228870 commit f04859d

File tree

5 files changed

+387
-8
lines changed

5 files changed

+387
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 createAutoPrefixerPlugin from 'autoprefixer';
10+
import type { OnLoadResult, Plugin, PluginBuild } from 'esbuild';
11+
import assert from 'node:assert';
12+
import { readFile } from 'node:fs/promises';
13+
14+
/**
15+
* The lazy-loaded instance of the postcss stylesheet postprocessor.
16+
* It is only imported and initialized if postcss is needed.
17+
*/
18+
let postcss: typeof import('postcss')['default'] | undefined;
19+
20+
/**
21+
* An object containing the plugin options to use when processing CSS stylesheets.
22+
*/
23+
export interface CssPluginOptions {
24+
/**
25+
* Controls the use and creation of sourcemaps when processing the stylesheets.
26+
* If true, sourcemap processing is enabled; if false, disabled.
27+
*/
28+
sourcemap: boolean;
29+
/**
30+
* Optional component data for any inline styles from Component decorator `styles` fields.
31+
* The key is an internal angular resource URI and the value is the stylesheet content.
32+
*/
33+
inlineComponentData?: Record<string, string>;
34+
/**
35+
* The browsers to support in browserslist format when processing stylesheets.
36+
* Some postcss plugins such as autoprefixer require the raw browserslist information instead
37+
* of the esbuild formatted target.
38+
*/
39+
browsers: string[];
40+
}
41+
42+
/**
43+
* Creates an esbuild plugin to process CSS stylesheets.
44+
* @param options An object containing the plugin options.
45+
* @returns An esbuild Plugin instance.
46+
*/
47+
export function createCssPlugin(options: CssPluginOptions): Plugin {
48+
return {
49+
name: 'angular-css',
50+
async setup(build: PluginBuild): Promise<void> {
51+
const autoprefixer = createAutoPrefixerPlugin({
52+
overrideBrowserslist: options.browsers,
53+
ignoreUnknownVersions: true,
54+
});
55+
56+
// Autoprefixer currently does not contain a method to check if autoprefixer is required
57+
// based on the provided list of browsers. However, it does contain a method that returns
58+
// informational text that can be used as a replacement. The text "Awesome!" will be present
59+
// when autoprefixer determines no actions are needed.
60+
// ref: https://github.com/postcss/autoprefixer/blob/e2f5c26ff1f3eaca95a21873723ce1cdf6e59f0e/lib/info.js#L118
61+
const autoprefixerInfo = autoprefixer.info({ from: build.initialOptions.absWorkingDir });
62+
const skipAutoprefixer = autoprefixerInfo.includes('Awesome!');
63+
64+
if (skipAutoprefixer) {
65+
return;
66+
}
67+
68+
postcss ??= (await import('postcss')).default;
69+
const postcssProcessor = postcss([autoprefixer]);
70+
71+
// Add a load callback to support inline Component styles
72+
build.onLoad({ filter: /^css;/, namespace: 'angular:styles/component' }, async (args) => {
73+
const data = options.inlineComponentData?.[args.path];
74+
assert(data, `component style name should always be found [${args.path}]`);
75+
76+
const [, , filePath] = args.path.split(';', 3);
77+
78+
return compileString(data, filePath, postcssProcessor, options);
79+
});
80+
81+
// Add a load callback to support files from disk
82+
build.onLoad({ filter: /\.css$/ }, async (args) => {
83+
const data = await readFile(args.path, 'utf-8');
84+
85+
return compileString(data, args.path, postcssProcessor, options);
86+
});
87+
},
88+
};
89+
}
90+
91+
/**
92+
* Compiles the provided CSS stylesheet data using a provided postcss processor and provides an
93+
* esbuild load result that can be used directly by an esbuild Plugin.
94+
* @param data The stylesheet content to process.
95+
* @param filename The name of the file that contains the data.
96+
* @param postcssProcessor A postcss processor instance to use.
97+
* @param options The plugin options to control the processing.
98+
* @returns An esbuild OnLoaderResult object with the processed content, warnings, and/or errors.
99+
*/
100+
async function compileString(
101+
data: string,
102+
filename: string,
103+
postcssProcessor: import('postcss').Processor,
104+
options: CssPluginOptions,
105+
): Promise<OnLoadResult> {
106+
try {
107+
const result = await postcssProcessor.process(data, {
108+
from: filename,
109+
to: filename,
110+
map: options.sourcemap && {
111+
inline: true,
112+
sourcesContent: true,
113+
},
114+
});
115+
116+
const rawWarnings = result.warnings();
117+
let warnings;
118+
if (rawWarnings.length > 0) {
119+
const lineMappings = new Map<string, string[] | null>();
120+
warnings = rawWarnings.map((warning) => {
121+
const file = warning.node.source?.input.file;
122+
if (file === undefined) {
123+
return { text: warning.text };
124+
}
125+
126+
let lines = lineMappings.get(file);
127+
if (lines === undefined) {
128+
lines = warning.node.source?.input.css.split(/\r?\n/);
129+
lineMappings.set(file, lines ?? null);
130+
}
131+
132+
return {
133+
text: warning.text,
134+
location: {
135+
file,
136+
line: warning.line,
137+
column: warning.column - 1,
138+
lineText: lines?.[warning.line - 1],
139+
},
140+
};
141+
});
142+
}
143+
144+
return {
145+
contents: result.css,
146+
loader: 'css',
147+
warnings,
148+
};
149+
} catch (error) {
150+
postcss ??= (await import('postcss')).default;
151+
if (error instanceof postcss.CssSyntaxError) {
152+
const lines = error.source?.split(/\r?\n/);
153+
154+
return {
155+
errors: [
156+
{
157+
text: error.reason,
158+
location: {
159+
file: error.file,
160+
line: error.line,
161+
column: error.column && error.column - 1,
162+
lineText: error.line === undefined ? undefined : lines?.[error.line - 1],
163+
},
164+
},
165+
],
166+
};
167+
}
168+
169+
throw error;
170+
}
171+
}

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,8 @@ async function execute(
8787
indexHtmlOptions,
8888
} = options;
8989

90-
const target = transformSupportedBrowsersToTargets(
91-
getSupportedBrowsers(projectRoot, context.logger),
92-
);
90+
const browsers = getSupportedBrowsers(projectRoot, context.logger);
91+
const target = transformSupportedBrowsersToTargets(browsers);
9392

9493
// Reuse rebuild state or create new bundle contexts for code and global stylesheets
9594
const codeBundleCache = options.watch
@@ -100,14 +99,14 @@ async function execute(
10099
new BundlerContext(
101100
workspaceRoot,
102101
!!options.watch,
103-
createCodeBundleOptions(options, target, codeBundleCache),
102+
createCodeBundleOptions(options, target, browsers, codeBundleCache),
104103
);
105104
const globalStylesBundleContext =
106105
rebuildState?.globalStylesRebuild ??
107106
new BundlerContext(
108107
workspaceRoot,
109108
!!options.watch,
110-
createGlobalStylesBundleOptions(options, target),
109+
createGlobalStylesBundleOptions(options, target, browsers),
111110
);
112111

113112
const [codeResults, styleResults] = await Promise.all([
@@ -269,6 +268,7 @@ function createOutputFileFromText(path: string, text: string): OutputFile {
269268
function createCodeBundleOptions(
270269
options: NormalizedBrowserOptions,
271270
target: string[],
271+
browsers: string[],
272272
sourceFileCache?: SourceFileCache,
273273
): BuildOptions {
274274
const {
@@ -338,6 +338,7 @@ function createCodeBundleOptions(
338338
externalDependencies,
339339
target,
340340
inlineStyleLanguage,
341+
browsers,
341342
},
342343
),
343344
],
@@ -405,6 +406,7 @@ function getFeatureSupport(target: string[]): BuildOptions['supported'] {
405406
function createGlobalStylesBundleOptions(
406407
options: NormalizedBrowserOptions,
407408
target: string[],
409+
browsers: string[],
408410
): BuildOptions {
409411
const {
410412
workspaceRoot,
@@ -415,7 +417,6 @@ function createGlobalStylesBundleOptions(
415417
preserveSymlinks,
416418
externalDependencies,
417419
stylePreprocessorOptions,
418-
watch,
419420
} = options;
420421

421422
const buildOptions = createStylesheetBundleOptions({
@@ -427,6 +428,7 @@ function createGlobalStylesBundleOptions(
427428
externalDependencies,
428429
outputNames,
429430
includePaths: stylePreprocessorOptions?.includePaths,
431+
browsers,
430432
});
431433
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';
432434

packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
*/
88

99
import type { BuildOptions, OutputFile } from 'esbuild';
10-
import * as path from 'node:path';
10+
import path from 'node:path';
11+
import { createCssPlugin } from './css-plugin';
1112
import { createCssResourcePlugin } from './css-resource-plugin';
1213
import { BundlerContext } from './esbuild';
1314
import { createLessPlugin } from './less-plugin';
@@ -27,6 +28,7 @@ export interface BundleStylesheetOptions {
2728
includePaths?: string[];
2829
externalDependencies?: string[];
2930
target: string[];
31+
browsers: string[];
3032
}
3133

3234
export function createStylesheetBundleOptions(
@@ -66,6 +68,11 @@ export function createStylesheetBundleOptions(
6668
includePaths,
6769
inlineComponentData,
6870
}),
71+
createCssPlugin({
72+
sourcemap: !!options.sourcemap,
73+
inlineComponentData,
74+
browsers: options.browsers,
75+
}),
6976
createCssResourcePlugin(),
7077
],
7178
};

0 commit comments

Comments
 (0)