Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): initial autoprefixer support for…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
clydin authored and angular-robot[bot] committed Mar 15, 2023
1 parent b228870 commit f04859d
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* @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 createAutoPrefixerPlugin from 'autoprefixer';
import type { OnLoadResult, Plugin, PluginBuild } from 'esbuild';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';

/**
* The lazy-loaded instance of the postcss stylesheet postprocessor.
* It is only imported and initialized if postcss is needed.
*/
let postcss: typeof import('postcss')['default'] | undefined;

/**
* An object containing the plugin options to use when processing CSS stylesheets.
*/
export interface CssPluginOptions {
/**
* Controls the use and creation of sourcemaps when processing the stylesheets.
* If true, sourcemap processing is enabled; if false, disabled.
*/
sourcemap: boolean;
/**
* Optional component data for any inline styles from Component decorator `styles` fields.
* The key is an internal angular resource URI and the value is the stylesheet content.
*/
inlineComponentData?: Record<string, string>;
/**
* The browsers to support in browserslist format when processing stylesheets.
* Some postcss plugins such as autoprefixer require the raw browserslist information instead
* of the esbuild formatted target.
*/
browsers: string[];
}

/**
* Creates an esbuild plugin to process CSS stylesheets.
* @param options An object containing the plugin options.
* @returns An esbuild Plugin instance.
*/
export function createCssPlugin(options: CssPluginOptions): Plugin {
return {
name: 'angular-css',
async setup(build: PluginBuild): Promise<void> {
const autoprefixer = createAutoPrefixerPlugin({
overrideBrowserslist: options.browsers,
ignoreUnknownVersions: true,
});

// Autoprefixer currently does not contain a method to check if autoprefixer is required
// based on the provided list of browsers. However, it does contain a method that returns
// informational text that can be used as a replacement. The text "Awesome!" will be present
// when autoprefixer determines no actions are needed.
// ref: https://github.com/postcss/autoprefixer/blob/e2f5c26ff1f3eaca95a21873723ce1cdf6e59f0e/lib/info.js#L118
const autoprefixerInfo = autoprefixer.info({ from: build.initialOptions.absWorkingDir });
const skipAutoprefixer = autoprefixerInfo.includes('Awesome!');

if (skipAutoprefixer) {
return;
}

postcss ??= (await import('postcss')).default;
const postcssProcessor = postcss([autoprefixer]);

// Add a load callback to support inline Component styles
build.onLoad({ filter: /^css;/, namespace: 'angular:styles/component' }, async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(data, `component style name should always be found [${args.path}]`);

const [, , filePath] = args.path.split(';', 3);

return compileString(data, filePath, postcssProcessor, options);
});

// Add a load callback to support files from disk
build.onLoad({ filter: /\.css$/ }, async (args) => {
const data = await readFile(args.path, 'utf-8');

return compileString(data, args.path, postcssProcessor, options);
});
},
};
}

/**
* Compiles the provided CSS stylesheet data using a provided postcss processor and provides an
* esbuild load result that can be used directly by an esbuild Plugin.
* @param data The stylesheet content to process.
* @param filename The name of the file that contains the data.
* @param postcssProcessor A postcss processor instance to use.
* @param options The plugin options to control the processing.
* @returns An esbuild OnLoaderResult object with the processed content, warnings, and/or errors.
*/
async function compileString(
data: string,
filename: string,
postcssProcessor: import('postcss').Processor,
options: CssPluginOptions,
): Promise<OnLoadResult> {
try {
const result = await postcssProcessor.process(data, {
from: filename,
to: filename,
map: options.sourcemap && {
inline: true,
sourcesContent: true,
},
});

const rawWarnings = result.warnings();
let warnings;
if (rawWarnings.length > 0) {
const lineMappings = new Map<string, string[] | null>();
warnings = rawWarnings.map((warning) => {
const file = warning.node.source?.input.file;
if (file === undefined) {
return { text: warning.text };
}

let lines = lineMappings.get(file);
if (lines === undefined) {
lines = warning.node.source?.input.css.split(/\r?\n/);
lineMappings.set(file, lines ?? null);
}

return {
text: warning.text,
location: {
file,
line: warning.line,
column: warning.column - 1,
lineText: lines?.[warning.line - 1],
},
};
});
}

return {
contents: result.css,
loader: 'css',
warnings,
};
} catch (error) {
postcss ??= (await import('postcss')).default;
if (error instanceof postcss.CssSyntaxError) {
const lines = error.source?.split(/\r?\n/);

return {
errors: [
{
text: error.reason,
location: {
file: error.file,
line: error.line,
column: error.column && error.column - 1,
lineText: error.line === undefined ? undefined : lines?.[error.line - 1],
},
},
],
};
}

throw error;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@ async function execute(
indexHtmlOptions,
} = options;

const target = transformSupportedBrowsersToTargets(
getSupportedBrowsers(projectRoot, context.logger),
);
const browsers = getSupportedBrowsers(projectRoot, context.logger);
const target = transformSupportedBrowsersToTargets(browsers);

// Reuse rebuild state or create new bundle contexts for code and global stylesheets
const codeBundleCache = options.watch
Expand All @@ -100,14 +99,14 @@ async function execute(
new BundlerContext(
workspaceRoot,
!!options.watch,
createCodeBundleOptions(options, target, codeBundleCache),
createCodeBundleOptions(options, target, browsers, codeBundleCache),
);
const globalStylesBundleContext =
rebuildState?.globalStylesRebuild ??
new BundlerContext(
workspaceRoot,
!!options.watch,
createGlobalStylesBundleOptions(options, target),
createGlobalStylesBundleOptions(options, target, browsers),
);

const [codeResults, styleResults] = await Promise.all([
Expand Down Expand Up @@ -269,6 +268,7 @@ function createOutputFileFromText(path: string, text: string): OutputFile {
function createCodeBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
browsers: string[],
sourceFileCache?: SourceFileCache,
): BuildOptions {
const {
Expand Down Expand Up @@ -338,6 +338,7 @@ function createCodeBundleOptions(
externalDependencies,
target,
inlineStyleLanguage,
browsers,
},
),
],
Expand Down Expand Up @@ -405,6 +406,7 @@ function getFeatureSupport(target: string[]): BuildOptions['supported'] {
function createGlobalStylesBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
browsers: string[],
): BuildOptions {
const {
workspaceRoot,
Expand All @@ -415,7 +417,6 @@ function createGlobalStylesBundleOptions(
preserveSymlinks,
externalDependencies,
stylePreprocessorOptions,
watch,
} = options;

const buildOptions = createStylesheetBundleOptions({
Expand All @@ -427,6 +428,7 @@ function createGlobalStylesBundleOptions(
externalDependencies,
outputNames,
includePaths: stylePreprocessorOptions?.includePaths,
browsers,
});
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

import type { BuildOptions, OutputFile } from 'esbuild';
import * as path from 'node:path';
import path from 'node:path';
import { createCssPlugin } from './css-plugin';
import { createCssResourcePlugin } from './css-resource-plugin';
import { BundlerContext } from './esbuild';
import { createLessPlugin } from './less-plugin';
Expand All @@ -27,6 +28,7 @@ export interface BundleStylesheetOptions {
includePaths?: string[];
externalDependencies?: string[];
target: string[];
browsers: string[];
}

export function createStylesheetBundleOptions(
Expand Down Expand Up @@ -66,6 +68,11 @@ export function createStylesheetBundleOptions(
includePaths,
inlineComponentData,
}),
createCssPlugin({
sourcemap: !!options.sourcemap,
inlineComponentData,
browsers: options.browsers,
}),
createCssResourcePlugin(),
],
};
Expand Down
Loading

0 comments on commit f04859d

Please sign in to comment.