From 6787a39588a1dc0dd61118ef60eade52374a5ac7 Mon Sep 17 00:00:00 2001 From: JasonKleban Date: Fri, 23 Apr 2021 13:30:22 -0400 Subject: [PATCH] Backport #1287 to v8 (#1291) * Backport "Use caches for module resolution and type reference directives when using compiler default functions" #1287 to v8 * regen comparison tests * Revert "regen comparison tests" This reverts commit f04c5cf001817bf60de0dd989f9ab929efdf77ea. * only the two failing tests rerun. * yarn run comparison-tests -- --save-output --single-test projectReferencesSymLinks * yarn run comparison-tests -- --single-test projectReferencesSymLinks_WatchApi --save-output Co-authored-by: Sheetal Nandi Co-authored-by: John Reilly --- CHANGELOG.md | 5 + package.json | 2 +- src/interfaces.ts | 48 ++++- src/servicesHost.ts | 190 ++++++++++++++---- src/watch-run.ts | 3 + .../instance/expectedOutput-4.1/output.txt | 16 +- .../expectedOutput-transpile-4.1/output.txt | 14 +- .../expectedOutput-4.1/patch0/output.txt | 2 +- .../expectedOutput-4.1/patch0/output.txt | 2 +- 9 files changed, 219 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122e0dc5a..4eebfdc47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v8.2.0 + +* [Use caches for module resolution and type reference directives when using compiler default functions](https://github.com/TypeStrong/ts-loader/pull/1287) - thanks @sheetalkamat - uses: https://github.com/microsoft/TypeScript/pull/43700 +* This is a backport from v9.1.0 for webpack 4 compatibility + ## v8.1.0 * [feat: remove top-level typescript import statements](https://github.com/TypeStrong/ts-loader/pull/1259) - thanks @ulivz diff --git a/package.json b/package.json index 55ad0e6e8..f4bdf0c9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-loader", - "version": "8.1.0", + "version": "8.2.0", "description": "TypeScript loader for webpack", "main": "index.js", "types": "dist", diff --git a/src/interfaces.ts b/src/interfaces.ts index f2bf5a4b5..6fec20479 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -102,8 +102,9 @@ export interface ServiceHostWhichMayBeCacheable export interface WatchHost extends typescript.WatchCompilerHostOfFilesAndCompilerOptions< - typescript.EmitAndSemanticDiagnosticsBuilderProgram - > { + typescript.EmitAndSemanticDiagnosticsBuilderProgram + >, + HostMayBeCacheable { invokeFileWatcher: WatchFactory['invokeFileWatcher']; updateRootFileNames(): void; outputFiles: Map; @@ -178,6 +179,47 @@ export interface ConfigFileInfo { dtsFiles?: string[]; } +interface CacheWithRedirects { + ownMap: Map; + redirectsMap: Map>; + getOrCreateMapOfCacheRedirects( + redirectedReference: typescript.ResolvedProjectReference | undefined + ): Map; + clear(): void; + setOwnOptions(newOptions: typescript.CompilerOptions): void; + setOwnMap(newOwnMap: Map): void; +} +interface PerModuleNameCache { + get( + directory: string + ): typescript.ResolvedModuleWithFailedLookupLocations | undefined; + set( + directory: string, + result: typescript.ResolvedModuleWithFailedLookupLocations + ): void; +} +export interface ModuleResolutionCache + extends typescript.ModuleResolutionCache { + directoryToModuleNameMap: CacheWithRedirects< + Map + >; + moduleNameToDirectoryMap: CacheWithRedirects; + clear(): void; + update(compilerOptions: typescript.CompilerOptions): void; + getPackageJsonInfoCache?(): any; +} +// Until the API has been released and ts-loader is built against a version of TypeScript that contains it - see https://github.com/microsoft/TypeScript/blob/74993a2a64bb2e423b40204bb54ff749cdd4ef54/src/compiler/moduleNameResolver.ts#L458 +export interface TypeReferenceDirectiveResolutionCache { + getOrCreateCacheForDirectory( + directoryName: string, + redirectedReference?: typescript.ResolvedProjectReference + ): Map< + string, + typescript.ResolvedTypeReferenceDirectiveWithFailedLookupLocations + >; + clear(): void; + update(compilerOptions: typescript.CompilerOptions): void; +} export interface TSInstance { compiler: typeof typescript; compilerOptions: typescript.CompilerOptions; @@ -185,6 +227,8 @@ export interface TSInstance { appendTsTsxSuffixesIfRequired: (filePath: string) => string; loaderOptions: LoaderOptions; rootFileNames: Set; + moduleResolutionCache?: ModuleResolutionCache; + typeReferenceResolutionCache?: TypeReferenceDirectiveResolutionCache; /** * a cache of all the files */ diff --git a/src/servicesHost.ts b/src/servicesHost.ts index d44dce8b7..3125bc7a0 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -10,6 +10,7 @@ import { CustomResolveModuleName, CustomResolveTypeReferenceDirective, FilePathKey, + ModuleResolutionCache, ModuleResolutionHostMayBeCacheable, ResolvedModule, ResolveSync, @@ -248,39 +249,19 @@ function makeResolvers( scriptRegex: RegExp, instance: TSInstance ) { - const resolveTypeReferenceDirective = makeResolveTypeReferenceDirective( - compiler, - compilerOptions, - moduleResolutionHost, - customResolveTypeReferenceDirective - ); - - const resolveTypeReferenceDirectives = ( - typeDirectiveNames: string[], - containingFile: string, - _redirectedReference?: typescript.ResolvedProjectReference - ): (typescript.ResolvedTypeReferenceDirective | undefined)[] => - typeDirectiveNames.map( - directive => - resolveTypeReferenceDirective( - directive, - containingFile, - _redirectedReference - ).resolvedTypeReferenceDirective - ); - const resolveModuleName = makeResolveModuleName( compiler, compilerOptions, moduleResolutionHost, - customResolveModuleName + customResolveModuleName, + instance ); const resolveModuleNames = ( moduleNames: string[], containingFile: string, _reusedNames?: string[] | undefined, - _redirectedReference?: typescript.ResolvedProjectReference | undefined + redirectedReference?: typescript.ResolvedProjectReference | undefined ): (typescript.ResolvedModule | undefined)[] => { const resolvedModules = moduleNames.map(moduleName => resolveModule( @@ -289,7 +270,8 @@ function makeResolvers( appendTsTsxSuffixesIfRequired, scriptRegex, moduleName, - containingFile + containingFile, + redirectedReference ) ); @@ -298,6 +280,28 @@ function makeResolvers( return resolvedModules; }; + const resolveTypeReferenceDirective = makeResolveTypeReferenceDirective( + compiler, + compilerOptions, + moduleResolutionHost, + customResolveTypeReferenceDirective, + instance + ); + + const resolveTypeReferenceDirectives = ( + typeDirectiveNames: string[], + containingFile: string, + redirectedReference?: typescript.ResolvedProjectReference + ): (typescript.ResolvedTypeReferenceDirective | undefined)[] => + typeDirectiveNames.map( + directive => + resolveTypeReferenceDirective( + directive, + containingFile, + redirectedReference + ).resolvedTypeReferenceDirective + ); + return { resolveTypeReferenceDirectives, resolveModuleNames, @@ -493,7 +497,7 @@ export function makeWatchHost( fileName => files.has(filePathKeyMapper(fileName)) || compiler.sys.fileExists(fileName), - /*enabledCaching*/ false + instance.loaderOptions.experimentalFileCaching ); const watchHost: WatchHost = { @@ -601,6 +605,60 @@ export function makeWatchHost( const missingFileModifiedTime = new Date(0); +function identity(x: T) { + return x; +} +function toLowerCase(x: string) { + return x.toLowerCase(); +} +const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g; +function toFileNameLowerCase(x: string) { + return fileNameLowerCaseRegExp.test(x) + ? x.replace(fileNameLowerCaseRegExp, toLowerCase) + : x; +} +function createGetCanonicalFileName(instance: TSInstance) { + return useCaseSensitiveFileNames(instance.compiler, instance.loaderOptions) + ? identity + : toFileNameLowerCase; +} + +function createModuleResolutionCache( + instance: TSInstance, + moduleResolutionHost: typescript.ModuleResolutionHost +): ModuleResolutionCache { + const cache = instance.compiler.createModuleResolutionCache( + moduleResolutionHost.getCurrentDirectory!(), + createGetCanonicalFileName(instance), + instance.compilerOptions + ) as ModuleResolutionCache; + // Add new API optional methods + if (!cache.clear) { + cache.clear = () => { + cache.directoryToModuleNameMap.clear(); + cache.moduleNameToDirectoryMap.clear(); + }; + } + if (!cache.update) { + cache.update = options => { + if (!options.configFile) return; + const ref: typescript.ResolvedProjectReference = { + sourceFile: options.configFile! as typescript.TsConfigSourceFile, + commandLine: { options } as typescript.ParsedCommandLine, + }; + cache.directoryToModuleNameMap.setOwnMap( + cache.directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref) + ); + cache.moduleNameToDirectoryMap.setOwnMap( + cache.moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref) + ); + cache.directoryToModuleNameMap.setOwnOptions(options); + cache.moduleNameToDirectoryMap.setOwnOptions(options); + }; + } + return cache; +} + /** * Create the TypeScript Watch host */ @@ -618,12 +676,7 @@ export function makeSolutionBuilderHost( // loader.context seems to work fine on Linux / Mac regardless causes problems for @types resolution on Windows for TypeScript < 2.3 const formatDiagnosticHost: typescript.FormatDiagnosticsHost = { getCurrentDirectory: compiler.sys.getCurrentDirectory, - getCanonicalFileName: useCaseSensitiveFileNames( - compiler, - instance.loaderOptions - ) - ? s => s - : s => s.toLowerCase(), + getCanonicalFileName: createGetCanonicalFileName(instance), getNewLine: () => compiler.sys.newLine, }; @@ -706,6 +759,28 @@ export function makeSolutionBuilderHost( const solutionBuilderHost: SolutionBuilderWithWatchHost = { ...sysHost, ...moduleResolutionHost, + createProgram: ( + rootNames, + options, + host, + oldProgram, + configFileParsingDiagnostics, + projectReferences + ) => { + instance.moduleResolutionCache?.update(options || {}); + instance.typeReferenceResolutionCache?.update(options || {}); + const result = sysHost.createProgram( + rootNames, + options, + host, + oldProgram, + configFileParsingDiagnostics, + projectReferences + ); + instance.typeReferenceResolutionCache?.update(instance.compilerOptions); + instance.moduleResolutionCache?.update(instance.compilerOptions); + return result; + }, resolveModuleNames, resolveTypeReferenceDirectives, diagnostics, @@ -1092,16 +1167,31 @@ function makeResolveTypeReferenceDirective( moduleResolutionHost: typescript.ModuleResolutionHost, customResolveTypeReferenceDirective: | CustomResolveTypeReferenceDirective - | undefined + | undefined, + instance: TSInstance ): ResolveTypeReferenceDirective { if (customResolveTypeReferenceDirective === undefined) { + // Until the api is published + if ( + (compiler as any).createTypeReferenceDirectiveResolutionCache && + !instance.typeReferenceResolutionCache + ) { + instance.typeReferenceResolutionCache = (compiler as any).createTypeReferenceDirectiveResolutionCache( + moduleResolutionHost.getCurrentDirectory!(), + createGetCanonicalFileName(instance), + instance.compilerOptions, + instance.moduleResolutionCache?.getPackageJsonInfoCache?.() + ); + } return (directive, containingFile, redirectedReference) => - compiler.resolveTypeReferenceDirective( + // Until the api is published + (compiler.resolveTypeReferenceDirective as any)( directive, containingFile, compilerOptions, moduleResolutionHost, - redirectedReference + redirectedReference, + instance.typeReferenceResolutionCache ); } @@ -1131,7 +1221,8 @@ function resolveModule( appendTsTsxSuffixesIfRequired: (filePath: string) => string, scriptRegex: RegExp, moduleName: string, - containingFile: string + containingFile: string, + redirectedReference: typescript.ResolvedProjectReference | undefined ) { let resolutionResult: ResolvedModule; @@ -1149,16 +1240,19 @@ function resolveModule( } } catch (e) {} - const tsResolution = resolveModuleName(moduleName, containingFile); + const tsResolution = resolveModuleName( + moduleName, + containingFile, + redirectedReference + ); if (tsResolution.resolvedModule !== undefined) { const resolvedFileName = path.normalize( tsResolution.resolvedModule.resolvedFileName ); const tsResolutionResult: ResolvedModule = { + ...tsResolution.resolvedModule, originalFileName: resolvedFileName, resolvedFileName, - isExternalLibraryImport: - tsResolution.resolvedModule.isExternalLibraryImport, }; return resolutionResult! === undefined || @@ -1173,26 +1267,36 @@ function resolveModule( type ResolveModuleName = ( moduleName: string, - containingFile: string + containingFile: string, + redirectedReference: typescript.ResolvedProjectReference | undefined ) => typescript.ResolvedModuleWithFailedLookupLocations; function makeResolveModuleName( compiler: typeof typescript, compilerOptions: typescript.CompilerOptions, moduleResolutionHost: typescript.ModuleResolutionHost, - customResolveModuleName: CustomResolveModuleName | undefined + customResolveModuleName: CustomResolveModuleName | undefined, + instance: TSInstance ): ResolveModuleName { if (customResolveModuleName === undefined) { - return (moduleName: string, containingFile: string) => + if (!instance.moduleResolutionCache) { + instance.moduleResolutionCache = createModuleResolutionCache( + instance, + moduleResolutionHost + ); + } + return (moduleName, containingFile, redirectedReference) => compiler.resolveModuleName( moduleName, containingFile, compilerOptions, - moduleResolutionHost + moduleResolutionHost, + instance.moduleResolutionCache, + redirectedReference ); } - return (moduleName: string, containingFile: string) => + return (moduleName, containingFile) => customResolveModuleName( moduleName, containingFile, diff --git a/src/watch-run.ts b/src/watch-run.ts index 69b9e0c6d..f117979f7 100644 --- a/src/watch-run.ts +++ b/src/watch-run.ts @@ -22,6 +22,9 @@ export function makeWatchRun( return (compiler: webpack.Compiler, callback: (err?: Error) => void) => { instance.servicesHost?.clearCache?.(); + instance.watchHost?.clearCache?.(); + instance.moduleResolutionCache?.clear(); + instance.typeReferenceResolutionCache?.clear(); const promises = []; if (instance.loaderOptions.transpileOnly) { instance.reportTranspileErrors = true; diff --git a/test/comparison-tests/instance/expectedOutput-4.1/output.txt b/test/comparison-tests/instance/expectedOutput-4.1/output.txt index 09690c0b5..1167dfee7 100644 --- a/test/comparison-tests/instance/expectedOutput-4.1/output.txt +++ b/test/comparison-tests/instance/expectedOutput-4.1/output.txt @@ -1,19 +1,19 @@ - Asset Size Chunks Chunk Names -bundle.js 4 KiB a [emitted] a + Asset Size Chunks Chunk Names +bundle.js 4.01 KiB a [emitted] a Entrypoint a = bundle.js Entrypoint b = -[./a.ts] 276 bytes {a} [built] [failed] [1 error] -[./b.ts] 276 bytes {b} [built] [failed] [1 error] +[./a.ts] 273 bytes {a} [built] [failed] [1 error] +[./b.ts] 273 bytes {b} [built] [failed] [1 error] ERROR in ./a.ts -Module build failed (from index.js): +Module build failed (from /index.js): Error: A file specified in tsconfig.json could not be found: i-dont-exist - at Object.loader (dist/index.js:18:18) + at Object.loader (dist\index.js:18:18) ERROR in ./b.ts -Module build failed (from index.js): +Module build failed (from /index.js): Error: A file specified in tsconfig.json could not be found: i-dont-exist - at Object.loader (dist/index.js:18:18) + at Object.loader (dist\index.js:18:18) ERROR in chunk b [entry] bundle.js diff --git a/test/comparison-tests/instance/expectedOutput-transpile-4.1/output.txt b/test/comparison-tests/instance/expectedOutput-transpile-4.1/output.txt index 1da55bdfe..41b0bbbb8 100644 --- a/test/comparison-tests/instance/expectedOutput-transpile-4.1/output.txt +++ b/test/comparison-tests/instance/expectedOutput-transpile-4.1/output.txt @@ -1,19 +1,19 @@ Asset Size Chunks Chunk Names -bundle.js 4.01 KiB a [emitted] a +bundle.js 4.02 KiB a [emitted] a Entrypoint a = bundle.js Entrypoint b = -[./a.ts] 286 bytes {a} [built] [failed] [1 error] -[./b.ts] 286 bytes {b} [built] [failed] [1 error] +[./a.ts] 283 bytes {a} [built] [failed] [1 error] +[./b.ts] 283 bytes {b} [built] [failed] [1 error] ERROR in ./a.ts -Module build failed (from index.js): +Module build failed (from /index.js): Error: A file specified in tsconfig.json could not be found: i-dont-exist - at Object.loader (dist/index.js:18:18) + at Object.loader (dist\index.js:18:18) ERROR in ./b.ts -Module build failed (from index.js): +Module build failed (from /index.js): Error: A file specified in tsconfig.json could not be found: i-dont-exist - at Object.loader (dist/index.js:18:18) + at Object.loader (dist\index.js:18:18) ERROR in chunk b [entry] bundle.js diff --git a/test/comparison-tests/projectReferencesSymLinks/expectedOutput-4.1/patch0/output.txt b/test/comparison-tests/projectReferencesSymLinks/expectedOutput-4.1/patch0/output.txt index 86ba1eac3..ab18dd44d 100644 --- a/test/comparison-tests/projectReferencesSymLinks/expectedOutput-4.1/patch0/output.txt +++ b/test/comparison-tests/projectReferencesSymLinks/expectedOutput-4.1/patch0/output.txt @@ -11,4 +11,4 @@ Entrypoint main = index.js ERROR in app/src/index.ts ./src/index.ts 1:9-25 [tsl] ERROR in app/src/index.ts(1,10) - TS2724: '"../../lib/dist"' has no exported member named 'getMeaningOfLife'. Did you mean 'getMeaningOfLife3'? \ No newline at end of file + TS2724: '"../../node_modules/lib/dist"' has no exported member named 'getMeaningOfLife'. Did you mean 'getMeaningOfLife3'? \ No newline at end of file diff --git a/test/comparison-tests/projectReferencesSymLinks_WatchApi/expectedOutput-4.1/patch0/output.txt b/test/comparison-tests/projectReferencesSymLinks_WatchApi/expectedOutput-4.1/patch0/output.txt index 86ba1eac3..ab18dd44d 100644 --- a/test/comparison-tests/projectReferencesSymLinks_WatchApi/expectedOutput-4.1/patch0/output.txt +++ b/test/comparison-tests/projectReferencesSymLinks_WatchApi/expectedOutput-4.1/patch0/output.txt @@ -11,4 +11,4 @@ Entrypoint main = index.js ERROR in app/src/index.ts ./src/index.ts 1:9-25 [tsl] ERROR in app/src/index.ts(1,10) - TS2724: '"../../lib/dist"' has no exported member named 'getMeaningOfLife'. Did you mean 'getMeaningOfLife3'? \ No newline at end of file + TS2724: '"../../node_modules/lib/dist"' has no exported member named 'getMeaningOfLife'. Did you mean 'getMeaningOfLife3'? \ No newline at end of file