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
|
name: Execute CLI E2E Tests Subset with esbuild builder
|
||||||
command: |
|
command: |
|
||||||
mkdir /mnt/ramdisk/e2e-esbuild
|
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
|
- fail_fast
|
||||||
|
|
||||||
test-browsers:
|
test-browsers:
|
||||||
|
@ -16,7 +16,7 @@ import ts from 'typescript';
|
|||||||
import angularApplicationPreset from '../../babel/presets/application';
|
import angularApplicationPreset from '../../babel/presets/application';
|
||||||
import { requiresLinking } from '../../babel/webpack-loader';
|
import { requiresLinking } from '../../babel/webpack-loader';
|
||||||
import { loadEsmModule } from '../../utils/load-esm';
|
import { loadEsmModule } from '../../utils/load-esm';
|
||||||
import { BundleStylesheetOptions, bundleStylesheetText } from './stylesheets';
|
import { BundleStylesheetOptions, bundleStylesheetFile, bundleStylesheetText } from './stylesheets';
|
||||||
|
|
||||||
interface EmitFileResult {
|
interface EmitFileResult {
|
||||||
content?: string;
|
content?: string;
|
||||||
@ -191,17 +191,27 @@ export function createCompilerPlugin(
|
|||||||
// Create TypeScript compiler host
|
// Create TypeScript compiler host
|
||||||
const host = ts.createIncrementalCompilerHost(compilerOptions);
|
const host = ts.createIncrementalCompilerHost(compilerOptions);
|
||||||
|
|
||||||
// Temporarily add a readResource hook to allow for a transformResource hook.
|
// Temporarily process external resources via readResource.
|
||||||
// Once the AOT compiler allows only a transformResource hook this can be removed.
|
// The AOT compiler currently requires this hook to allow for a transformResource hook.
|
||||||
(host as CompilerHost).readResource = function (fileName) {
|
// Once the AOT compiler allows only a transformResource hook, this can be reevaluated.
|
||||||
// Provide same no file found behavior as @ngtools/webpack
|
(host as CompilerHost).readResource = async function (fileName) {
|
||||||
return this.readFile(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
|
// Add an AOT compiler resource transform hook
|
||||||
(host as CompilerHost).transformResource = async function (data, context) {
|
(host as CompilerHost).transformResource = async function (data, context) {
|
||||||
// Only style resources are transformed currently
|
// Only inline style resources are transformed separately currently
|
||||||
if (context.type !== 'style') {
|
if (context.resourceFile || context.type !== 'style') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,8 +160,9 @@ export async function buildEsbuildBrowser(
|
|||||||
{ virtualName: `angular:style/global;${name}`, resolvePath: workspaceRoot },
|
{ virtualName: `angular:style/global;${name}`, resolvePath: workspaceRoot },
|
||||||
{
|
{
|
||||||
optimization: !!optimizationOptions.styles.minify,
|
optimization: !!optimizationOptions.styles.minify,
|
||||||
sourcemap: !!sourcemapOptions.styles,
|
sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true),
|
||||||
outputNames: noInjectNames.includes(name) ? { media: outputNames.media } : outputNames,
|
outputNames: noInjectNames.includes(name) ? { media: outputNames.media } : outputNames,
|
||||||
|
includePaths: options.stylePreprocessorOptions?.includePaths,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -334,6 +335,7 @@ async function bundleCode(
|
|||||||
// of sourcemap processing.
|
// of sourcemap processing.
|
||||||
!!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'),
|
!!sourcemapOptions.styles && (sourcemapOptions.hidden ? false : 'inline'),
|
||||||
outputNames,
|
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 type { BuildOptions, OutputFile } from 'esbuild';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { DEFAULT_OUTDIR, bundle } from './esbuild';
|
import { DEFAULT_OUTDIR, bundle } from './esbuild';
|
||||||
|
import { createSassPlugin } from './sass-plugin';
|
||||||
|
|
||||||
export interface BundleStylesheetOptions {
|
export interface BundleStylesheetOptions {
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
@ -16,6 +17,7 @@ export interface BundleStylesheetOptions {
|
|||||||
preserveSymlinks?: boolean;
|
preserveSymlinks?: boolean;
|
||||||
sourcemap: boolean | 'external' | 'inline';
|
sourcemap: boolean | 'external' | 'inline';
|
||||||
outputNames?: { bundles?: string; media?: string };
|
outputNames?: { bundles?: string; media?: string };
|
||||||
|
includePaths?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bundleStylesheet(
|
async function bundleStylesheet(
|
||||||
@ -39,7 +41,7 @@ async function bundleStylesheet(
|
|||||||
conditions: ['style'],
|
conditions: ['style'],
|
||||||
mainFields: ['style'],
|
mainFields: ['style'],
|
||||||
plugins: [
|
plugins: [
|
||||||
// TODO: preprocessor plugins
|
createSassPlugin({ sourcemap: !!options.sourcemap, includePaths: options.includePaths }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,78 +1,86 @@
|
|||||||
|
import { getGlobalVariable } from '../../../utils/env';
|
||||||
import { writeMultipleFiles, expectFileToMatch, replaceInFile, createDir } from '../../../utils/fs';
|
import { writeMultipleFiles, expectFileToMatch, replaceInFile, createDir } from '../../../utils/fs';
|
||||||
import { ng } from '../../../utils/process';
|
import { ng } from '../../../utils/process';
|
||||||
import { updateJsonFile } from '../../../utils/project';
|
import { updateJsonFile } from '../../../utils/project';
|
||||||
|
|
||||||
export default function () {
|
export default async function () {
|
||||||
return (
|
// esbuild currently only supports Sass
|
||||||
Promise.resolve()
|
const esbuild = getGlobalVariable('argv')['esbuild'];
|
||||||
.then(() => createDir('src/style-paths'))
|
|
||||||
.then(() =>
|
await createDir('src/style-paths');
|
||||||
writeMultipleFiles({
|
await writeMultipleFiles({
|
||||||
'src/style-paths/_variables.scss': '$primary-color: red;',
|
'src/style-paths/_variables.scss': '$primary-color: red;',
|
||||||
'src/styles.scss': `
|
'src/styles.scss': `
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
h1 { color: $primary-color; }
|
h1 { color: $primary-color; }
|
||||||
`,
|
`,
|
||||||
'src/app/app.component.scss': `
|
'src/app/app.component.scss': `
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
h2 { background-color: $primary-color; }
|
h2 { background-color: $primary-color; }
|
||||||
`,
|
`,
|
||||||
'src/style-paths/variables.styl': '$primary-color = green',
|
'src/style-paths/variables.styl': '$primary-color = green',
|
||||||
'src/styles.styl': `
|
'src/styles.styl': `
|
||||||
@import 'variables'
|
@import 'variables'
|
||||||
h3
|
h3
|
||||||
color: $primary-color
|
color: $primary-color
|
||||||
`,
|
`,
|
||||||
'src/app/app.component.styl': `
|
'src/app/app.component.styl': `
|
||||||
@import 'variables'
|
@import 'variables'
|
||||||
h4
|
h4
|
||||||
background-color: $primary-color
|
background-color: $primary-color
|
||||||
`,
|
`,
|
||||||
'src/style-paths/variables.less': '@primary-color: #ADDADD;',
|
'src/style-paths/variables.less': '@primary-color: #ADDADD;',
|
||||||
'src/styles.less': `
|
'src/styles.less': `
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
h5 { color: @primary-color; }
|
h5 { color: @primary-color; }
|
||||||
`,
|
`,
|
||||||
'src/app/app.component.less': `
|
'src/app/app.component.less': `
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
h6 { color: @primary-color; }
|
h6 { color: @primary-color; }
|
||||||
`,
|
`,
|
||||||
}),
|
});
|
||||||
)
|
|
||||||
.then(() =>
|
await replaceInFile(
|
||||||
replaceInFile(
|
'src/app/app.component.ts',
|
||||||
'src/app/app.component.ts',
|
`'./app.component.css\'`,
|
||||||
`'./app.component.css\'`,
|
`'./app.component.scss'` + (esbuild ? '' : `, './app.component.styl', './app.component.less'`),
|
||||||
`'./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 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;.*}/);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user