Skip to content

Commit 4288e44

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular/build): scope createRequire module resolution using paths to prevent parent paths
Ensure that custom resolvers created via createRequire use the strict paths resolution option when checking for the presence of local packages (such as zone.js, @angular/localize, @angular/core, or vitest test environments). This prevents Node's resolver from using the parent module context (located inside the virtual store .pnpm) and falsely finding packages that are not direct dependencies of the project. (cherry picked from commit 0010b92)
1 parent 5f774f8 commit 4288e44

7 files changed

Lines changed: 43 additions & 20 deletions

File tree

packages/angular/build/src/builders/application/options.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
loadPostcssConfiguration,
2626
} from '../../utils/postcss-configuration';
2727
import { getProjectRootPaths, normalizeDirectoryPath } from '../../utils/project-metadata';
28+
import { createProjectResolver } from '../../utils/resolve-project';
2829
import { addTrailingSlash, joinUrlParts, stripLeadingSlash } from '../../utils/url';
2930
import {
3031
Schema as ApplicationBuilderOptions,
@@ -728,9 +729,7 @@ function normalizeExternals(value: string[] | undefined): string[] | undefined {
728729
}
729730

730731
async function findFrameworkVersion(projectRoot: string): Promise<string> {
731-
// Create a custom require function for ESM compliance.
732-
// NOTE: The trailing slash is significant.
733-
const projectResolve = createRequire(projectRoot + '/').resolve;
732+
const projectResolve = createProjectResolver(projectRoot);
734733

735734
try {
736735
const manifestPath = projectResolve('@angular/core/package.json');

packages/angular/build/src/builders/karma/application_builder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { createRequire } from 'node:module';
1515
import path from 'node:path';
1616
import { ReadableStream } from 'node:stream/web';
1717
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
18+
import { createProjectResolver } from '../../utils/resolve-project';
1819
import { writeTestFiles } from '../../utils/test-files';
1920
import { buildApplicationInternal } from '../application/index';
2021
import { ApplicationBuilderInternalOptions } from '../application/options';
@@ -202,8 +203,8 @@ async function runEsbuild(
202203
const usesZoneJS = buildOptions.polyfills?.includes('zone.js');
203204
let hasLocalize = false;
204205
try {
205-
const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json'));
206-
projectRequire.resolve('@angular/localize');
206+
const projectResolve = createProjectResolver(projectSourceRoot);
207+
projectResolve('@angular/localize');
207208
hasLocalize = true;
208209
} catch {}
209210

packages/angular/build/src/builders/unit-test/runners/dependency-checker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { createRequire } from 'node:module';
9+
import { createProjectResolver } from '../../../utils/resolve-project';
1010

1111
/**
1212
* A custom error class to represent missing dependency errors.
@@ -26,7 +26,7 @@ export class DependencyChecker {
2626
private readonly missingDependencies = new Set<string>();
2727

2828
constructor(projectSourceRoot: string) {
29-
this.resolver = createRequire(projectSourceRoot + '/').resolve;
29+
this.resolver = createProjectResolver(projectSourceRoot);
3030
}
3131

3232
/**

packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { createRequire } from 'node:module';
109
import type {
1110
BrowserBuiltinProvider,
1211
BrowserConfigOptions,
1312
BrowserProviderOption,
1413
} from 'vitest/node';
1514
import { assertIsError } from '../../../../utils/error';
15+
import { createProjectResolver } from '../../../../utils/resolve-project';
1616

1717
export interface BrowserConfiguration {
1818
browser?: BrowserConfigOptions;
@@ -21,7 +21,7 @@ export interface BrowserConfiguration {
2121
}
2222

2323
function findBrowserProvider(
24-
projectResolver: NodeJS.RequireResolve,
24+
projectResolver: (packageName: string) => string,
2525
): BrowserBuiltinProvider | undefined {
2626
const requiresPreview = !!process.versions.webcontainer;
2727

@@ -138,7 +138,7 @@ export async function setupBrowserConfiguration(
138138
return {};
139139
}
140140

141-
const projectResolver = createRequire(projectSourceRoot + '/').resolve;
141+
const projectResolver = createProjectResolver(projectSourceRoot);
142142
let errors: string[] | undefined;
143143

144144
const providerName = findBrowserProvider(projectResolver);

packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
* Provides Vitest-specific build options and virtual file contents for Angular unit testing.
1212
*/
1313

14-
import { createRequire } from 'node:module';
1514
import path from 'node:path';
1615
import { toPosixPath } from '../../../../utils/path';
16+
import { createProjectResolver } from '../../../../utils/resolve-project';
1717
import type { ApplicationBuilderInternalOptions } from '../../../application/options';
1818
import { OutputHashing } from '../../../application/schema';
1919
import { NormalizedUnitTestBuilderOptions } from '../../options';
@@ -169,8 +169,8 @@ function getZoneTestingStrategy(
169169
}
170170

171171
try {
172-
const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json'));
173-
projectRequire.resolve('zone.js');
172+
const projectResolve = createProjectResolver(projectSourceRoot);
173+
projectResolve('zone.js');
174174

175175
// If polyfills is undefined (e.g. library build target), load zone.js dynamically.
176176
// If polyfills is defined but doesn't include zone.js (e.g. zoneless application), do NOT load zone.js.
@@ -268,8 +268,8 @@ export async function getVitestBuildOptions(
268268

269269
let hasLocalize = false;
270270
try {
271-
const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json'));
272-
projectRequire.resolve('@angular/localize');
271+
const projectResolve = createProjectResolver(projectSourceRoot);
272+
projectResolve('@angular/localize');
273273
hasLocalize = true;
274274
} catch {}
275275

packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import assert from 'node:assert';
1010
import { readFile } from 'node:fs/promises';
11-
import { createRequire } from 'node:module';
1211
import { platform } from 'node:os';
1312
import path from 'node:path';
1413

@@ -21,6 +20,7 @@ import type {
2120
} from 'vitest/node';
2221
import { createBuildAssetsMiddleware } from '../../../../tools/vite/middlewares/assets-middleware';
2322
import { toPosixPath } from '../../../../utils/path';
23+
import { createProjectResolver } from '../../../../utils/resolve-project';
2424
import type { ResultFile } from '../../../application/results';
2525
import type { NormalizedUnitTestBuilderOptions } from '../../options';
2626
import { normalizeBrowserName } from './browser-provider';
@@ -59,7 +59,7 @@ interface VitestConfigPluginOptions {
5959
}
6060

6161
async function findTestEnvironment(
62-
projectResolver: NodeJS.RequireResolve,
62+
projectResolver: (packageName: string) => string,
6363
): Promise<'jsdom' | 'happy-dom'> {
6464
try {
6565
projectResolver('happy-dom');
@@ -88,10 +88,10 @@ function determineCoverageProvider(
8888
if (hasNonChromium) {
8989
determinedProvider = 'istanbul';
9090
} else {
91-
const projectRequire = createRequire(projectSourceRoot + '/');
91+
const projectResolve = createProjectResolver(projectSourceRoot);
9292
const checkInstalled = (pkg: string) => {
9393
try {
94-
projectRequire.resolve(pkg);
94+
projectResolve(pkg);
9595

9696
return true;
9797
} catch {
@@ -242,7 +242,7 @@ export async function createVitestConfigPlugin(
242242
validateBrowserCoverage(browser, testConfig?.browser, determinedProvider);
243243
}
244244

245-
const projectResolver = createRequire(projectSourceRoot + '/').resolve;
245+
const projectResolver = createProjectResolver(projectSourceRoot);
246246

247247
const projectDefaults: UserWorkspaceConfig = {
248248
test: {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.dev/license
7+
*/
8+
9+
import { createRequire } from 'node:module';
10+
import { join } from 'node:path';
11+
12+
/**
13+
* Creates a module resolver function that is strictly scoped to the project root.
14+
* This prevents module resolution from leaking the parent module context when executing inside virtual stores (like pnpm).
15+
*
16+
* @param projectRoot The root directory of the project.
17+
* @returns A resolver function that takes a package name/path and returns its resolved path.
18+
*/
19+
export function createProjectResolver(projectRoot: string): (packageName: string) => string {
20+
const projectRequire = createRequire(join(projectRoot, 'package.json'));
21+
22+
return (packageName: string) => projectRequire.resolve(packageName, { paths: [projectRoot] });
23+
}

0 commit comments

Comments
 (0)