Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion packages/angular/build/src/builders/unit-test/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
81 changes: 1 addition & 80 deletions packages/angular/build/src/tools/esbuild/bundler-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ import {
context,
} from 'esbuild';
import assert from 'node:assert';
import { realpathSync } from 'node:fs';
Comment thread
clydin marked this conversation as resolved.
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,
Expand Down Expand Up @@ -66,7 +64,6 @@ export class BundlerContext {
#optionsFactory: BundlerOptionsFactory<BuildOptions & { metafile: true; write: false }>;
#shouldCacheResult: boolean;
#loadCache?: MemoryLoadResultCache;
#realWorkspaceRoot?: string;
readonly watchFiles = new Set<string>();

constructor(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Comment thread
clydin marked this conversation as resolved.
const remapped = new Map<string, string>();
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:');
}
Expand Down
99 changes: 0 additions & 99 deletions packages/angular/build/src/tools/esbuild/bundler-context_spec.ts

This file was deleted.

14 changes: 5 additions & 9 deletions packages/angular/build/src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}
16 changes: 15 additions & 1 deletion packages/angular/build/src/utils/path_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Comment thread
clydin marked this conversation as resolved.
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');
});
}
});
Loading