Skip to content

Commit 19a69dc

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 ceb97be commit 19a69dc

File tree

13 files changed

+668
-185
lines changed

13 files changed

+668
-185
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@
148148
"eslint-plugin-header": "3.1.1",
149149
"eslint-plugin-import": "2.26.0",
150150
"express": "4.18.1",
151-
"font-awesome": "^4.7.0",
152151
"glob": "8.0.3",
153152
"http-proxy": "^1.18.1",
154153
"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: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,74 @@
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();
19+
build.onStart(async () => {
20+
// Lazily load Sass
21+
sass = await import('sass');
2122
});
2223

23-
build.onEnd(() => {
24-
sass?.close();
25-
});
26-
27-
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-
}
24+
build.onLoad({ filter: /\.s[ac]ss$/ }, (args) => {
25+
try {
26+
const warnings: PartialMessage[] = [];
27+
// Use sync version as async version is slower.
28+
const { css, sourceMap, loadedUrls } = sass.compile(args.path, {
29+
style: 'expanded',
30+
loadPaths: options.loadPaths,
31+
sourceMap: options.sourcemap,
32+
sourceMapIncludeSources: options.sourcemap,
33+
quietDeps: true,
34+
logger: {
35+
warn: (text, _options) => {
36+
warnings.push({
37+
text,
38+
});
39+
},
4740
},
48-
);
49-
});
50-
51-
return {
52-
contents: result.css,
53-
loader: 'css',
54-
watchFiles: result.stats.includedFiles,
55-
};
41+
});
42+
43+
return {
44+
loader: 'css',
45+
contents: css + sourceMapToUrlComment(sourceMap),
46+
watchFiles: loadedUrls.map((url) => fileURLToPath(url)),
47+
warnings,
48+
};
49+
} catch (error) {
50+
if (error instanceof sass.Exception) {
51+
const file = error.span.url ? fileURLToPath(error.span.url) : undefined;
52+
53+
return {
54+
loader: 'css',
55+
errors: [
56+
{
57+
text: error.toString(),
58+
},
59+
],
60+
watchFiles: file ? [file] : undefined,
61+
};
62+
}
63+
64+
throw error;
65+
}
5666
});
5767
},
5868
};
5969
}
70+
71+
function sourceMapToUrlComment(sourceMap: CompileResult['sourceMap']): string {
72+
if (!sourceMap) {
73+
return '';
74+
}
75+
76+
const urlSourceMap = Buffer.from(JSON.stringify(sourceMap), 'utf-8').toString('base64');
77+
78+
return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${urlSourceMap}`;
79+
}

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', 'sass'],
4246
mainFields: ['style', 'sass'],
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";

0 commit comments

Comments
 (0)