Skip to content

Commit 2361cc3

Browse files
committed
feat(@angular-devkit/build-angular): switch to use Sass modern API
Sass modern API provides faster compilations times when used in an async manner. Users can temporary opt-out from using the modern API by setting `NG_BUILD_LEGACY_SASS` to `true` or `1`. Application compilation duration | Sass API and Compiler -- | -- 60852ms | dart-sass legacy sync API 52666ms | dart-sass modern API Note: https://github.com/johannesjo/super-productivity was used for benchmarking. Prior art: http://docs/document/d/1CvEceWMpBoEBd8SfvksGMdVHxaZMH93b0EGS3XbR3_Q?resourcekey=0-vFm-xMspT65FZLIyX7xWFQ
1 parent e995bda commit 2361cc3

File tree

9 files changed

+175
-99
lines changed

9 files changed

+175
-99
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@
146146
"eslint-plugin-header": "3.1.1",
147147
"eslint-plugin-import": "2.26.0",
148148
"express": "4.18.1",
149-
"font-awesome": "^4.7.0",
150149
"glob": "8.0.3",
151150
"http-proxy": "^1.18.1",
152151
"https-proxy-agent": "5.0.1",

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,6 @@ LARGE_SPECS = {
341341
"@npm//@angular/animations",
342342
"@npm//@angular/material",
343343
"@npm//bootstrap",
344-
"@npm//font-awesome",
345344
"@npm//jquery",
346345
"@npm//popper.js",
347346
],

packages/angular_devkit/build_angular/src/builders/browser-esbuild/sass-plugin.ts

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,73 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import type { Plugin, PluginBuild } from 'esbuild';
10-
import type { LegacyResult } from 'sass';
11-
import { SassWorkerImplementation } from '../../sass/sass-service';
9+
import type { PartialMessage, Plugin, PluginBuild } from 'esbuild';
10+
import type { CompileResult } from 'sass';
11+
import { fileURLToPath } from 'url';
1212

13-
export function createSassPlugin(options: { sourcemap: boolean; includePaths?: string[] }): Plugin {
13+
export function createSassPlugin(options: { sourcemap: boolean; loadPaths?: string[] }): Plugin {
1414
return {
1515
name: 'angular-sass',
1616
setup(build: PluginBuild): void {
17-
let sass: SassWorkerImplementation;
17+
let sass: typeof import('sass');
1818

19-
build.onStart(() => {
20-
sass = new SassWorkerImplementation();
21-
});
22-
23-
build.onEnd(() => {
24-
sass?.close();
19+
build.onStart(async () => {
20+
// Lazily load Sass
21+
sass = await import('sass');
2522
});
2623

2724
build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => {
28-
const result = await new Promise<LegacyResult>((resolve, reject) => {
29-
sass.render(
30-
{
31-
file: args.path,
32-
includePaths: options.includePaths,
33-
indentedSyntax: args.path.endsWith('.sass'),
34-
outputStyle: 'expanded',
35-
sourceMap: options.sourcemap,
36-
sourceMapContents: options.sourcemap,
37-
sourceMapEmbed: options.sourcemap,
38-
quietDeps: true,
39-
},
40-
(error, result) => {
41-
if (error) {
42-
reject(error);
43-
}
44-
if (result) {
45-
resolve(result);
46-
}
25+
try {
26+
const warnings: PartialMessage[] = [];
27+
const { css, sourceMap, loadedUrls } = await sass.compileAsync(args.path, {
28+
style: 'expanded',
29+
loadPaths: options.loadPaths,
30+
sourceMap: options.sourcemap,
31+
sourceMapIncludeSources: options.sourcemap,
32+
quietDeps: true,
33+
logger: {
34+
warn: (text, _options) => {
35+
warnings.push({
36+
text,
37+
});
38+
},
4739
},
48-
);
49-
});
50-
51-
return {
52-
contents: result.css,
53-
loader: 'css',
54-
watchFiles: result.stats.includedFiles,
55-
};
40+
});
41+
42+
return {
43+
loader: 'css',
44+
contents: css + sourceMapToUrlComment(sourceMap),
45+
watchFiles: loadedUrls.map((url) => fileURLToPath(url)),
46+
warnings,
47+
};
48+
} catch (error) {
49+
if (error instanceof sass.Exception) {
50+
const file = error.span.url ? fileURLToPath(error.span.url) : undefined;
51+
52+
return {
53+
loader: 'css',
54+
errors: [
55+
{
56+
text: error.toString(),
57+
},
58+
],
59+
watchFiles: file ? [file] : undefined,
60+
};
61+
}
62+
63+
throw error;
64+
}
5665
});
5766
},
5867
};
5968
}
69+
70+
function sourceMapToUrlComment(sourceMap: CompileResult['sourceMap']): string {
71+
if (!sourceMap) {
72+
return '';
73+
}
74+
75+
const urlSourceMap = Buffer.from(JSON.stringify(sourceMap), 'utf-8').toString('base64');
76+
77+
return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${urlSourceMap}`;
78+
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ async function bundleStylesheet(
2424
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
2525
options: BundleStylesheetOptions,
2626
) {
27+
const loadPaths = options.includePaths ?? [];
28+
// Needed to resolve node packages.
29+
loadPaths.push(path.join(options.workspaceRoot, 'node_modules'));
30+
2731
// Execute esbuild
2832
const result = await bundle({
2933
...entry,
@@ -40,9 +44,7 @@ async function bundleStylesheet(
4044
preserveSymlinks: options.preserveSymlinks,
4145
conditions: ['style'],
4246
mainFields: ['style'],
43-
plugins: [
44-
createSassPlugin({ sourcemap: !!options.sourcemap, includePaths: options.includePaths }),
45-
],
47+
plugins: [createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths })],
4648
});
4749

4850
// Extract the result of the bundling from the output files

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { Architect } from '@angular-devkit/architect';
10+
import { TestProjectHost } from '@angular-devkit/architect/testing';
1011
import { normalize, tags } from '@angular-devkit/core';
1112
import { dirname } from 'path';
1213
import { browserBuild, createArchitect, host } from '../../../testing/test-utils';
@@ -259,7 +260,19 @@ describe('Browser Builder styles', () => {
259260
});
260261
});
261262

263+
/**
264+
* font-awesome mock to avoid having an extra dependency.
265+
*/
266+
function mockFontAwesomePackage(host: TestProjectHost): void {
267+
host.writeMultipleFiles({
268+
'node_modules/font-awesome/scss/font-awesome.scss': `
269+
* { color: red }
270+
`,
271+
});
272+
}
273+
262274
it(`supports font-awesome imports`, async () => {
275+
mockFontAwesomePackage(host);
263276
host.writeMultipleFiles({
264277
'src/styles.scss': `
265278
@import "font-awesome/scss/font-awesome";
@@ -271,6 +284,7 @@ describe('Browser Builder styles', () => {
271284
});
272285

273286
it(`supports font-awesome imports (tilde)`, async () => {
287+
mockFontAwesomePackage(host);
274288
host.writeMultipleFiles({
275289
'src/styles.scss': `
276290
$fa-font-path: "~font-awesome/fonts";

packages/angular_devkit/build_angular/src/utils/environment-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,6 @@ export const allowMinify = debugOptimize.minify;
7575
*/
7676
const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS'];
7777
export const maxWorkers = isPresent(maxWorkersVariable) ? +maxWorkersVariable : 4;
78+
79+
const legacySassVariable = process.env['NG_BUILD_LEGACY_SASS'];
80+
export const useLegacySass = isPresent(legacySassVariable) && isEnabled(legacySassVariable);

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

Lines changed: 96 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99
import * as fs from 'fs';
1010
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
1111
import * as path from 'path';
12+
import type { LegacyOptions, StringOptionsWithoutImporter } from 'sass';
13+
import { pathToFileURL } from 'url';
1214
import { Configuration, RuleSetUseItem } from 'webpack';
1315
import { StyleElement } from '../../builders/browser/schema';
1416
import { SassWorkerImplementation } from '../../sass/sass-service';
1517
import { WebpackConfigOptions } from '../../utils/build-options';
18+
import { useLegacySass } from '../../utils/environment-options';
1619
import {
1720
AnyComponentStyleBudgetChecker,
1821
PostcssCliResources,
@@ -88,7 +91,8 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
8891
// use includePaths from appConfig
8992
const includePaths =
9093
buildOptions.stylePreprocessorOptions?.includePaths?.map((p) => path.resolve(root, p)) ?? [];
91-
94+
// Needed to resolve node packages.
95+
includePaths.push(path.join(root, 'node_modules'));
9296
// Process global styles.
9397
const {
9498
entryPoints,
@@ -107,14 +111,16 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
107111
);
108112
}
109113

110-
const sassImplementation = new SassWorkerImplementation();
111-
extraPlugins.push({
112-
apply(compiler) {
113-
compiler.hooks.shutdown.tap('sass-worker', () => {
114-
sassImplementation.close();
115-
});
116-
},
117-
});
114+
const sassImplementation = useLegacySass ? new SassWorkerImplementation() : require('sass');
115+
if (useLegacySass) {
116+
extraPlugins.push({
117+
apply(compiler) {
118+
compiler.hooks.shutdown.tap('sass-worker', () => {
119+
sassImplementation.close();
120+
});
121+
},
122+
});
123+
}
118124

119125
const assetNameTemplate = assetNameTemplateFactory(hashFormat);
120126

@@ -266,24 +272,13 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
266272
},
267273
{
268274
loader: require.resolve('sass-loader'),
269-
options: {
270-
implementation: sassImplementation,
271-
sourceMap: true,
272-
sassOptions: {
273-
// Prevent use of `fibers` package as it no longer works in newer Node.js versions
274-
fiber: false,
275-
// bootstrap-sass requires a minimum precision of 8
276-
precision: 8,
277-
includePaths,
278-
// Use expanded as otherwise sass will remove comments that are needed for autoprefixer
279-
// Ex: /* autoprefixer grid: autoplace */
280-
// See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70
281-
outputStyle: 'expanded',
282-
// Silences compiler warnings from 3rd party stylesheets
283-
quietDeps: !buildOptions.verbose,
284-
verbose: buildOptions.verbose,
285-
},
286-
},
275+
options: getSassLoaderOptions(
276+
root,
277+
sassImplementation,
278+
includePaths,
279+
false,
280+
!buildOptions.verbose,
281+
),
287282
},
288283
],
289284
},
@@ -298,25 +293,13 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
298293
},
299294
{
300295
loader: require.resolve('sass-loader'),
301-
options: {
302-
implementation: sassImplementation,
303-
sourceMap: true,
304-
sassOptions: {
305-
// Prevent use of `fibers` package as it no longer works in newer Node.js versions
306-
fiber: false,
307-
indentedSyntax: true,
308-
// bootstrap-sass requires a minimum precision of 8
309-
precision: 8,
310-
includePaths,
311-
// Use expanded as otherwise sass will remove comments that are needed for autoprefixer
312-
// Ex: /* autoprefixer grid: autoplace */
313-
// See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70
314-
outputStyle: 'expanded',
315-
// Silences compiler warnings from 3rd party stylesheets
316-
quietDeps: !buildOptions.verbose,
317-
verbose: buildOptions.verbose,
318-
},
319-
},
296+
options: getSassLoaderOptions(
297+
root,
298+
sassImplementation,
299+
includePaths,
300+
true,
301+
!buildOptions.verbose,
302+
),
320303
},
321304
],
322305
},
@@ -411,3 +394,70 @@ function getTailwindConfigPath({ projectRoot, root }: WebpackConfigOptions): str
411394

412395
return undefined;
413396
}
397+
398+
function getSassCompilerOptions(
399+
root: string,
400+
verbose: boolean,
401+
includePaths: string[],
402+
indentedSyntax: boolean,
403+
):
404+
| StringOptionsWithoutImporter<'async'>
405+
| (LegacyOptions<'async'> & { fiber?: boolean; precision?: number }) {
406+
return useLegacySass
407+
? {
408+
// Prevent use of `fibers` package as it no longer works in newer Node.js versions
409+
fiber: false,
410+
// bootstrap-sass requires a minimum precision of 8
411+
precision: 8,
412+
includePaths,
413+
// Use expanded as otherwise sass will remove comments that are needed for autoprefixer
414+
// Ex: /* autoprefixer grid: autoplace */
415+
// See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70
416+
outputStyle: 'expanded',
417+
// Silences compiler warnings from 3rd party stylesheets
418+
quietDeps: !verbose,
419+
verbose,
420+
indentedSyntax,
421+
}
422+
: {
423+
loadPaths: includePaths,
424+
// Use expanded as otherwise sass will remove comments that are needed for autoprefixer
425+
// Ex: /* autoprefixer grid: autoplace */
426+
// See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70
427+
style: 'expanded',
428+
// Silences compiler warnings from 3rd party stylesheets
429+
quietDeps: !verbose,
430+
verbose,
431+
syntax: indentedSyntax ? 'indented' : 'scss',
432+
importers: [
433+
{
434+
// An importer that redirects relative URLs starting with "~" to
435+
// `node_modules`.
436+
// See: https://sass-lang.com/documentation/js-api/interfaces/FileImporter
437+
findFileUrl(url) {
438+
if (url.charAt(0) !== '~') {
439+
return null;
440+
}
441+
442+
// TODO: issue warning.
443+
return new URL(url.substring(1), pathToFileURL(path.join(root, 'node_modules/')));
444+
},
445+
},
446+
],
447+
};
448+
}
449+
450+
function getSassLoaderOptions(
451+
root: string,
452+
implementation: SassWorkerImplementation | typeof import('sass'),
453+
includePaths: string[],
454+
indentedSyntax: boolean,
455+
verbose: boolean,
456+
): Record<string, unknown> {
457+
return {
458+
sourceMap: true,
459+
api: useLegacySass ? 'legacy' : 'modern',
460+
implementation,
461+
sassOptions: getSassCompilerOptions(root, verbose, includePaths, indentedSyntax),
462+
};
463+
}

scripts/validate-licenses.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,6 @@ const ignoredPackages = [
7575
'pako@1.0.11', // MIT but broken license in package.json
7676
'fs-monkey@1.0.1', // Unlicense but missing license field (PR: https://github.com/streamich/fs-monkey/pull/209)
7777
'memfs@3.2.0', // Unlicense but missing license field (PR: https://github.com/streamich/memfs/pull/594)
78-
79-
// * Other
80-
'font-awesome@4.7.0', // (OFL-1.1 AND MIT)
8178
];
8279

8380
// Ignore own packages (all MIT)

0 commit comments

Comments
 (0)