diff --git a/.circleci/config.yml b/.circleci/config.yml index 2dbb2e260e..0cbf22a6d3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -221,7 +221,7 @@ jobs: name: Execute CLI E2E Tests Subset with esbuild builder command: | mkdir /mnt/ramdisk/e2e-esbuild - node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.snapshots >>--ng-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" + node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.snapshots >>--ng-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" - fail_fast test-browsers: diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts index 85896b7fb5..85220ef735 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts @@ -16,7 +16,7 @@ import ts from 'typescript'; import angularApplicationPreset from '../../babel/presets/application'; import { requiresLinking } from '../../babel/webpack-loader'; import { loadEsmModule } from '../../utils/load-esm'; -import { BundleStylesheetOptions, bundleStylesheetText } from './stylesheets'; +import { BundleStylesheetOptions, bundleStylesheetFile, bundleStylesheetText } from './stylesheets'; interface EmitFileResult { content?: string; @@ -191,17 +191,27 @@ export function createCompilerPlugin( // Create TypeScript compiler host const host = ts.createIncrementalCompilerHost(compilerOptions); - // Temporarily add a readResource hook to allow for a transformResource hook. - // Once the AOT compiler allows only a transformResource hook this can be removed. - (host as CompilerHost).readResource = function (fileName) { - // Provide same no file found behavior as @ngtools/webpack - return this.readFile(fileName) ?? ''; + // Temporarily process external resources via readResource. + // The AOT compiler currently requires this hook to allow for a transformResource hook. + // Once the AOT compiler allows only a transformResource hook, this can be reevaluated. + (host as CompilerHost).readResource = async function (fileName) { + // Template resources (.html) files are not bundled or transformed + if (fileName.endsWith('.html')) { + return this.readFile(fileName) ?? ''; + } + + const { contents, errors, warnings } = await bundleStylesheetFile(fileName, styleOptions); + + (result.errors ??= []).push(...errors); + (result.warnings ??= []).push(...warnings); + + return contents; }; // Add an AOT compiler resource transform hook (host as CompilerHost).transformResource = async function (data, context) { - // Only style resources are transformed currently - if (context.type !== 'style') { + // Only inline style resources are transformed separately currently + if (context.resourceFile || context.type !== 'style') { return null; } diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts index e19939cfea..347e93676c 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts @@ -160,8 +160,9 @@ export async function buildEsbuildBrowser( { virtualName: `angular:style/global;${name}`, resolvePath: workspaceRoot }, { optimization: !!optimizationOptions.styles.minify, - sourcemap: !!sourcemapOptions.styles, + sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true), outputNames: noInjectNames.includes(name) ? { media: outputNames.media } : outputNames, + includePaths: options.stylePreprocessorOptions?.includePaths, }, ); @@ -334,6 +335,7 @@ async function bundleCode( // of sourcemap processing. !!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'), outputNames, + includePaths: options.stylePreprocessorOptions?.includePaths, }, ), ], 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 new file mode 100644 index 0000000000..dfafa49049 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/sass-plugin.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * 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'; + +export function createSassPlugin(options: { sourcemap: boolean; includePaths?: string[] }): Plugin { + return { + name: 'angular-sass', + setup(build: PluginBuild): void { + let sass: SassWorkerImplementation; + + build.onStart(() => { + sass = new SassWorkerImplementation(); + }); + + 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); + } + }, + ); + }); + + return { + contents: result.css, + loader: 'css', + watchFiles: result.stats.includedFiles, + }; + }); + }, + }; +} 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 ecf2082722..1878202ccd 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 @@ -9,6 +9,7 @@ import type { BuildOptions, OutputFile } from 'esbuild'; import * as path from 'path'; import { DEFAULT_OUTDIR, bundle } from './esbuild'; +import { createSassPlugin } from './sass-plugin'; export interface BundleStylesheetOptions { workspaceRoot?: string; @@ -16,6 +17,7 @@ export interface BundleStylesheetOptions { preserveSymlinks?: boolean; sourcemap: boolean | 'external' | 'inline'; outputNames?: { bundles?: string; media?: string }; + includePaths?: string[]; } async function bundleStylesheet( @@ -39,7 +41,7 @@ async function bundleStylesheet( conditions: ['style'], mainFields: ['style'], plugins: [ - // TODO: preprocessor plugins + createSassPlugin({ sourcemap: !!options.sourcemap, includePaths: options.includePaths }), ], }); diff --git a/tests/legacy-cli/e2e/tests/build/styles/include-paths.ts b/tests/legacy-cli/e2e/tests/build/styles/include-paths.ts index 041ef7a2c9..e1c77e33be 100644 --- a/tests/legacy-cli/e2e/tests/build/styles/include-paths.ts +++ b/tests/legacy-cli/e2e/tests/build/styles/include-paths.ts @@ -1,78 +1,86 @@ +import { getGlobalVariable } from '../../../utils/env'; import { writeMultipleFiles, expectFileToMatch, replaceInFile, createDir } from '../../../utils/fs'; import { ng } from '../../../utils/process'; import { updateJsonFile } from '../../../utils/project'; -export default function () { - return ( - Promise.resolve() - .then(() => createDir('src/style-paths')) - .then(() => - writeMultipleFiles({ - 'src/style-paths/_variables.scss': '$primary-color: red;', - 'src/styles.scss': ` +export default async function () { + // esbuild currently only supports Sass + const esbuild = getGlobalVariable('argv')['esbuild']; + + await createDir('src/style-paths'); + await writeMultipleFiles({ + 'src/style-paths/_variables.scss': '$primary-color: red;', + 'src/styles.scss': ` @import 'variables'; h1 { color: $primary-color; } - `, - 'src/app/app.component.scss': ` + `, + 'src/app/app.component.scss': ` @import 'variables'; h2 { background-color: $primary-color; } - `, - 'src/style-paths/variables.styl': '$primary-color = green', - 'src/styles.styl': ` + `, + 'src/style-paths/variables.styl': '$primary-color = green', + 'src/styles.styl': ` @import 'variables' h3 color: $primary-color - `, - 'src/app/app.component.styl': ` + `, + 'src/app/app.component.styl': ` @import 'variables' h4 background-color: $primary-color - `, - 'src/style-paths/variables.less': '@primary-color: #ADDADD;', - 'src/styles.less': ` + `, + 'src/style-paths/variables.less': '@primary-color: #ADDADD;', + 'src/styles.less': ` @import 'variables'; h5 { color: @primary-color; } - `, - 'src/app/app.component.less': ` + `, + 'src/app/app.component.less': ` @import 'variables'; h6 { color: @primary-color; } - `, - }), - ) - .then(() => - replaceInFile( - 'src/app/app.component.ts', - `'./app.component.css\'`, - `'./app.component.scss', './app.component.styl', './app.component.less'`, - ), - ) - .then(() => - updateJsonFile('angular.json', (workspaceJson) => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/styles.scss' }, - { input: 'src/styles.styl' }, - { input: 'src/styles.less' }, - ]; - appArchitect.build.options.stylePreprocessorOptions = { - includePaths: ['src/style-paths'], - }; - }), - ) - // files were created successfully - .then(() => ng('build', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/)) - .then(() => ng('build', '--aot', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/)) + `, + }); + + await replaceInFile( + 'src/app/app.component.ts', + `'./app.component.css\'`, + `'./app.component.scss'` + (esbuild ? '' : `, './app.component.styl', './app.component.less'`), ); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [{ input: 'src/styles.scss' }]; + if (!esbuild) { + appArchitect.build.options.styles.push( + { input: 'src/styles.styl' }, + { input: 'src/styles.less' }, + ); + } + appArchitect.build.options.stylePreprocessorOptions = { + includePaths: ['src/style-paths'], + }; + }); + + await ng('build', '--configuration=development'); + + expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/); + expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/); + if (!esbuild) { + // These checks are for the less and stylus files + expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/); + expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/); + expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/); + expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/); + } + + // esbuild currently only supports AOT and not JIT mode + if (!esbuild) { + ng('build', '--no-aot', '--configuration=development'); + + expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/); + expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/); + expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/); + expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/); + expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/); + expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/); + } }