From 3fb569b5c82f22afca4dc59313356f198755827e Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 5 Aug 2022 12:44:33 +0000 Subject: [PATCH] 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. --- .../builders/browser-esbuild/sass-plugin.ts | 98 +++++++++++-------- .../builders/browser-esbuild/stylesheets.ts | 8 +- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/sass-plugin.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/sass-plugin.ts index dfafa49049e4..c0c0a4304f83 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/sass-plugin.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/sass-plugin.ts @@ -6,54 +6,74 @@ * found in the LICENSE file at https://angular.io/license */ -import type { Plugin, PluginBuild } from 'esbuild'; -import type { LegacyResult } from 'sass'; -import { SassWorkerImplementation } from '../../sass/sass-service'; +import type { PartialMessage, Plugin, PluginBuild } from 'esbuild'; +import type { CompileResult } from 'sass'; +import { fileURLToPath } from 'url'; -export function createSassPlugin(options: { sourcemap: boolean; includePaths?: string[] }): Plugin { +export function createSassPlugin(options: { sourcemap: boolean; loadPaths?: string[] }): Plugin { return { name: 'angular-sass', setup(build: PluginBuild): void { - let sass: SassWorkerImplementation; + let sass: typeof import('sass'); - build.onStart(() => { - sass = new SassWorkerImplementation(); + build.onStart(async () => { + // Lazily load Sass + sass = await import('sass'); }); - build.onEnd(() => { - sass?.close(); - }); - - build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => { - const result = await new Promise((resolve, reject) => { - sass.render( - { - file: args.path, - includePaths: options.includePaths, - indentedSyntax: args.path.endsWith('.sass'), - outputStyle: 'expanded', - sourceMap: options.sourcemap, - sourceMapContents: options.sourcemap, - sourceMapEmbed: options.sourcemap, - quietDeps: true, - }, - (error, result) => { - if (error) { - reject(error); - } - if (result) { - resolve(result); - } + build.onLoad({ filter: /\.s[ac]ss$/ }, (args) => { + try { + const warnings: PartialMessage[] = []; + // Use sync version as async version is slower. + const { css, sourceMap, loadedUrls } = sass.compile(args.path, { + style: 'expanded', + loadPaths: options.loadPaths, + sourceMap: options.sourcemap, + sourceMapIncludeSources: options.sourcemap, + quietDeps: true, + logger: { + warn: (text, _options) => { + warnings.push({ + text, + }); + }, }, - ); - }); - - return { - contents: result.css, - loader: 'css', - watchFiles: result.stats.includedFiles, - }; + }); + + return { + loader: 'css', + contents: css + sourceMapToUrlComment(sourceMap), + watchFiles: loadedUrls.map((url) => fileURLToPath(url)), + warnings, + }; + } catch (error) { + if (error instanceof sass.Exception) { + const file = error.span.url ? fileURLToPath(error.span.url) : undefined; + + return { + loader: 'css', + errors: [ + { + text: error.toString(), + }, + ], + watchFiles: file ? [file] : undefined, + }; + } + + throw error; + } }); }, }; } + +function sourceMapToUrlComment(sourceMap: CompileResult['sourceMap']): string { + if (!sourceMap) { + return ''; + } + + const urlSourceMap = Buffer.from(JSON.stringify(sourceMap), 'utf-8').toString('base64'); + + return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${urlSourceMap}`; +} diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts index 81ef5c5286f7..901f0af03cfd 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets.ts @@ -24,6 +24,10 @@ async function bundleStylesheet( entry: Required | Pick>, options: BundleStylesheetOptions, ) { + const loadPaths = options.includePaths ?? []; + // Needed to resolve node packages. + loadPaths.push(path.join(options.workspaceRoot, 'node_modules')); + // Execute esbuild const result = await bundle({ ...entry, @@ -40,9 +44,7 @@ async function bundleStylesheet( preserveSymlinks: options.preserveSymlinks, conditions: ['style', 'sass'], mainFields: ['style', 'sass'], - plugins: [ - createSassPlugin({ sourcemap: !!options.sourcemap, includePaths: options.includePaths }), - ], + plugins: [createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths })], }); // Extract the result of the bundling from the output files