Skip to content

Commit b6df9c1

Browse files
committed
fix(@angular-devkit/build-angular): handle conditional exports in scripts and styles option
With this change scripts and styles options better support Yarn PNP resolution. Closes #23568
1 parent f64c3bc commit b6df9c1

File tree

9 files changed

+163
-103
lines changed

9 files changed

+163
-103
lines changed

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

+2-7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { generateEntryPoints } from '../../utils/package-chunk-sort';
2121
import { augmentAppWithServiceWorker } from '../../utils/service-worker';
2222
import { getSupportedBrowsers } from '../../utils/supported-browsers';
2323
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
24-
import { resolveGlobalStyles } from '../../webpack/configs';
24+
import { normalizeGlobalStyles } from '../../webpack/utils/helpers';
2525
import { createCompilerPlugin } from './compiler-plugin';
2626
import { bundle, logMessages } from './esbuild';
2727
import { logExperimentalWarnings } from './experimental-warnings';
@@ -347,13 +347,8 @@ async function bundleGlobalStylesheets(
347347
const warnings: Message[] = [];
348348

349349
// resolveGlobalStyles is temporarily reused from the Webpack builder code
350-
const { entryPoints: stylesheetEntrypoints, noInjectNames } = resolveGlobalStyles(
350+
const { entryPoints: stylesheetEntrypoints, noInjectNames } = normalizeGlobalStyles(
351351
options.styles || [],
352-
workspaceRoot,
353-
// preserveSymlinks is always true here to allow the bundler to handle the option
354-
true,
355-
// skipResolution to leverage the bundler's more comprehensive resolution
356-
true,
357352
);
358353

359354
for (const [name, files] of Object.entries(stylesheetEntrypoints)) {

packages/angular_devkit/build_angular/src/builders/browser/specs/scripts-array_spec.ts

-8
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,4 @@ describe('Browser Builder scripts array', () => {
145145
expect(joinedLogs).toMatch(/renamed-lazy-script.+\d+ bytes/);
146146
expect(joinedLogs).not.toContain('Lazy Chunks');
147147
});
148-
149-
it(`should error when a script doesn't exist`, async () => {
150-
await expectAsync(
151-
browserBuild(architect, host, target, {
152-
scripts: ['./invalid.js'],
153-
}),
154-
).toBeRejectedWithError(`Script file ./invalid.js does not exist.`);
155-
});
156148
});

packages/angular_devkit/build_angular/src/builders/browser/tests/options/scripts_spec.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,19 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
9393
);
9494
});
9595

96-
it('throws an exception if script does not exist', async () => {
96+
it('fails and shows an error if script does not exist', async () => {
9797
harness.useTarget('build', {
9898
...BASE_OPTIONS,
9999
scripts: ['src/test-script-a.js'],
100100
});
101101

102-
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
102+
const { result, logs } = await harness.executeOnce();
103103

104-
expect(result).toBeUndefined();
105-
expect(error).toEqual(
104+
expect(result?.success).toBeFalse();
105+
expect(logs).toContain(
106106
jasmine.objectContaining({
107-
message: jasmine.stringMatching(`Script file src/test-script-a.js does not exist.`),
107+
level: 'error',
108+
message: jasmine.stringMatching(`Can't resolve 'src/test-script-a.js'`),
108109
}),
109110
);
110111

packages/angular_devkit/build_angular/src/builders/browser/tests/options/styles_spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
121121

122122
expect(result?.success).toBeFalse();
123123
expect(logs).toContain(
124-
jasmine.objectContaining({ message: jasmine.stringMatching('Module not found:') }),
124+
jasmine.objectContaining({
125+
level: 'error',
126+
message: jasmine.stringMatching(`Can't resolve 'src/test-style-a.css'`),
127+
}),
125128
);
126129

127130
harness.expectFile('dist/styles.css').toNotExist();

packages/angular_devkit/build_angular/src/webpack/configs/common.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
147147

148148
// process global scripts
149149
// Add a new asset for each entry.
150-
for (const { bundleName, inject, paths } of globalScriptsByBundleName(
151-
root,
152-
buildOptions.scripts,
153-
)) {
150+
for (const { bundleName, inject, paths } of globalScriptsByBundleName(buildOptions.scripts)) {
154151
// Lazy scripts don't get a hash, otherwise they can't be loaded by name.
155152
const hash = inject ? hashFormat.script : '';
156153

@@ -160,7 +157,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
160157
sourceMap: scriptsSourceMap,
161158
scripts: paths,
162159
filename: `${path.basename(bundleName)}${hash}.js`,
163-
basePath: projectRoot,
160+
basePath: root,
164161
}),
165162
);
166163
}

packages/angular_devkit/build_angular/src/webpack/configs/styles.ts

+17-59
Original file line numberDiff line numberDiff line change
@@ -24,60 +24,14 @@ import {
2424
SuppressExtractedTextChunksWebpackPlugin,
2525
} from '../plugins';
2626
import { CssOptimizerPlugin } from '../plugins/css-optimizer-plugin';
27+
import { StylesWebpackPlugin } from '../plugins/styles-webpack-plugin';
2728
import {
2829
assetNameTemplateFactory,
2930
getOutputHashFormat,
3031
normalizeExtraEntryPoints,
32+
normalizeGlobalStyles,
3133
} from '../utils/helpers';
3234

33-
export function resolveGlobalStyles(
34-
styleEntrypoints: StyleElement[],
35-
root: string,
36-
preserveSymlinks: boolean,
37-
skipResolution = false,
38-
): { entryPoints: Record<string, string[]>; noInjectNames: string[]; paths: string[] } {
39-
const entryPoints: Record<string, string[]> = {};
40-
const noInjectNames: string[] = [];
41-
const paths: string[] = [];
42-
43-
if (styleEntrypoints.length === 0) {
44-
return { entryPoints, noInjectNames, paths };
45-
}
46-
47-
for (const style of normalizeExtraEntryPoints(styleEntrypoints, 'styles')) {
48-
let stylesheetPath = style.input;
49-
if (!skipResolution) {
50-
stylesheetPath = path.resolve(root, stylesheetPath);
51-
if (!fs.existsSync(stylesheetPath)) {
52-
try {
53-
stylesheetPath = require.resolve(style.input, { paths: [root] });
54-
} catch {}
55-
}
56-
}
57-
58-
if (!preserveSymlinks) {
59-
stylesheetPath = fs.realpathSync(stylesheetPath);
60-
}
61-
62-
// Add style entry points.
63-
if (entryPoints[style.bundleName]) {
64-
entryPoints[style.bundleName].push(stylesheetPath);
65-
} else {
66-
entryPoints[style.bundleName] = [stylesheetPath];
67-
}
68-
69-
// Add non injected styles to the list.
70-
if (!style.inject) {
71-
noInjectNames.push(style.bundleName);
72-
}
73-
74-
// Add global css paths.
75-
paths.push(stylesheetPath);
76-
}
77-
78-
return { entryPoints, noInjectNames, paths };
79-
}
80-
8135
// eslint-disable-next-line max-lines-per-function
8236
export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
8337
const { root, projectRoot, buildOptions } = wco;
@@ -95,14 +49,20 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
9549
buildOptions.stylePreprocessorOptions?.includePaths?.map((p) => path.resolve(root, p)) ?? [];
9650

9751
// Process global styles.
98-
const {
99-
entryPoints,
100-
noInjectNames,
101-
paths: globalStylePaths,
102-
} = resolveGlobalStyles(buildOptions.styles, root, !!buildOptions.preserveSymlinks);
103-
if (noInjectNames.length > 0) {
104-
// Add plugin to remove hashes from lazy styles.
105-
extraPlugins.push(new RemoveHashPlugin({ chunkNames: noInjectNames, hashFormat }));
52+
if (buildOptions.styles.length > 0) {
53+
const { entryPoints, noInjectNames } = normalizeGlobalStyles(buildOptions.styles);
54+
extraPlugins.push(
55+
new StylesWebpackPlugin({
56+
root,
57+
entryPoints,
58+
preserveSymlinks: buildOptions.preserveSymlinks,
59+
}),
60+
);
61+
62+
if (noInjectNames.length > 0) {
63+
// Add plugin to remove hashes from lazy styles.
64+
extraPlugins.push(new RemoveHashPlugin({ chunkNames: noInjectNames, hashFormat }));
65+
}
10666
}
10767

10868
const sassImplementation = useLegacySass
@@ -319,7 +279,6 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
319279
];
320280

321281
return {
322-
entry: entryPoints,
323282
module: {
324283
rules: styleLanguages.map(({ extensions, use }) => ({
325284
test: new RegExp(`\\.(?:${extensions.join('|')})$`, 'i'),
@@ -330,8 +289,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
330289
// Global styles are only defined global styles
331290
{
332291
use: globalStyleLoaders,
333-
include: globalStylePaths,
334-
resourceQuery: { not: [/\?ngResource/] },
292+
resourceQuery: /\?ngGlobalStyle/,
335293
},
336294
// Component styles are all styles except defined global styles
337295
{

packages/angular_devkit/build_angular/src/webpack/plugins/scripts-webpack-plugin.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import { interpolateName } from 'loader-utils';
1010
import * as path from 'path';
1111
import { Chunk, Compilation, Compiler, sources as webpackSources } from 'webpack';
12+
import { assertIsError } from '../../utils/error';
13+
import { addError } from '../../utils/webpack-diagnostics';
1214

1315
const Entrypoint = require('webpack/lib/Entrypoint');
1416

@@ -35,6 +37,7 @@ function addDependencies(compilation: Compilation, scripts: string[]): void {
3537
compilation.fileDependencies.add(script);
3638
}
3739
}
40+
3841
export class ScriptsWebpackPlugin {
3942
private _lastBuildTime?: number;
4043
private _cachedOutput?: ScriptOutput;
@@ -88,21 +91,38 @@ export class ScriptsWebpackPlugin {
8891
compilation.entrypoints.set(this.options.name, entrypoint);
8992
compilation.chunks.add(chunk);
9093

91-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
92-
compilation.assets[filename] = source as any;
94+
compilation.assets[filename] = source;
9395
compilation.hooks.chunkAsset.call(chunk, filename);
9496
}
9597

9698
apply(compiler: Compiler): void {
97-
if (!this.options.scripts || this.options.scripts.length === 0) {
99+
if (this.options.scripts.length === 0) {
98100
return;
99101
}
100102

101-
const scripts = this.options.scripts
102-
.filter((script) => !!script)
103-
.map((script) => path.resolve(this.options.basePath || '', script));
103+
const resolver = compiler.resolverFactory.get('normal', {
104+
preferRelative: true,
105+
useSyncFileSystemCalls: true,
106+
fileSystem: compiler.inputFileSystem,
107+
});
104108

105109
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
110+
const scripts: string[] = [];
111+
112+
for (const script of this.options.scripts) {
113+
try {
114+
const resolvedPath = resolver.resolveSync({}, this.options.basePath, script);
115+
if (resolvedPath) {
116+
scripts.push(resolvedPath);
117+
} else {
118+
addError(compilation, `Cannot resolve '${script}'.`);
119+
}
120+
} catch (error) {
121+
assertIsError(error);
122+
addError(compilation, error.message);
123+
}
124+
}
125+
106126
compilation.hooks.additionalAssets.tapPromise(PLUGIN_NAME, async () => {
107127
if (await this.shouldSkip(compilation, scripts)) {
108128
if (this._cachedOutput) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 assert from 'assert';
10+
import { pluginName } from 'mini-css-extract-plugin';
11+
import type { Compilation, Compiler } from 'webpack';
12+
import { assertIsError } from '../../utils/error';
13+
import { addError } from '../../utils/webpack-diagnostics';
14+
15+
export interface StylesWebpackPluginOptions {
16+
preserveSymlinks?: boolean;
17+
root: string;
18+
entryPoints: Record<string, string[]>;
19+
}
20+
21+
/**
22+
* The name of the plugin provided to Webpack when tapping Webpack compiler hooks.
23+
*/
24+
const PLUGIN_NAME = 'styles-webpack-plugin';
25+
26+
export class StylesWebpackPlugin {
27+
private compilation: Compilation | undefined;
28+
29+
constructor(private readonly options: StylesWebpackPluginOptions) {}
30+
31+
apply(compiler: Compiler): void {
32+
const { entryPoints, preserveSymlinks, root } = this.options;
33+
const webpackOptions = compiler.options;
34+
const entry =
35+
typeof webpackOptions.entry === 'function' ? webpackOptions.entry() : webpackOptions.entry;
36+
37+
const resolver = compiler.resolverFactory.get('global-styles', {
38+
conditionNames: ['sass', 'less', 'style'],
39+
mainFields: ['sass', 'less', 'style', 'main', '...'],
40+
extensions: ['.scss', '.sass', '.less', '.css'],
41+
restrictions: [/\.((le|sa|sc|c)ss)$/i],
42+
preferRelative: true,
43+
useSyncFileSystemCalls: true,
44+
symlinks: !preserveSymlinks,
45+
fileSystem: compiler.inputFileSystem,
46+
});
47+
48+
webpackOptions.entry = async () => {
49+
const entrypoints = await entry;
50+
51+
for (const [bundleName, paths] of Object.entries(entryPoints)) {
52+
entrypoints[bundleName] ??= {};
53+
const entryImport = (entrypoints[bundleName].import ??= []);
54+
55+
for (const path of paths) {
56+
try {
57+
const resolvedPath = resolver.resolveSync({}, root, path);
58+
if (resolvedPath) {
59+
entryImport.push(`${resolvedPath}?ngGlobalStyle`);
60+
} else {
61+
assert(this.compilation, 'Compilation cannot be undefined.');
62+
addError(this.compilation, `Cannot resolve '${path}'.`);
63+
}
64+
} catch (error) {
65+
assert(this.compilation, 'Compilation cannot be undefined.');
66+
assertIsError(error);
67+
addError(this.compilation, error.message);
68+
}
69+
}
70+
}
71+
72+
return entrypoints;
73+
};
74+
75+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
76+
this.compilation = compilation;
77+
});
78+
}
79+
}

0 commit comments

Comments
 (0)