Skip to content

Commit 248860a

Browse files
clydindgp1130
authored andcommitted
feat(@angular-devkit/build-angular): add Sass file support to experimental esbuild-based builder
This change adds support for using Sass stylesheets within an application built with the experimental esbuild-based browser application builder. Global stylesheets (`styles` build option) and component stylesheets (`@Component({ styleUrls: [...], ...})`) with Sass can now be used. The `stylePreprocessorOptions.includePaths` option is also available for Sass stylesheets. Both the default format (`.scss`) and the indented format (`.sass`) are supported. Inline component stylesheet support is not yet available with the esbuild-based builder.
1 parent 6693459 commit 248860a

File tree

6 files changed

+150
-69
lines changed

6 files changed

+150
-69
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ jobs:
221221
name: Execute CLI E2E Tests Subset with esbuild builder
222222
command: |
223223
mkdir /mnt/ramdisk/e2e-esbuild
224-
node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.snapshots >>--ng-snapshots<</ parameters.snapshots >> --esbuild --tmpdir=/mnt/ramdisk/e2e-esbuild --glob="{tests/basic/**,tests/build/prod-build.ts,tests/commands/add/add-pwa.ts}" --ignore="tests/basic/{environment,rebuild,serve,scripts-array}.ts"
224+
node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.snapshots >>--ng-snapshots<</ parameters.snapshots >> --esbuild --tmpdir=/mnt/ramdisk/e2e-esbuild --glob="{tests/basic/**,tests/build/prod-build.ts,tests/build/styles/scss.ts,tests/build/styles/include-paths.ts,tests/commands/add/add-pwa.ts}" --ignore="tests/basic/{environment,rebuild,serve,scripts-array}.ts"
225225
- fail_fast
226226

227227
test-browsers:

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

+18-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import ts from 'typescript';
1616
import angularApplicationPreset from '../../babel/presets/application';
1717
import { requiresLinking } from '../../babel/webpack-loader';
1818
import { loadEsmModule } from '../../utils/load-esm';
19-
import { BundleStylesheetOptions, bundleStylesheetText } from './stylesheets';
19+
import { BundleStylesheetOptions, bundleStylesheetFile, bundleStylesheetText } from './stylesheets';
2020

2121
interface EmitFileResult {
2222
content?: string;
@@ -191,17 +191,27 @@ export function createCompilerPlugin(
191191
// Create TypeScript compiler host
192192
const host = ts.createIncrementalCompilerHost(compilerOptions);
193193

194-
// Temporarily add a readResource hook to allow for a transformResource hook.
195-
// Once the AOT compiler allows only a transformResource hook this can be removed.
196-
(host as CompilerHost).readResource = function (fileName) {
197-
// Provide same no file found behavior as @ngtools/webpack
198-
return this.readFile(fileName) ?? '';
194+
// Temporarily process external resources via readResource.
195+
// The AOT compiler currently requires this hook to allow for a transformResource hook.
196+
// Once the AOT compiler allows only a transformResource hook, this can be reevaluated.
197+
(host as CompilerHost).readResource = async function (fileName) {
198+
// Template resources (.html) files are not bundled or transformed
199+
if (fileName.endsWith('.html')) {
200+
return this.readFile(fileName) ?? '';
201+
}
202+
203+
const { contents, errors, warnings } = await bundleStylesheetFile(fileName, styleOptions);
204+
205+
(result.errors ??= []).push(...errors);
206+
(result.warnings ??= []).push(...warnings);
207+
208+
return contents;
199209
};
200210

201211
// Add an AOT compiler resource transform hook
202212
(host as CompilerHost).transformResource = async function (data, context) {
203-
// Only style resources are transformed currently
204-
if (context.type !== 'style') {
213+
// Only inline style resources are transformed separately currently
214+
if (context.resourceFile || context.type !== 'style') {
205215
return null;
206216
}
207217

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,9 @@ export async function buildEsbuildBrowser(
160160
{ virtualName: `angular:style/global;${name}`, resolvePath: workspaceRoot },
161161
{
162162
optimization: !!optimizationOptions.styles.minify,
163-
sourcemap: !!sourcemapOptions.styles,
163+
sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true),
164164
outputNames: noInjectNames.includes(name) ? { media: outputNames.media } : outputNames,
165+
includePaths: options.stylePreprocessorOptions?.includePaths,
165166
},
166167
);
167168

@@ -334,6 +335,7 @@ async function bundleCode(
334335
// of sourcemap processing.
335336
!!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'),
336337
outputNames,
338+
includePaths: options.stylePreprocessorOptions?.includePaths,
337339
},
338340
),
339341
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import type { Plugin, PluginBuild } from 'esbuild';
10+
import type { LegacyResult } from 'sass';
11+
import { SassWorkerImplementation } from '../../sass/sass-service';
12+
13+
export function createSassPlugin(options: { sourcemap: boolean; includePaths?: string[] }): Plugin {
14+
return {
15+
name: 'angular-sass',
16+
setup(build: PluginBuild): void {
17+
let sass: SassWorkerImplementation;
18+
19+
build.onStart(() => {
20+
sass = new SassWorkerImplementation();
21+
});
22+
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+
}
47+
},
48+
);
49+
});
50+
51+
return {
52+
contents: result.css,
53+
loader: 'css',
54+
watchFiles: result.stats.includedFiles,
55+
};
56+
});
57+
},
58+
};
59+
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import type { BuildOptions, OutputFile } from 'esbuild';
1010
import * as path from 'path';
1111
import { DEFAULT_OUTDIR, bundle } from './esbuild';
12+
import { createSassPlugin } from './sass-plugin';
1213

1314
export interface BundleStylesheetOptions {
1415
workspaceRoot?: string;
1516
optimization: boolean;
1617
preserveSymlinks?: boolean;
1718
sourcemap: boolean | 'external' | 'inline';
1819
outputNames?: { bundles?: string; media?: string };
20+
includePaths?: string[];
1921
}
2022

2123
async function bundleStylesheet(
@@ -39,7 +41,7 @@ async function bundleStylesheet(
3941
conditions: ['style'],
4042
mainFields: ['style'],
4143
plugins: [
42-
// TODO: preprocessor plugins
44+
createSassPlugin({ sourcemap: !!options.sourcemap, includePaths: options.includePaths }),
4345
],
4446
});
4547

Original file line numberDiff line numberDiff line change
@@ -1,78 +1,86 @@
1+
import { getGlobalVariable } from '../../../utils/env';
12
import { writeMultipleFiles, expectFileToMatch, replaceInFile, createDir } from '../../../utils/fs';
23
import { ng } from '../../../utils/process';
34
import { updateJsonFile } from '../../../utils/project';
45

5-
export default function () {
6-
return (
7-
Promise.resolve()
8-
.then(() => createDir('src/style-paths'))
9-
.then(() =>
10-
writeMultipleFiles({
11-
'src/style-paths/_variables.scss': '$primary-color: red;',
12-
'src/styles.scss': `
6+
export default async function () {
7+
// esbuild currently only supports Sass
8+
const esbuild = getGlobalVariable('argv')['esbuild'];
9+
10+
await createDir('src/style-paths');
11+
await writeMultipleFiles({
12+
'src/style-paths/_variables.scss': '$primary-color: red;',
13+
'src/styles.scss': `
1314
@import 'variables';
1415
h1 { color: $primary-color; }
15-
`,
16-
'src/app/app.component.scss': `
16+
`,
17+
'src/app/app.component.scss': `
1718
@import 'variables';
1819
h2 { background-color: $primary-color; }
19-
`,
20-
'src/style-paths/variables.styl': '$primary-color = green',
21-
'src/styles.styl': `
20+
`,
21+
'src/style-paths/variables.styl': '$primary-color = green',
22+
'src/styles.styl': `
2223
@import 'variables'
2324
h3
2425
color: $primary-color
25-
`,
26-
'src/app/app.component.styl': `
26+
`,
27+
'src/app/app.component.styl': `
2728
@import 'variables'
2829
h4
2930
background-color: $primary-color
30-
`,
31-
'src/style-paths/variables.less': '@primary-color: #ADDADD;',
32-
'src/styles.less': `
31+
`,
32+
'src/style-paths/variables.less': '@primary-color: #ADDADD;',
33+
'src/styles.less': `
3334
@import 'variables';
3435
h5 { color: @primary-color; }
35-
`,
36-
'src/app/app.component.less': `
36+
`,
37+
'src/app/app.component.less': `
3738
@import 'variables';
3839
h6 { color: @primary-color; }
39-
`,
40-
}),
41-
)
42-
.then(() =>
43-
replaceInFile(
44-
'src/app/app.component.ts',
45-
`'./app.component.css\'`,
46-
`'./app.component.scss', './app.component.styl', './app.component.less'`,
47-
),
48-
)
49-
.then(() =>
50-
updateJsonFile('angular.json', (workspaceJson) => {
51-
const appArchitect = workspaceJson.projects['test-project'].architect;
52-
appArchitect.build.options.styles = [
53-
{ input: 'src/styles.scss' },
54-
{ input: 'src/styles.styl' },
55-
{ input: 'src/styles.less' },
56-
];
57-
appArchitect.build.options.stylePreprocessorOptions = {
58-
includePaths: ['src/style-paths'],
59-
};
60-
}),
61-
)
62-
// files were created successfully
63-
.then(() => ng('build', '--configuration=development'))
64-
.then(() => expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/))
65-
.then(() => expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/))
66-
.then(() => expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/))
67-
.then(() => expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/))
68-
.then(() => expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/))
69-
.then(() => expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/))
70-
.then(() => ng('build', '--aot', '--configuration=development'))
71-
.then(() => expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/))
72-
.then(() => expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/))
73-
.then(() => expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/))
74-
.then(() => expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/))
75-
.then(() => expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/))
76-
.then(() => expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/))
40+
`,
41+
});
42+
43+
await replaceInFile(
44+
'src/app/app.component.ts',
45+
`'./app.component.css\'`,
46+
`'./app.component.scss'` + (esbuild ? '' : `, './app.component.styl', './app.component.less'`),
7747
);
48+
49+
await updateJsonFile('angular.json', (workspaceJson) => {
50+
const appArchitect = workspaceJson.projects['test-project'].architect;
51+
appArchitect.build.options.styles = [{ input: 'src/styles.scss' }];
52+
if (!esbuild) {
53+
appArchitect.build.options.styles.push(
54+
{ input: 'src/styles.styl' },
55+
{ input: 'src/styles.less' },
56+
);
57+
}
58+
appArchitect.build.options.stylePreprocessorOptions = {
59+
includePaths: ['src/style-paths'],
60+
};
61+
});
62+
63+
await ng('build', '--configuration=development');
64+
65+
expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/);
66+
expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/);
67+
if (!esbuild) {
68+
// These checks are for the less and stylus files
69+
expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/);
70+
expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/);
71+
expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/);
72+
expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/);
73+
}
74+
75+
// esbuild currently only supports AOT and not JIT mode
76+
if (!esbuild) {
77+
ng('build', '--no-aot', '--configuration=development');
78+
79+
expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/);
80+
expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/);
81+
expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/);
82+
expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/);
83+
expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/);
84+
expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/);
85+
}
7886
}

0 commit comments

Comments
 (0)