Skip to content

Commit ddd9bfc

Browse files
committed
refactor(@angular/build): remove unnecessary realpath resolution for workspace root
Previously, `canonicalizePath` resolved `context.workspaceRoot` using `realpathSync` unless `preserveSymlinks` was enabled. Resolving the workspace root was originally intended to align workspace paths with Node/TypeScript default physical file resolution, but created an unnecessary path asymmetry between `preserveSymlinks` settings and mutated the logical workspace path. Furthermore, passing the resolved real path as `absWorkingDir` to esbuild caused esbuild to format metafile paths relative to the real path rather than the logical workspace root. This required workarounds to remap metafile paths back to the workspace root. This change is safe today because Angular compiler plugins and path resolution utilities normalize relative paths dynamically without requiring `workspaceRoot` to be realpath'd. Removing `realpathSync` from `canonicalizePath` ensures `workspaceRoot` consistently remains the logical workspace path across all builders, esbuild natively formats metafile paths relative to `workspaceRoot`, and `remapMetafileBasePath` is no longer needed.
1 parent f8ccc41 commit ddd9bfc

6 files changed

Lines changed: 14 additions & 179 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export async function normalizeOptions(
160160
options.preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks');
161161

162162
// Setup base paths based on workspace root and project information
163-
const workspaceRoot = canonicalizePath(context.workspaceRoot, preserveSymlinks);
163+
const workspaceRoot = canonicalizePath(context.workspaceRoot);
164164
const projectMetadata = await context.getProjectMetadata(projectName);
165165
const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata);
166166

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export async function normalizeOptions(
5858
: process.execArgv.includes('--preserve-symlinks');
5959

6060
// Setup base paths based on workspace root and project information
61-
const workspaceRoot = canonicalizePath(context.workspaceRoot, preserveSymlinks);
61+
const workspaceRoot = canonicalizePath(context.workspaceRoot);
6262

6363
const projectMetadata = await context.getProjectMetadata(projectName);
6464
const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata);

packages/angular/build/src/tools/esbuild/bundler-context.ts

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
context,
1818
} from 'esbuild';
1919
import assert from 'node:assert';
20-
import { realpathSync } from 'node:fs';
2120
import { basename, extname, join, relative, resolve } from 'node:path';
2221
import { toPosixPath } from '../../utils/path';
2322
import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest';
@@ -66,7 +65,6 @@ export class BundlerContext {
6665
#optionsFactory: BundlerOptionsFactory<BuildOptions & { metafile: true; write: false }>;
6766
#shouldCacheResult: boolean;
6867
#loadCache?: MemoryLoadResultCache;
69-
#realWorkspaceRoot?: string;
7068
readonly watchFiles = new Set<string>();
7169

7270
constructor(
@@ -264,17 +262,6 @@ export class BundlerContext {
264262
}
265263
}
266264

267-
// esbuild always resolves its working directory through symbolic links (including
268-
// Windows directory junctions) and generates metafile paths relative to the resolved
269-
// path. When `preserveSymlinks` is enabled, the workspace root is intentionally not
270-
// resolved, and the metafile paths are then relative to a different base directory.
271-
// The paths are remapped so that all downstream consumers can rely on the documented
272-
// invariant that metafile paths are relative to the workspace root.
273-
this.#realWorkspaceRoot ??= realpathSync(this.workspaceRoot);
274-
if (this.#realWorkspaceRoot !== this.workspaceRoot) {
275-
remapMetafileBasePath(result.metafile, this.#realWorkspaceRoot, this.workspaceRoot);
276-
}
277-
278265
// Update files that should be watched.
279266
// While this should technically not be linked to incremental mode, incremental is only
280267
// currently enabled with watch mode where watch files are needed.
@@ -510,61 +497,6 @@ export class BundlerContext {
510497
* @param fromBase The absolute base directory the metafile paths are currently relative to.
511498
* @param toBase The absolute base directory the metafile paths should be made relative to.
512499
*/
513-
export function remapMetafileBasePath(metafile: Metafile, fromBase: string, toBase: string): void {
514-
const remapped = new Map<string, string>();
515-
const remap = (value: string): string => {
516-
// Skip virtual files and paths with a scheme-like or namespace prefix (e.g., `angular:`)
517-
if (
518-
isInternalAngularFile(value) ||
519-
isInternalBundlerFile(value) ||
520-
/^[^\\/.]{2,}:/.test(value)
521-
) {
522-
return value;
523-
}
524-
525-
let result = remapped.get(value);
526-
if (result === undefined) {
527-
// esbuild metafile paths always use POSIX path separators
528-
result = toPosixPath(relative(toBase, resolve(fromBase, value)));
529-
remapped.set(value, result);
530-
}
531-
532-
return result;
533-
};
534-
535-
const inputs: Metafile['inputs'] = {};
536-
for (const [key, value] of Object.entries(metafile.inputs)) {
537-
for (const importRecord of value.imports) {
538-
if (!importRecord.external) {
539-
importRecord.path = remap(importRecord.path);
540-
}
541-
}
542-
inputs[remap(key)] = value;
543-
}
544-
metafile.inputs = inputs;
545-
546-
const outputs: Metafile['outputs'] = {};
547-
for (const [key, value] of Object.entries(metafile.outputs)) {
548-
if (value.entryPoint !== undefined) {
549-
value.entryPoint = remap(value.entryPoint);
550-
}
551-
if (value.cssBundle !== undefined) {
552-
value.cssBundle = remap(value.cssBundle);
553-
}
554-
for (const importRecord of value.imports) {
555-
if (!importRecord.external) {
556-
importRecord.path = remap(importRecord.path);
557-
}
558-
}
559-
const outputInputs: (typeof value)['inputs'] = {};
560-
for (const [inputKey, inputValue] of Object.entries(value.inputs)) {
561-
outputInputs[remap(inputKey)] = inputValue;
562-
}
563-
value.inputs = outputInputs;
564-
outputs[remap(key)] = value;
565-
}
566-
metafile.outputs = outputs;
567-
}
568500

569501
function isInternalAngularFile(file: string) {
570502
return file.startsWith('angular:');

packages/angular/build/src/tools/esbuild/bundler-context_spec.ts

Lines changed: 0 additions & 99 deletions
This file was deleted.

packages/angular/build/src/utils/path.ts

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

9-
import { realpathSync } from 'node:fs';
109
import { isAbsolute, posix, relative, resolve } from 'node:path';
1110
import { platform } from 'node:process';
1211

@@ -53,18 +52,15 @@ export function isSubDirectory(parent: string, child: string): boolean {
5352
}
5453

5554
/**
56-
* Canonicalizes a file path by normalising Windows drive-letter casing to uppercase
57-
* and optionally resolving symbolic links.
55+
* Canonicalizes a file path by normalising Windows drive-letter casing to uppercase.
5856
*
5957
* @param pathString - The file path to canonicalize.
60-
* @param preserveSymlinks - If true, symbolic links will not be resolved.
6158
* @returns The canonicalized file path.
6259
*/
63-
export function canonicalizePath(pathString: string, preserveSymlinks = false): string {
64-
const resolved = preserveSymlinks ? pathString : realpathSync(pathString);
65-
if (platform === 'win32' && /^[a-z]:/.test(resolved)) {
66-
return resolved[0].toUpperCase() + resolved.slice(1);
60+
export function canonicalizePath(pathString: string): string {
61+
if (platform === 'win32' && /^[a-z]:/.test(pathString)) {
62+
return pathString[0].toUpperCase() + pathString.slice(1);
6763
}
6864

69-
return resolved;
65+
return pathString;
7066
}

packages/angular/build/src/utils/path_spec.ts

Lines changed: 7 additions & 1 deletion
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 { isSubDirectory } from './path';
9+
import { canonicalizePath, isSubDirectory } from './path';
1010

1111
describe('isSubDirectory', () => {
1212
it('should return true for a direct child', () => {
@@ -39,3 +39,9 @@ describe('isSubDirectory', () => {
3939
expect(isSubDirectory('/foo/bar', '/foo/bar/..baz/qux')).toBeTrue();
4040
});
4141
});
42+
43+
describe('canonicalizePath', () => {
44+
it('should return the path unmodified on POSIX systems', () => {
45+
expect(canonicalizePath('/foo/bar/baz')).toBe('/foo/bar/baz');
46+
});
47+
});

0 commit comments

Comments
 (0)