Skip to content

Commit 3fb569b

Browse files
alan-agius4dgp1130
authored andcommitted
feat(@angular-devkit/build-angular): switch to Sass modern API in esbuild builder
With this change we replace Sass legacy with the modern API in the experimental esbuilder. The goal is that in the next major version this change is propagated to the Webpack builder. Based on the benchmarks that we did Sass modern API is faster compared to the legacy version.
1 parent 4d2f2bd commit 3fb569b

File tree

2 files changed

+64
-42
lines changed

2 files changed

+64
-42
lines changed

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

+59-39
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

+5-3
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

0 commit comments

Comments
 (0)