Skip to content

Commit 6f386d8

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 91a6bd4 commit 6f386d8

File tree

8 files changed

+119
-72
lines changed

8 files changed

+119
-72
lines changed

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

Lines changed: 2 additions & 7 deletions
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,6 @@ describe('Browser Builder scripts array', () => {
151151
browserBuild(architect, host, target, {
152152
scripts: ['./invalid.js'],
153153
}),
154-
).toBeRejectedWithError(`Script file ./invalid.js does not exist.`);
154+
).toBeRejectedWithError(/Can't resolve '\.\/invalid\.js'/);
155155
});
156156
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
104104
expect(result).toBeUndefined();
105105
expect(error).toEqual(
106106
jasmine.objectContaining({
107-
message: jasmine.stringMatching(`Script file src/test-script-a.js does not exist.`),
107+
message: jasmine.stringMatching(`Can't resolve 'src/test-script-a.js'`),
108108
}),
109109
);
110110

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

Lines changed: 2 additions & 5 deletions
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

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,58 +22,36 @@ import {
2222
SuppressExtractedTextChunksWebpackPlugin,
2323
} from '../plugins';
2424
import { CssOptimizerPlugin } from '../plugins/css-optimizer-plugin';
25+
import { StylesWebpackPlugin } from '../plugins/styles-webpack-plugin';
2526
import {
2627
assetNameTemplateFactory,
2728
getOutputHashFormat,
2829
normalizeExtraEntryPoints,
2930
} from '../utils/helpers';
3031

31-
export function resolveGlobalStyles(
32-
styleEntrypoints: StyleElement[],
33-
root: string,
34-
preserveSymlinks: boolean,
35-
skipResolution = false,
36-
): { entryPoints: Record<string, string[]>; noInjectNames: string[]; paths: string[] } {
32+
export function normalizeGlobalStyles(styleEntrypoints: StyleElement[]): {
33+
entryPoints: Record<string, string[]>;
34+
noInjectNames: string[];
35+
} {
3736
const entryPoints: Record<string, string[]> = {};
3837
const noInjectNames: string[] = [];
39-
const paths: string[] = [];
4038

4139
if (styleEntrypoints.length === 0) {
42-
return { entryPoints, noInjectNames, paths };
40+
return { entryPoints, noInjectNames };
4341
}
4442

4543
for (const style of normalizeExtraEntryPoints(styleEntrypoints, 'styles')) {
46-
let stylesheetPath = style.input;
47-
if (!skipResolution) {
48-
stylesheetPath = path.resolve(root, stylesheetPath);
49-
if (!fs.existsSync(stylesheetPath)) {
50-
try {
51-
stylesheetPath = require.resolve(style.input, { paths: [root] });
52-
} catch {}
53-
}
54-
}
55-
56-
if (!preserveSymlinks) {
57-
stylesheetPath = fs.realpathSync(stylesheetPath);
58-
}
59-
6044
// Add style entry points.
61-
if (entryPoints[style.bundleName]) {
62-
entryPoints[style.bundleName].push(stylesheetPath);
63-
} else {
64-
entryPoints[style.bundleName] = [stylesheetPath];
65-
}
45+
entryPoints[style.bundleName] ??= [];
46+
entryPoints[style.bundleName].push(style.input);
6647

6748
// Add non injected styles to the list.
6849
if (!style.inject) {
6950
noInjectNames.push(style.bundleName);
7051
}
71-
72-
// Add global css paths.
73-
paths.push(stylesheetPath);
7452
}
7553

76-
return { entryPoints, noInjectNames, paths };
54+
return { entryPoints, noInjectNames };
7755
}
7856

7957
// eslint-disable-next-line max-lines-per-function
@@ -93,14 +71,20 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
9371
buildOptions.stylePreprocessorOptions?.includePaths?.map((p) => path.resolve(root, p)) ?? [];
9472

9573
// Process global styles.
96-
const {
97-
entryPoints,
98-
noInjectNames,
99-
paths: globalStylePaths,
100-
} = resolveGlobalStyles(buildOptions.styles, root, !!buildOptions.preserveSymlinks);
101-
if (noInjectNames.length > 0) {
102-
// Add plugin to remove hashes from lazy styles.
103-
extraPlugins.push(new RemoveHashPlugin({ chunkNames: noInjectNames, hashFormat }));
74+
if (buildOptions.styles.length > 0) {
75+
const { entryPoints, noInjectNames } = normalizeGlobalStyles(buildOptions.styles);
76+
extraPlugins.push(
77+
new StylesWebpackPlugin({
78+
root,
79+
entryPoints,
80+
preserveSymlinks: buildOptions.preserveSymlinks,
81+
}),
82+
);
83+
84+
if (noInjectNames.length > 0) {
85+
// Add plugin to remove hashes from lazy styles.
86+
extraPlugins.push(new RemoveHashPlugin({ chunkNames: noInjectNames, hashFormat }));
87+
}
10488
}
10589

10690
const sassImplementation = useLegacySass
@@ -317,7 +301,6 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
317301
];
318302

319303
return {
320-
entry: entryPoints,
321304
module: {
322305
rules: styleLanguages.map(({ extensions, use }) => ({
323306
test: new RegExp(`\\.(?:${extensions.join('|')})$`, 'i'),
@@ -328,8 +311,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
328311
// Global styles are only defined global styles
329312
{
330313
use: globalStyleLoaders,
331-
include: globalStylePaths,
332-
resourceQuery: { not: [/\?ngResource/] },
314+
resourceQuery: /\?ngGlobalStyle/,
333315
},
334316
// Component styles are all styles except defined global styles
335317
{

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function addDependencies(compilation: Compilation, scripts: string[]): void {
3535
compilation.fileDependencies.add(script);
3636
}
3737
}
38+
3839
export class ScriptsWebpackPlugin {
3940
private _lastBuildTime?: number;
4041
private _cachedOutput?: ScriptOutput;
@@ -94,13 +95,19 @@ export class ScriptsWebpackPlugin {
9495
}
9596

9697
apply(compiler: Compiler): void {
97-
if (!this.options.scripts || this.options.scripts.length === 0) {
98+
if (!this.options.scripts.length) {
9899
return;
99100
}
100101

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

105112
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
106113
compilation.hooks.additionalAssets.tapPromise(PLUGIN_NAME, async () => {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 { Compiler } from 'webpack';
10+
11+
export interface StylesWebpackPluginOptions {
12+
preserveSymlinks?: boolean;
13+
root: string;
14+
entryPoints: Record<string, string[]>;
15+
}
16+
17+
export class StylesWebpackPlugin {
18+
constructor(private readonly options: StylesWebpackPluginOptions) {}
19+
20+
apply(compiler: Compiler): void {
21+
const { entryPoints, preserveSymlinks, root } = this.options;
22+
const webpackOptions = compiler.options;
23+
const entry =
24+
typeof webpackOptions.entry === 'function' ? webpackOptions.entry() : webpackOptions.entry;
25+
26+
const resolver = compiler.resolverFactory.get('global-styles', {
27+
conditionNames: ['sass', 'less', 'style'],
28+
mainFields: ['sass', 'less', 'style', 'main', '...'],
29+
extensions: ['.scss', '.sass', '.less', '.css'],
30+
restrictions: [/\.((le|sa|sc|c)ss)$/i],
31+
preferRelative: true,
32+
useSyncFileSystemCalls: true,
33+
symlinks: !preserveSymlinks,
34+
fileSystem: compiler.inputFileSystem,
35+
});
36+
37+
const normalizedEntryPoint: typeof entry = {};
38+
for (const [bundleName, paths] of Object.entries(entryPoints)) {
39+
normalizedEntryPoint[bundleName] = {
40+
import: paths.map((p) => `${resolver.resolveSync({}, root, p) || p}?ngGlobalStyle`),
41+
};
42+
}
43+
44+
webpackOptions.entry = async () => {
45+
return {
46+
...(await entry),
47+
...normalizedEntryPoint,
48+
};
49+
};
50+
}
51+
}

packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,21 +176,11 @@ export function getCacheSettings(
176176
}
177177

178178
export function globalScriptsByBundleName(
179-
root: string,
180179
scripts: ScriptElement[],
181180
): { bundleName: string; inject: boolean; paths: string[] }[] {
182181
return normalizeExtraEntryPoints(scripts, 'scripts').reduce(
183182
(prev: { bundleName: string; paths: string[]; inject: boolean }[], curr) => {
184183
const { bundleName, inject, input } = curr;
185-
let resolvedPath = path.resolve(root, input);
186-
187-
if (!existsSync(resolvedPath)) {
188-
try {
189-
resolvedPath = require.resolve(input, { paths: [root] });
190-
} catch {
191-
throw new Error(`Script file ${input} does not exist.`);
192-
}
193-
}
194184

195185
const existingEntry = prev.find((el) => el.bundleName === bundleName);
196186
if (existingEntry) {
@@ -199,12 +189,12 @@ export function globalScriptsByBundleName(
199189
throw new Error(`The ${bundleName} bundle is mixing injected and non-injected scripts.`);
200190
}
201191

202-
existingEntry.paths.push(resolvedPath);
192+
existingEntry.paths.push(input);
203193
} else {
204194
prev.push({
205195
bundleName,
206196
inject,
207-
paths: [resolvedPath],
197+
paths: [input],
208198
});
209199
}
210200

@@ -214,6 +204,31 @@ export function globalScriptsByBundleName(
214204
);
215205
}
216206

207+
export function normalizeGlobalStyles(styleEntrypoints: StyleElement[]): {
208+
entryPoints: Record<string, string[]>;
209+
noInjectNames: string[];
210+
} {
211+
const entryPoints: Record<string, string[]> = {};
212+
const noInjectNames: string[] = [];
213+
214+
if (styleEntrypoints.length === 0) {
215+
return { entryPoints, noInjectNames };
216+
}
217+
218+
for (const style of normalizeExtraEntryPoints(styleEntrypoints, 'styles')) {
219+
// Add style entry points.
220+
entryPoints[style.bundleName] ??= [];
221+
entryPoints[style.bundleName].push(style.input);
222+
223+
// Add non injected styles to the list.
224+
if (!style.inject) {
225+
noInjectNames.push(style.bundleName);
226+
}
227+
}
228+
229+
return { entryPoints, noInjectNames };
230+
}
231+
217232
export function assetPatterns(root: string, assets: AssetPatternClass[]) {
218233
return assets.map((asset: AssetPatternClass, index: number): ObjectPattern => {
219234
// Resolve input paths relative to workspace root and add slash at the end.

0 commit comments

Comments
 (0)