diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 4ec810ff3c07..b3c180843f70 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -160,7 +160,7 @@ export async function normalizeOptions( options.preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'); // Setup base paths based on workspace root and project information - const workspaceRoot = canonicalizePath(context.workspaceRoot, preserveSymlinks); + const workspaceRoot = canonicalizePath(context.workspaceRoot); const projectMetadata = await context.getProjectMetadata(projectName); const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata); diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index d3402b090e0f..1206bb2c1f21 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -58,7 +58,7 @@ export async function normalizeOptions( : process.execArgv.includes('--preserve-symlinks'); // Setup base paths based on workspace root and project information - const workspaceRoot = canonicalizePath(context.workspaceRoot, preserveSymlinks); + const workspaceRoot = canonicalizePath(context.workspaceRoot); const projectMetadata = await context.getProjectMetadata(projectName); const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata); diff --git a/packages/angular/build/src/tools/esbuild/bundler-context.ts b/packages/angular/build/src/tools/esbuild/bundler-context.ts index 58f2df2a05c8..d3f3ca567a0f 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-context.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-context.ts @@ -17,9 +17,7 @@ import { context, } from 'esbuild'; import assert from 'node:assert'; -import { realpathSync } from 'node:fs'; -import { basename, extname, join, relative, resolve } from 'node:path'; -import { toPosixPath } from '../../utils/path'; +import { basename, extname, join, relative } from 'node:path'; import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest'; import { type BuildOutputFile, @@ -66,7 +64,6 @@ export class BundlerContext { #optionsFactory: BundlerOptionsFactory; #shouldCacheResult: boolean; #loadCache?: MemoryLoadResultCache; - #realWorkspaceRoot?: string; readonly watchFiles = new Set(); constructor( @@ -264,17 +261,6 @@ export class BundlerContext { } } - // esbuild always resolves its working directory through symbolic links (including - // Windows directory junctions) and generates metafile paths relative to the resolved - // path. When `preserveSymlinks` is enabled, the workspace root is intentionally not - // resolved, and the metafile paths are then relative to a different base directory. - // The paths are remapped so that all downstream consumers can rely on the documented - // invariant that metafile paths are relative to the workspace root. - this.#realWorkspaceRoot ??= realpathSync(this.workspaceRoot); - if (this.#realWorkspaceRoot !== this.workspaceRoot) { - remapMetafileBasePath(result.metafile, this.#realWorkspaceRoot, this.workspaceRoot); - } - // Update files that should be watched. // While this should technically not be linked to incremental mode, incremental is only // currently enabled with watch mode where watch files are needed. @@ -501,71 +487,6 @@ export class BundlerContext { } } -/** - * Remaps all relative paths within an esbuild metafile from one base directory to another. - * Virtual files (e.g., `angular:` namespaced or bundler generated), external imports, and - * non-relative paths are left unmodified. - * - * @param metafile The metafile to update in place. - * @param fromBase The absolute base directory the metafile paths are currently relative to. - * @param toBase The absolute base directory the metafile paths should be made relative to. - */ -export function remapMetafileBasePath(metafile: Metafile, fromBase: string, toBase: string): void { - const remapped = new Map(); - const remap = (value: string): string => { - // Skip virtual files and paths with a scheme-like or namespace prefix (e.g., `angular:`) - if ( - isInternalAngularFile(value) || - isInternalBundlerFile(value) || - /^[^\\/.]{2,}:/.test(value) - ) { - return value; - } - - let result = remapped.get(value); - if (result === undefined) { - // esbuild metafile paths always use POSIX path separators - result = toPosixPath(relative(toBase, resolve(fromBase, value))); - remapped.set(value, result); - } - - return result; - }; - - const inputs: Metafile['inputs'] = {}; - for (const [key, value] of Object.entries(metafile.inputs)) { - for (const importRecord of value.imports) { - if (!importRecord.external) { - importRecord.path = remap(importRecord.path); - } - } - inputs[remap(key)] = value; - } - metafile.inputs = inputs; - - const outputs: Metafile['outputs'] = {}; - for (const [key, value] of Object.entries(metafile.outputs)) { - if (value.entryPoint !== undefined) { - value.entryPoint = remap(value.entryPoint); - } - if (value.cssBundle !== undefined) { - value.cssBundle = remap(value.cssBundle); - } - for (const importRecord of value.imports) { - if (!importRecord.external) { - importRecord.path = remap(importRecord.path); - } - } - const outputInputs: (typeof value)['inputs'] = {}; - for (const [inputKey, inputValue] of Object.entries(value.inputs)) { - outputInputs[remap(inputKey)] = inputValue; - } - value.inputs = outputInputs; - outputs[remap(key)] = value; - } - metafile.outputs = outputs; -} - function isInternalAngularFile(file: string) { return file.startsWith('angular:'); } diff --git a/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts b/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts deleted file mode 100644 index 8806f1d90406..000000000000 --- a/packages/angular/build/src/tools/esbuild/bundler-context_spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @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.dev/license - */ - -import type { Metafile } from 'esbuild'; -import { join, relative } from 'node:path'; -import { remapMetafileBasePath } from './bundler-context'; - -describe('remapMetafileBasePath', () => { - // Simulates a workspace root accessed through a symbolic link or Windows - // directory junction (`toBase`) that resolves to a different real path - // (`fromBase`), as esbuild resolves its working directory through links. - const fromBase = join('/real', 'projects', 'demo'); - const toBase = join('/linked', 'demo'); - - /** Creates a metafile path as esbuild would: relative to the resolved (real) base. */ - const fromBaseRelative = (filePath: string): string => relative(fromBase, join(toBase, filePath)); - - it('remaps input and output paths onto the target base directory', () => { - const metafile: Metafile = { - inputs: { - [fromBaseRelative('src/main.ts')]: { bytes: 10, imports: [] }, - }, - outputs: { - [fromBaseRelative('main.js')]: { - bytes: 100, - inputs: { [fromBaseRelative('src/main.ts')]: { bytesInOutput: 10 } }, - imports: [{ path: fromBaseRelative('chunk-ABC.js'), kind: 'import-statement' }], - exports: [], - entryPoint: fromBaseRelative('src/main.ts'), - cssBundle: fromBaseRelative('main.css'), - }, - }, - }; - - remapMetafileBasePath(metafile, fromBase, toBase); - - expect(Object.keys(metafile.inputs)).toEqual(['src/main.ts']); - expect(Object.keys(metafile.outputs)).toEqual(['main.js']); - - const output = metafile.outputs['main.js']; - expect(output.entryPoint).toBe('src/main.ts'); - expect(output.cssBundle).toBe('main.css'); - expect(Object.keys(output.inputs)).toEqual(['src/main.ts']); - expect(output.imports[0].path).toBe('chunk-ABC.js'); - }); - - it('does not modify virtual and namespaced files', () => { - const metafile: Metafile = { - inputs: { - 'angular:polyfills': { - bytes: 10, - imports: [{ path: '', kind: 'import-statement' }], - }, - }, - outputs: { - [fromBaseRelative('polyfills.js')]: { - bytes: 100, - inputs: { 'angular:polyfills': { bytesInOutput: 10 } }, - imports: [], - exports: [], - entryPoint: 'angular:polyfills', - }, - }, - }; - - remapMetafileBasePath(metafile, fromBase, toBase); - - expect(Object.keys(metafile.inputs)).toEqual(['angular:polyfills']); - expect(metafile.inputs['angular:polyfills'].imports[0].path).toBe(''); - - const output = metafile.outputs['polyfills.js']; - expect(output.entryPoint).toBe('angular:polyfills'); - expect(Object.keys(output.inputs)).toEqual(['angular:polyfills']); - }); - - it('does not modify external imports', () => { - const externalPath = 'https://example.com/module.js'; - const metafile: Metafile = { - inputs: {}, - outputs: { - [fromBaseRelative('main.js')]: { - bytes: 100, - inputs: {}, - imports: [{ path: externalPath, kind: 'import-statement', external: true }], - exports: [], - }, - }, - }; - - remapMetafileBasePath(metafile, fromBase, toBase); - - expect(metafile.outputs['main.js'].imports[0].path).toBe(externalPath); - }); -}); diff --git a/packages/angular/build/src/utils/path.ts b/packages/angular/build/src/utils/path.ts index 4056972f790c..a236878d134e 100644 --- a/packages/angular/build/src/utils/path.ts +++ b/packages/angular/build/src/utils/path.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { realpathSync } from 'node:fs'; import { isAbsolute, posix, relative, resolve } from 'node:path'; import { platform } from 'node:process'; @@ -53,18 +52,15 @@ export function isSubDirectory(parent: string, child: string): boolean { } /** - * Canonicalizes a file path by normalising Windows drive-letter casing to uppercase - * and optionally resolving symbolic links. + * Canonicalizes a file path by normalising Windows drive-letter casing to uppercase. * * @param pathString - The file path to canonicalize. - * @param preserveSymlinks - If true, symbolic links will not be resolved. * @returns The canonicalized file path. */ -export function canonicalizePath(pathString: string, preserveSymlinks = false): string { - const resolved = preserveSymlinks ? pathString : realpathSync(pathString); - if (platform === 'win32' && /^[a-z]:/.test(resolved)) { - return resolved[0].toUpperCase() + resolved.slice(1); +export function canonicalizePath(pathString: string): string { + if (platform === 'win32' && /^[a-z]:/.test(pathString)) { + return pathString[0].toUpperCase() + pathString.slice(1); } - return resolved; + return pathString; } diff --git a/packages/angular/build/src/utils/path_spec.ts b/packages/angular/build/src/utils/path_spec.ts index 84c7d8ac31c4..cd8ca3372433 100644 --- a/packages/angular/build/src/utils/path_spec.ts +++ b/packages/angular/build/src/utils/path_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { isSubDirectory } from './path'; +import { canonicalizePath, isSubDirectory } from './path'; describe('isSubDirectory', () => { it('should return true for a direct child', () => { @@ -39,3 +39,17 @@ describe('isSubDirectory', () => { expect(isSubDirectory('/foo/bar', '/foo/bar/..baz/qux')).toBeTrue(); }); }); + +describe('canonicalizePath', () => { + it('should return the path unmodified on POSIX systems', () => { + expect(canonicalizePath('/foo/bar/baz')).toBe('/foo/bar/baz'); + }); + + if (process.platform === 'win32') { + it('should uppercase Windows drive-letter casing', () => { + expect(canonicalizePath('c:/foo/bar')).toBe('C:/foo/bar'); + expect(canonicalizePath('d:\\foo\\bar')).toBe('D:\\foo\\bar'); + expect(canonicalizePath('C:/foo/bar')).toBe('C:/foo/bar'); + }); + } +});