From 0a655f3ab71125610069849effe2d5aeabed1683 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:32:32 +0000 Subject: [PATCH 1/2] fix(@angular/build): normalize code coverage include paths to POSIX Ensures that code coverage `include` patterns are converted to a POSIX-style format. Closes #30698 --- .../build/src/builders/unit-test/builder.ts | 14 ++++--- packages/angular/build/src/utils/path.ts | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 packages/angular/build/src/utils/path.ts diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index fd9f88580d70..c62310e0798d 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -14,6 +14,7 @@ import path from 'node:path'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; import { assertIsError } from '../../utils/error'; import { loadEsmModule } from '../../utils/load-esm'; +import { toPosixPath } from '../../utils/path'; import { buildApplicationInternal } from '../application'; import type { ApplicationBuilderExtensions, @@ -117,7 +118,7 @@ export async function* execute( buildTargetOptions.polyfills = injectTestingPolyfills(buildTargetOptions.polyfills); - const outputPath = path.join(context.workspaceRoot, generateOutputPath()); + const outputPath = toPosixPath(path.join(context.workspaceRoot, generateOutputPath())); const buildOptions: ApplicationBuilderInternalOptions = { ...buildTargetOptions, watch: normalizedOptions.watch, @@ -156,10 +157,11 @@ export async function* execute( `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`, '', normalizedOptions.providersFile - ? `import providers from './${path - .relative(projectSourceRoot, normalizedOptions.providersFile) - .replace(/.[mc]?ts$/, '') - .replace(/\\/g, '/')}'` + ? `import providers from './${toPosixPath( + path + .relative(projectSourceRoot, normalizedOptions.providersFile) + .replace(/.[mc]?ts$/, ''), + )}'` : 'const providers = [];', '', // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/src/test_hooks.ts#L21-L29 @@ -406,7 +408,7 @@ function generateCoverageOption( return { enabled: true, excludeAfterRemap: true, - include: [`${path.relative(workspaceRoot, outputPath)}/**`], + include: [`${toPosixPath(path.relative(workspaceRoot, outputPath))}/**`], // Special handling for `reporter` due to an undefined value causing upstream failures ...(codeCoverage.reporters ? ({ reporter: codeCoverage.reporters } satisfies VitestCoverageOption) diff --git a/packages/angular/build/src/utils/path.ts b/packages/angular/build/src/utils/path.ts new file mode 100644 index 000000000000..036dcb23502e --- /dev/null +++ b/packages/angular/build/src/utils/path.ts @@ -0,0 +1,37 @@ +/** + * @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 { posix } from 'node:path'; +import { platform } from 'node:process'; + +const WINDOWS_PATH_SEPERATOR_REGEXP = /\\/g; + +/** + * Converts a Windows-style file path to a POSIX-compliant path. + * + * This function replaces all backslashes (`\`) with forward slashes (`/`). + * It is a no-op on POSIX systems (e.g., Linux, macOS), as the conversion + * only runs on Windows (`win32`). + * + * @param path - The file path to convert. + * @returns The POSIX-compliant file path. + * + * @example + * ```ts + * // On a Windows system: + * toPosixPath('C:\\Users\\Test\\file.txt'); + * // => 'C:/Users/Test/file.txt' + * + * // On a POSIX system (Linux/macOS): + * toPosixPath('/home/user/file.txt'); + * // => '/home/user/file.txt' + * ``` + */ +export function toPosixPath(path: string): string { + return platform === 'win32' ? path.replace(WINDOWS_PATH_SEPERATOR_REGEXP, posix.sep) : path; +} From a2fc52cd5b6c35dc8e6e5a3417b18fa0bf7b99bd Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:39:05 +0000 Subject: [PATCH 2/2] refactor(@angular/build): use the `toPosixPath` util to convert windows file Use the shared util instead of duplicated the code. --- .../build/src/builders/application/build-action.ts | 3 ++- .../angular/build/src/builders/karma/find-tests.ts | 9 ++++----- .../src/tools/esbuild/application-code-bundle.ts | 12 +++--------- .../angular/build/src/tools/esbuild/global-styles.ts | 3 ++- .../build/src/tools/sass/rebasing-importer.ts | 3 ++- .../build/src/utils/server-rendering/prerender.ts | 3 ++- packages/angular/build/src/utils/service-worker.ts | 7 ++++--- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index c59863f0ebf5..afc59785be7d 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -16,6 +16,7 @@ import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/ut import { ChangedFiles } from '../../tools/esbuild/watcher'; import { shouldWatchRoot } from '../../utils/environment-options'; import { NormalizedCachedOptions } from '../../utils/normalize-cache'; +import { toPosixPath } from '../../utils/path'; import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options'; import { ComponentUpdateResult, @@ -105,7 +106,7 @@ export async function* runEsBuildBuildAction( // Ignore the output and cache paths to avoid infinite rebuild cycles outputOptions.base, cacheOptions.basePath, - `${workspaceRoot.replace(/\\/g, '/')}/**/.*/**`, + `${toPosixPath(workspaceRoot)}/**/.*/**`, ]; // Setup a watcher diff --git a/packages/angular/build/src/builders/karma/find-tests.ts b/packages/angular/build/src/builders/karma/find-tests.ts index ec25d56cf9d2..67ae410c6125 100644 --- a/packages/angular/build/src/builders/karma/find-tests.ts +++ b/packages/angular/build/src/builders/karma/find-tests.ts @@ -9,6 +9,7 @@ import { PathLike, constants, promises as fs } from 'node:fs'; import { basename, dirname, extname, join, relative } from 'node:path'; import { glob, isDynamicPattern } from 'tinyglobby'; +import { toPosixPath } from '../../utils/path'; /* Go through all patterns and find unique list of files */ export async function findTests( @@ -59,8 +60,6 @@ export function getTestEntrypoints( ); } -const normalizePath = (path: string): string => path.replace(/\\/g, '/'); - const removeLeadingSlash = (pattern: string): string => { if (pattern.charAt(0) === '/') { return pattern.substring(1); @@ -94,10 +93,10 @@ async function findMatchingTests( projectSourceRoot: string, ): Promise { // normalize pattern, glob lib only accepts forward slashes - let normalizedPattern = normalizePath(pattern); + let normalizedPattern = toPosixPath(pattern); normalizedPattern = removeLeadingSlash(normalizedPattern); - const relativeProjectRoot = normalizePath(relative(workspaceRoot, projectSourceRoot) + '/'); + const relativeProjectRoot = toPosixPath(relative(workspaceRoot, projectSourceRoot) + '/'); // remove relativeProjectRoot to support relative paths from root // such paths are easy to get when running scripts via IDEs @@ -125,7 +124,7 @@ async function findMatchingTests( // normalize the patterns in the ignore list const normalizedIgnorePatternList = ignore.map((pattern: string) => - removeRelativeRoot(removeLeadingSlash(normalizePath(pattern)), relativeProjectRoot), + removeRelativeRoot(removeLeadingSlash(toPosixPath(pattern)), relativeProjectRoot), ); return glob(normalizedPattern, { diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index c5d18d67228d..b17029f6c5e1 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -13,6 +13,7 @@ import { extname, relative } from 'node:path'; import type { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { ExperimentalPlatform } from '../../builders/application/schema'; import { allowMangle } from '../../utils/environment-options'; +import { toPosixPath } from '../../utils/path'; import { SERVER_APP_ENGINE_MANIFEST_FILENAME, SERVER_APP_MANIFEST_FILENAME, @@ -719,9 +720,7 @@ function getEsBuildCommonPolyfillsOptions( } // Generate module contents with an import statement per defined polyfill - let contents = polyfillPaths - .map((file) => `import '${file.replace(/\\/g, '/')}';`) - .join('\n'); + let contents = polyfillPaths.map((file) => `import '${toPosixPath(file)}';`).join('\n'); // The below should be done after loading `$localize` as otherwise the locale will be overridden. if (i18nOptions.shouldInline) { @@ -746,10 +745,5 @@ function getEsBuildCommonPolyfillsOptions( } function entryFileToWorkspaceRelative(workspaceRoot: string, entryFile: string): string { - return ( - './' + - relative(workspaceRoot, entryFile) - .replace(/.[mc]?ts$/, '') - .replace(/\\/g, '/') - ); + return './' + toPosixPath(relative(workspaceRoot, entryFile).replace(/.[mc]?ts$/, '')); } diff --git a/packages/angular/build/src/tools/esbuild/global-styles.ts b/packages/angular/build/src/tools/esbuild/global-styles.ts index 682885c43350..fd2cb13fa7b2 100644 --- a/packages/angular/build/src/tools/esbuild/global-styles.ts +++ b/packages/angular/build/src/tools/esbuild/global-styles.ts @@ -8,6 +8,7 @@ import assert from 'node:assert'; import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; +import { toPosixPath } from '../../utils/path'; import { BundlerOptionsFactory } from './bundler-context'; import { createStylesheetBundleOptions } from './stylesheets/bundle-options'; import { createVirtualModulePlugin } from './virtual-module-plugin'; @@ -91,7 +92,7 @@ export function createGlobalStylesBundleOptions( assert(files, `global style name should always be found [${args.path}]`); return { - contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'), + contents: files.map((file) => `@import '${toPosixPath(file)}';`).join('\n'), loader: 'css', resolveDir: workspaceRoot, }; diff --git a/packages/angular/build/src/tools/sass/rebasing-importer.ts b/packages/angular/build/src/tools/sass/rebasing-importer.ts index d5ade8b6cf54..15c94a25aeef 100644 --- a/packages/angular/build/src/tools/sass/rebasing-importer.ts +++ b/packages/angular/build/src/tools/sass/rebasing-importer.ts @@ -13,6 +13,7 @@ import { basename, dirname, extname, join, relative } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { CanonicalizeContext, Importer, ImporterResult, Syntax } from 'sass'; import { assertIsError } from '../../utils/error'; +import { toPosixPath } from '../../utils/path'; import { findUrls } from './lexer'; /** @@ -83,7 +84,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> { // Normalize path separators and escape characters // https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax - const rebasedUrl = rebasedPath.replace(/\\/g, '/').replace(/[()\s'"]/g, '\\$&'); + const rebasedUrl = toPosixPath(rebasedPath).replace(/[()\s'"]/g, '\\$&'); updatedContents ??= new MagicString(contents); // Always quote the URL to avoid potential downstream parsing problems diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index a8a42c7c941a..e087262a7f0c 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -13,6 +13,7 @@ import { OutputMode } from '../../builders/application/schema'; import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result'; import { assertIsError } from '../error'; +import { toPosixPath } from '../path'; import { urlJoin } from '../url'; import { WorkerPool } from '../worker-pool'; import { IMPORT_EXEC_ARGV } from './esm-in-memory-loader/utils'; @@ -94,7 +95,7 @@ export async function prerenderPages( const assetsReversed: Record = {}; for (const { source, destination } of assets) { - assetsReversed[addLeadingSlash(destination.replace(/\\/g, posix.sep))] = source; + assetsReversed[addLeadingSlash(toPosixPath(destination))] = source; } // Get routes to prerender diff --git a/packages/angular/build/src/utils/service-worker.ts b/packages/angular/build/src/utils/service-worker.ts index f9d5c14d27fd..c6f95f99a595 100644 --- a/packages/angular/build/src/utils/service-worker.ts +++ b/packages/angular/build/src/utils/service-worker.ts @@ -14,6 +14,7 @@ import { BuildOutputFile, BuildOutputFileType } from '../tools/esbuild/bundler-c import { BuildOutputAsset } from '../tools/esbuild/bundler-execution-result'; import { assertIsError } from './error'; import { loadEsmModule } from './load-esm'; +import { toPosixPath } from './path'; class CliFilesystem implements Filesystem { constructor( @@ -52,7 +53,7 @@ class CliFilesystem implements Filesystem { if (stats.isFile()) { // Uses posix paths since the service worker expects URLs - items.push('/' + path.relative(this.base, entryPath).replace(/\\/g, '/')); + items.push('/' + toPosixPath(path.relative(this.base, entryPath))); } else if (stats.isDirectory()) { subdirectories.push(entryPath); } @@ -75,11 +76,11 @@ class ResultFilesystem implements Filesystem { ) { for (const file of outputFiles) { if (file.type === BuildOutputFileType.Media || file.type === BuildOutputFileType.Browser) { - this.fileReaders.set('/' + file.path.replace(/\\/g, '/'), async () => file.contents); + this.fileReaders.set('/' + toPosixPath(file.path), async () => file.contents); } } for (const file of assetFiles) { - this.fileReaders.set('/' + file.destination.replace(/\\/g, '/'), () => + this.fileReaders.set('/' + toPosixPath(file.destination), () => fsPromises.readFile(file.source), ); }