mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-14 17:43:52 +08:00
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.
This commit is contained in:
parent
669345998b
commit
248860ad67
@ -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<</ 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"
|
||||
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"
|
||||
- fail_fast
|
||||
|
||||
test-browsers:
|
||||
|
@ -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
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -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<LegacyResult>((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,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -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 }),
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
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({
|
||||
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';
|
||||
@ -37,42 +38,49 @@ export default function () {
|
||||
@import 'variables';
|
||||
h6 { color: @primary-color; }
|
||||
`,
|
||||
}),
|
||||
)
|
||||
.then(() =>
|
||||
replaceInFile(
|
||||
});
|
||||
|
||||
await replaceInFile(
|
||||
'src/app/app.component.ts',
|
||||
`'./app.component.css\'`,
|
||||
`'./app.component.scss', './app.component.styl', './app.component.less'`,
|
||||
),
|
||||
)
|
||||
.then(() =>
|
||||
updateJsonFile('angular.json', (workspaceJson) => {
|
||||
`'./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' },
|
||||
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'],
|
||||
};
|
||||
}),
|
||||
)
|
||||
// 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 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;.*}/);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user