diff --git a/packages/ngtools/webpack/src/ivy/host.ts b/packages/ngtools/webpack/src/ivy/host.ts index 85367d80af..01f8aa5414 100644 --- a/packages/ngtools/webpack/src/ivy/host.ts +++ b/packages/ngtools/webpack/src/ivy/host.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import { NgccProcessor } from '../ngcc_processor'; import { WebpackResourceLoader } from '../resource_loader'; -import { forwardSlashPath } from '../utils'; +import { normalizePath } from './paths'; export function augmentHostWithResources( host: ts.CompilerHost, @@ -21,7 +21,7 @@ export function augmentHostWithResources( const resourceHost = host as CompilerHost; resourceHost.readResource = function (fileName: string) { - const filePath = forwardSlashPath(fileName); + const filePath = normalizePath(fileName); if ( options.directTemplateLoading && @@ -41,7 +41,7 @@ export function augmentHostWithResources( }; resourceHost.resourceNameToFileName = function (resourceName: string, containingFile: string) { - return forwardSlashPath(path.join(path.dirname(containingFile), resourceName)); + return path.join(path.dirname(containingFile), resourceName); }; resourceHost.getModifiedResourceFiles = function () { @@ -157,7 +157,7 @@ export function augmentHostWithReplacements( const normalizedReplacements: Record = {}; for (const [key, value] of Object.entries(replacements)) { - normalizedReplacements[forwardSlashPath(key)] = forwardSlashPath(value); + normalizedReplacements[normalizePath(key)] = normalizePath(value); } const tryReplace = (resolvedModule: ts.ResolvedModule | undefined) => { diff --git a/packages/ngtools/webpack/src/ivy/paths.ts b/packages/ngtools/webpack/src/ivy/paths.ts new file mode 100644 index 0000000000..ff3b9e1b28 --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/paths.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google Inc. 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.io/license + */ +import * as nodePath from 'path'; + +const normalizationCache = new Map(); + +export function normalizePath(path: string): string { + let result = normalizationCache.get(path); + + if (result === undefined) { + result = nodePath.win32.normalize(path).replace(/\\/g, nodePath.posix.sep); + normalizationCache.set(path, result); + } + + return result; +} + +const externalizationCache = new Map(); + +function externalizeForWindows(path: string): string { + let result = externalizationCache.get(path); + + if (result === undefined) { + result = nodePath.win32.normalize(path); + externalizationCache.set(path, result); + } + + return result; +} + +export const externalizePath = (() => { + if (process.platform !== 'win32') { + return (path: string) => path; + } + + return externalizeForWindows; +})(); diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index cbb382eb60..7216bfe5df 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -18,7 +18,6 @@ import { import { NgccProcessor } from '../ngcc_processor'; import { TypeScriptPathsPlugin } from '../paths-plugin'; import { WebpackResourceLoader } from '../resource_loader'; -import { forwardSlashPath } from '../utils'; import { addError, addWarning } from '../webpack-diagnostics'; import { isWebpackFiveOrHigher, mergeResolverMainFields } from '../webpack-version'; import { DiagnosticsReporter, createDiagnosticsReporter } from './diagnostics'; @@ -30,6 +29,7 @@ import { augmentHostWithSubstitutions, augmentProgramWithVersioning, } from './host'; +import { externalizePath, normalizePath } from './paths'; import { AngularPluginSymbol, FileEmitter } from './symbol'; import { createWebpackSystem } from './system'; import { createAotTransformers, createJitTransformers, mergeTransformers } from './transformation'; @@ -180,7 +180,7 @@ export class AngularWebpackPlugin { // Create a Webpack-based TypeScript compiler host const system = createWebpackSystem( compiler.inputFileSystem, - forwardSlashPath(compiler.context), + normalizePath(compiler.context), ); const host = ts.createIncrementalCompilerHost(compilerOptions, system); @@ -190,7 +190,8 @@ export class AngularWebpackPlugin { // Invalidate existing cache based on compilation file timestamps for (const [file, time] of compilation.fileTimestamps) { if (this.buildTimestamp < time) { - cache.delete(forwardSlashPath(file)); + // Cache stores paths using the POSIX directory separator + cache.delete(normalizePath(file)); } } } else { @@ -254,7 +255,7 @@ export class AngularWebpackPlugin { const rebuild = (filename: string) => new Promise((resolve) => { const module = modules.find( ({ resource }: compilation.Module & { resource?: string }) => - resource && forwardSlashPath(resource) === filename, + resource && normalizePath(resource) === filename, ); if (!module) { resolve(); @@ -279,7 +280,7 @@ export class AngularWebpackPlugin { .map((sourceFile) => sourceFile.fileName), ); modules.forEach(({ resource }: compilation.Module & { resource?: string }) => { - const sourceFile = resource && builder.getSourceFile(forwardSlashPath(resource)); + const sourceFile = resource && builder.getSourceFile(resource); if (!sourceFile) { return; } @@ -408,8 +409,7 @@ export class AngularWebpackPlugin { const getDependencies = (sourceFile: ts.SourceFile) => { const dependencies = []; - for (const resourceDependency of angularCompiler.getResourceDependencies(sourceFile)) { - const resourcePath = forwardSlashPath(resourceDependency); + for (const resourcePath of angularCompiler.getResourceDependencies(sourceFile)) { dependencies.push( resourcePath, // Retrieve all dependencies of the resource (stylesheet imports, etc.) @@ -444,8 +444,7 @@ export class AngularWebpackPlugin { // NOTE: This can be removed once support for the deprecated lazy route string format is removed for (const lazyRoute of angularCompiler.listLazyRoutes()) { const [routeKey] = lazyRoute.route.split('#'); - const routePath = forwardSlashPath(lazyRoute.referencedModule.filePath); - this.lazyRouteMap[routeKey] = routePath; + this.lazyRouteMap[routeKey] = lazyRoute.referencedModule.filePath; } return this.createFileEmitter( @@ -513,8 +512,7 @@ export class AngularWebpackPlugin { const pendingAnalysis = angularCompiler.analyzeAsync().then(() => { for (const lazyRoute of angularCompiler.listLazyRoutes()) { const [routeKey] = lazyRoute.route.split('#'); - const routePath = forwardSlashPath(lazyRoute.referencedModule.filePath); - this.lazyRouteMap[routeKey] = routePath; + this.lazyRouteMap[routeKey] = lazyRoute.referencedModule.filePath; } return this.createFileEmitter(builder, transformers, () => []); diff --git a/packages/ngtools/webpack/src/ivy/system.ts b/packages/ngtools/webpack/src/ivy/system.ts index ac51e8d546..bb1f65dc22 100644 --- a/packages/ngtools/webpack/src/ivy/system.ts +++ b/packages/ngtools/webpack/src/ivy/system.ts @@ -7,45 +7,25 @@ */ import * as ts from 'typescript'; import { InputFileSystem } from 'webpack'; +import { externalizePath } from './paths'; function shouldNotWrite(): never { throw new Error('Webpack TypeScript System should not write.'); } -// Webpack's CachedInputFileSystem uses the default directory separator in the paths it uses -// for keys to its cache. If the keys do not match then the file watcher will not purge outdated -// files and cause stale data to be used in the next rebuild. TypeScript always uses a `/` (POSIX) -// directory separator internally which is also supported with Windows system APIs. However, -// if file operations are performed with the non-default directory separator, the Webpack cache -// will contain a key that will not be purged. -function createToSystemPath(): (path: string) => string { - if (process.platform === 'win32') { - const cache = new Map(); - - return (path) => { - let value = cache.get(path); - if (value === undefined) { - value = path.replace(/\//g, '\\'); - cache.set(path, value); - } - - return value; - }; - } - - // POSIX-like platforms retain the existing directory separator - return (path) => path; -} - export function createWebpackSystem(input: InputFileSystem, currentDirectory: string): ts.System { - const toSystemPath = createToSystemPath(); - + // Webpack's CachedInputFileSystem uses the default directory separator in the paths it uses + // for keys to its cache. If the keys do not match then the file watcher will not purge outdated + // files and cause stale data to be used in the next rebuild. TypeScript always uses a `/` (POSIX) + // directory separator internally which is also supported with Windows system APIs. However, + // if file operations are performed with the non-default directory separator, the Webpack cache + // will contain a key that will not be purged. `externalizePath` ensures the paths are as expected. const system: ts.System = { ...ts.sys, readFile(path: string) { let data; try { - data = input.readFileSync(toSystemPath(path)); + data = input.readFileSync(externalizePath(path)); } catch { return undefined; } @@ -60,28 +40,28 @@ export function createWebpackSystem(input: InputFileSystem, currentDirectory: st }, getFileSize(path: string) { try { - return input.statSync(toSystemPath(path)).size; + return input.statSync(externalizePath(path)).size; } catch { return 0; } }, fileExists(path: string) { try { - return input.statSync(toSystemPath(path)).isFile(); + return input.statSync(externalizePath(path)).isFile(); } catch { return false; } }, directoryExists(path: string) { try { - return input.statSync(toSystemPath(path)).isDirectory(); + return input.statSync(externalizePath(path)).isDirectory(); } catch { return false; } }, getModifiedTime(path: string) { try { - return input.statSync(toSystemPath(path)).mtime; + return input.statSync(externalizePath(path)).mtime; } catch { return undefined; } diff --git a/packages/ngtools/webpack/src/resource_loader.ts b/packages/ngtools/webpack/src/resource_loader.ts index aa5854ddcc..f3175c4b1d 100644 --- a/packages/ngtools/webpack/src/resource_loader.ts +++ b/packages/ngtools/webpack/src/resource_loader.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import * as vm from 'vm'; import { RawSource } from 'webpack-sources'; -import { forwardSlashPath } from './utils'; +import { normalizePath } from './ivy/paths'; const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); @@ -44,7 +44,7 @@ export class WebpackResourceLoader { this.changedFiles.clear(); for (const [file, time] of parentCompilation.fileTimestamps) { if (this.buildTimestamp < time) { - this.changedFiles.add(forwardSlashPath(file)); + this.changedFiles.add(normalizePath(file)); } } } @@ -159,7 +159,7 @@ export class WebpackResourceLoader { // Save the dependencies for this resource. this._fileDependencies.set(filePath, new Set(childCompilation.fileDependencies)); for (const file of childCompilation.fileDependencies) { - const resolvedFile = forwardSlashPath(file); + const resolvedFile = normalizePath(file); const entry = this._reverseDependencies.get(resolvedFile); if (entry) { entry.add(filePath);