diff --git a/packages/core/src/checkPackage.ts b/packages/core/src/checkPackage.ts index c615fa0..ebb331a 100644 --- a/packages/core/src/checkPackage.ts +++ b/packages/core/src/checkPackage.ts @@ -3,7 +3,7 @@ import { getEntrypointResolutionProblems } from "./checks/entrypointResolutionPr import { getFileProblems } from "./checks/fileProblems.js"; import { getResolutionBasedFileProblems } from "./checks/resolutionBasedFileProblems.js"; import type { Package } from "./createPackage.js"; -import { createMultiCompilerHost, type MultiCompilerHost } from "./multiCompilerHost.js"; +import { createCompilerHosts, type CompilerHosts, CompilerHostWrapper } from "./multiCompilerHost.js"; import type { CheckResult, EntrypointInfo, EntrypointResolutionAnalysis, Resolution, ResolutionKind } from "./types.js"; export async function checkPackage(pkg: Package): Promise { @@ -20,11 +20,11 @@ export async function checkPackage(pkg: Package): Promise { return { packageName, packageVersion, types }; } - const host = createMultiCompilerHost(pkg); - const entrypointResolutions = getEntrypointInfo(packageName, pkg, host); - const entrypointResolutionProblems = getEntrypointResolutionProblems(entrypointResolutions, host); - const resolutionBasedFileProblems = getResolutionBasedFileProblems(packageName, entrypointResolutions, host); - const fileProblems = getFileProblems(entrypointResolutions, host); + const hosts = createCompilerHosts(pkg); + const entrypointResolutions = getEntrypointInfo(packageName, pkg, hosts); + const entrypointResolutionProblems = getEntrypointResolutionProblems(entrypointResolutions, hosts); + const resolutionBasedFileProblems = getResolutionBasedFileProblems(packageName, entrypointResolutions, hosts); + const fileProblems = getFileProblems(entrypointResolutions, hosts); return { packageName, @@ -62,7 +62,7 @@ function getProxyDirectories(rootDir: string, fs: Package) { .filter((f) => f !== "./"); } -function getEntrypointInfo(packageName: string, fs: Package, host: MultiCompilerHost): Record { +function getEntrypointInfo(packageName: string, fs: Package, hosts: CompilerHosts): Record { const packageJson = JSON.parse(fs.readFile(`/node_modules/${packageName}/package.json`)); const subpaths = getSubpaths(packageJson.exports); const entrypoints = subpaths.length ? subpaths : ["."]; @@ -72,10 +72,10 @@ function getEntrypointInfo(packageName: string, fs: Package, host: MultiCompiler const result: Record = {}; for (const entrypoint of entrypoints) { const resolutions: Record = { - node10: getEntrypointResolution(packageName, "node10", entrypoint, host), - "node16-cjs": getEntrypointResolution(packageName, "node16-cjs", entrypoint, host), - "node16-esm": getEntrypointResolution(packageName, "node16-esm", entrypoint, host), - bundler: getEntrypointResolution(packageName, "bundler", entrypoint, host), + node10: getEntrypointResolution(packageName, hosts.node10, "node10", entrypoint), + "node16-cjs": getEntrypointResolution(packageName, hosts.node16, "node16-cjs", entrypoint), + "node16-esm": getEntrypointResolution(packageName, hosts.node16, "node16-esm", entrypoint), + bundler: getEntrypointResolution(packageName, hosts.bundler, "bundler", entrypoint), }; result[entrypoint] = { subpath: entrypoint, @@ -89,16 +89,15 @@ function getEntrypointInfo(packageName: string, fs: Package, host: MultiCompiler function getEntrypointResolution( packageName: string, + host: CompilerHostWrapper, resolutionKind: ResolutionKind, - entrypoint: string, - host: MultiCompilerHost + entrypoint: string ): EntrypointResolutionAnalysis { if (entrypoint.includes("*")) { return { name: entrypoint, resolutionKind, isWildcard: true }; } const moduleSpecifier = packageName + entrypoint.substring(1); // remove leading . before slash const importingFileName = resolutionKind === "node16-esm" ? "/index.mts" : "/index.ts"; - const moduleResolution = resolutionKind === "node10" ? "node10" : resolutionKind === "bundler" ? "bundler" : "node16"; const resolutionMode = resolutionKind === "node16-esm" ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS; const resolution = tryResolve(); @@ -107,7 +106,7 @@ function getEntrypointResolution( const files = resolution ? host - .createProgram(moduleResolution, [resolution.fileName]) + .createProgram([resolution.fileName]) .getSourceFiles() .map((f) => f.fileName) : undefined; @@ -124,7 +123,6 @@ function getEntrypointResolution( const { resolution, trace } = host.resolveModuleName( moduleSpecifier, importingFileName, - moduleResolution, resolutionMode, noDtsResolution ); @@ -135,7 +133,7 @@ function getEntrypointResolution( return { fileName, - moduleKind: host.getModuleKindForFile(fileName, moduleResolution), + moduleKind: host.getModuleKindForFile(fileName), isJson: resolution.resolvedModule.extension === ts.Extension.Json, isTypeScript: ts.hasTSFileExtension(resolution.resolvedModule.resolvedFileName), trace, diff --git a/packages/core/src/checks/entrypointResolutionProblems.ts b/packages/core/src/checks/entrypointResolutionProblems.ts index d0b2380..65387f1 100644 --- a/packages/core/src/checks/entrypointResolutionProblems.ts +++ b/packages/core/src/checks/entrypointResolutionProblems.ts @@ -1,11 +1,11 @@ import ts from "typescript"; import type { EntrypointInfo, EntrypointResolutionProblem } from "../types.js"; -import type { MultiCompilerHost } from "../multiCompilerHost.js"; +import type { CompilerHosts } from "../multiCompilerHost.js"; import { resolvedThroughFallback, visitResolutions } from "../utils.js"; export function getEntrypointResolutionProblems( entrypointResolutions: Record, - host: MultiCompilerHost + hosts: CompilerHosts ): EntrypointResolutionProblem[] { const problems: EntrypointResolutionProblem[] = []; visitResolutions(entrypointResolutions, (result, entrypoint) => { @@ -71,12 +71,12 @@ export function getEntrypointResolutionProblems( } if (resolutionKind === "node16-esm" && resolution && implementationResolution) { - const typesSourceFile = host.getSourceFile(resolution.fileName, "node16"); + const typesSourceFile = hosts.node16.getSourceFile(resolution.fileName); if (typesSourceFile) { ts.bindSourceFile(typesSourceFile, { target: ts.ScriptTarget.Latest, allowJs: true, checkJs: true }); } const typesExports = typesSourceFile?.symbol?.exports; - const jsSourceFile = typesExports && host.getSourceFile(implementationResolution.fileName, "node16"); + const jsSourceFile = typesExports && hosts.node16.getSourceFile(implementationResolution.fileName); if (jsSourceFile) { ts.bindSourceFile(jsSourceFile, { target: ts.ScriptTarget.Latest, allowJs: true, checkJs: true }); } diff --git a/packages/core/src/checks/fileProblems.ts b/packages/core/src/checks/fileProblems.ts index 7ba0747..193bd1a 100644 --- a/packages/core/src/checks/fileProblems.ts +++ b/packages/core/src/checks/fileProblems.ts @@ -1,11 +1,11 @@ import ts from "typescript"; -import type { MultiCompilerHost } from "../multiCompilerHost.js"; +import type { CompilerHosts } from "../multiCompilerHost.js"; import type { EntrypointInfo, FileProblem } from "../types.js"; import { isDefined } from "../utils.js"; export function getFileProblems( entrypointResolutions: Record, - host: MultiCompilerHost + hosts: CompilerHosts ): FileProblem[] { const problems: FileProblem[] = []; const visibleFiles = new Set( @@ -18,7 +18,7 @@ export function getFileProblems( for (const fileName of visibleFiles) { if (ts.hasJSFileExtension(fileName)) { - const sourceFile = host.getSourceFile(fileName, "node16")!; + const sourceFile = hosts.node16.getSourceFile(fileName)!; if ( !sourceFile.externalModuleIndicator && sourceFile.commonJsModuleIndicator && diff --git a/packages/core/src/checks/resolutionBasedFileProblems.ts b/packages/core/src/checks/resolutionBasedFileProblems.ts index 5357afc..cdc08ed 100644 --- a/packages/core/src/checks/resolutionBasedFileProblems.ts +++ b/packages/core/src/checks/resolutionBasedFileProblems.ts @@ -1,12 +1,12 @@ import ts from "typescript"; -import type { MultiCompilerHost } from "../multiCompilerHost.js"; +import type { CompilerHosts } from "../multiCompilerHost.js"; import type { EntrypointInfo, ResolutionBasedFileProblem } from "../types.js"; import { allResolutionOptions, getResolutionKinds } from "../utils.js"; export function getResolutionBasedFileProblems( packageName: string, entrypointResolutions: Record, - host: MultiCompilerHost + hosts: CompilerHosts ): ResolutionBasedFileProblem[] { const result: ResolutionBasedFileProblem[] = []; for (const resolutionOption of allResolutionOptions) { @@ -24,7 +24,8 @@ export function getResolutionBasedFileProblems( ); for (const fileName of visibleFiles) { - const sourceFile = host.getSourceFile(fileName, resolutionOption)!; + const host = hosts[resolutionOption]; + const sourceFile = host.getSourceFile(fileName)!; if (sourceFile.imports) { for (const moduleSpecifier of sourceFile.imports) { @@ -52,7 +53,7 @@ export function getResolutionBasedFileProblems( pos: moduleSpecifier.pos, end: moduleSpecifier.end, resolutionMode, - trace: host.getTrace(resolutionOption, fileName, moduleSpecifier.text, resolutionMode)!, + trace: host.getTrace(fileName, moduleSpecifier.text, resolutionMode)!, }); } } @@ -67,7 +68,7 @@ export function getResolutionBasedFileProblems( // for looking for a JS file. if (resolutionOption === "node16") { if (ts.hasJSFileExtension(fileName)) { - const expectedModuleKind = host.getModuleKindForFile(fileName, resolutionOption); + const expectedModuleKind = host.getModuleKindForFile(fileName); const syntaxImpliedModuleKind = sourceFile.externalModuleIndicator ? ts.ModuleKind.ESNext : sourceFile.commonJsModuleIndicator diff --git a/packages/core/src/multiCompilerHost.ts b/packages/core/src/multiCompilerHost.ts index 0ef4c55..fc33d2f 100644 --- a/packages/core/src/multiCompilerHost.ts +++ b/packages/core/src/multiCompilerHost.ts @@ -1,5 +1,5 @@ import ts from "typescript"; -import type { ModuleKind, ResolutionOption } from "./types.js"; +import type { ModuleKind } from "./types.js"; import type { Package } from "./createPackage.js"; export interface ResolveModuleNameResult { @@ -7,137 +7,57 @@ export interface ResolveModuleNameResult { trace: string[]; } -export interface MultiCompilerHost { - getSourceFile(fileName: string, moduleResolution?: ResolutionOption): ts.SourceFile | undefined; - getImpliedNodeFormatForFile( - fileName: string, - moduleResolution: ResolutionOption - ): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined; - getPackageScopeForPath(fileName: string): ts.PackageJsonInfo | undefined; - getModuleKindForFile(fileName: string, moduleResolution: "node16"): ModuleKind; - getModuleKindForFile(fileName: string, moduleResolution: ResolutionOption): ModuleKind | undefined; - resolveModuleName( - moduleName: string, - containingFile: string, - moduleResolution: ResolutionOption, - resolutionMode?: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS, - noDtsResolution?: boolean - ): ResolveModuleNameResult; - getTrace( - moduleResolution: ResolutionOption, - fromFileName: string, - moduleName: string, - resolutionMode: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined - ): string[] | undefined; - createProgram(moduleResolution: ResolutionOption, rootNames: string[]): ts.Program; +export interface CompilerHosts { + node10: CompilerHostWrapper; + node16: CompilerHostWrapper; + bundler: CompilerHostWrapper; } -export function createMultiCompilerHost(fs: Package): MultiCompilerHost { - const useCaseSensitiveFileNames = () => false; - const getCanonicalFileName = ts.createGetCanonicalFileName(false); - const getCurrentDirectory = () => "/"; - const getNewLine = () => "\n"; - const getDefaultLibFileName = () => "/node_modules/typescript/lib/lib.d.ts"; - const toPath = (fileName: string) => ts.toPath(fileName, "/", getCanonicalFileName); - const writeFile = () => { - throw new Error("Not implemented"); - }; - const languageVersion = ts.ScriptTarget.Latest; - const traceCollector = createTraceCollector(); - const compilerOptions: Record = { - node10: { - moduleResolution: ts.ModuleResolutionKind.NodeJs, - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.Latest, - resolveJsonModule: true, - traceResolution: true, - }, - node16: { - moduleResolution: ts.ModuleResolutionKind.Node16, - module: ts.ModuleKind.Node16, - target: ts.ScriptTarget.Latest, - resolveJsonModule: true, - traceResolution: true, - }, - bundler: { - moduleResolution: ts.ModuleResolutionKind.Bundler, - module: ts.ModuleKind.ESNext, - target: ts.ScriptTarget.Latest, - resolveJsonModule: true, - traceResolution: true, - }, - }; - const moduleResolutionCaches: Record< - ResolutionOption, - [normal: ts.ModuleResolutionCache, noDtsResolution: ts.ModuleResolutionCache] - > = { - node10: [ - ts.createModuleResolutionCache("/", getCanonicalFileName, compilerOptions.node10), - ts.createModuleResolutionCache("/", getCanonicalFileName, compilerOptions.node10), - ], - node16: [ - ts.createModuleResolutionCache("/", getCanonicalFileName, compilerOptions.node16), - ts.createModuleResolutionCache("/", getCanonicalFileName, compilerOptions.node16), - ], - bundler: [ - ts.createModuleResolutionCache("/", getCanonicalFileName, compilerOptions.bundler), - ts.createModuleResolutionCache("/", getCanonicalFileName, compilerOptions.bundler), - ], - }; - const compilerHosts: Record = { - node10: createCompilerHost("node10"), - node16: createCompilerHost("node16"), - bundler: createCompilerHost("bundler"), - }; - const traceCache: Record>> = { - node10: {}, - node16: {}, - bundler: {}, - }; - +export function createCompilerHosts(fs: Package): CompilerHosts { return { - getSourceFile, - getImpliedNodeFormatForFile, - getPackageScopeForPath, - getModuleKindForFile, - resolveModuleName, - createProgram, - getTrace, + node10: new CompilerHostWrapper(fs, ts.ModuleResolutionKind.Node10, ts.ModuleKind.CommonJS), + node16: new CompilerHostWrapper(fs, ts.ModuleResolutionKind.Node16, ts.ModuleKind.Node16), + bundler: new CompilerHostWrapper(fs, ts.ModuleResolutionKind.Bundler, ts.ModuleKind.ESNext), }; +} - function getSourceFile(fileName: string, moduleResolution: ResolutionOption = "bundler"): ts.SourceFile | undefined { - return compilerHosts[moduleResolution].getSourceFile(fileName, languageVersion); - } +const getCanonicalFileName = ts.createGetCanonicalFileName(false); +const toPath = (fileName: string) => ts.toPath(fileName, "/", getCanonicalFileName); - function getImpliedNodeFormatForFile( - fileName: string, - moduleResolution: ResolutionOption - ): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined { - return ts.getImpliedNodeFormatForFile( - toPath(fileName), - moduleResolutionCaches[moduleResolution][0].getPackageJsonInfoCache(), - compilerHosts[moduleResolution], - compilerOptions[moduleResolution] +export class CompilerHostWrapper { + private compilerHost: ts.CompilerHost; + private compilerOptions: ts.CompilerOptions; + private normalModuleResolutionCache: ts.ModuleResolutionCache; + private noDtsResolutionModuleResolutionCache: ts.ModuleResolutionCache; + + private traceCache: Record> = {}; + private traceCollector: TraceCollector = new TraceCollector(); + + private languageVersion = ts.ScriptTarget.Latest; + + constructor(fs: Package, moduleResolution: ts.ModuleResolutionKind, moduleKind: ts.ModuleKind) { + this.compilerOptions = { + moduleResolution, + module: moduleKind, + target: ts.ScriptTarget.Latest, + resolveJsonModule: true, + traceResolution: true, + }; + this.normalModuleResolutionCache = ts.createModuleResolutionCache("/", getCanonicalFileName, this.compilerOptions); + this.noDtsResolutionModuleResolutionCache = ts.createModuleResolutionCache( + "/", + getCanonicalFileName, + this.compilerOptions ); + this.compilerHost = this.createCompilerHost(fs); } - function getPackageScopeForPath(fileName: string): ts.PackageJsonInfo | undefined { - // Which compiler options get used here is irrelevant. - // Use the node16 cache because package.json it should be a hit. - return ts.getPackageScopeForPath( - fileName, - ts.getTemporaryModuleResolutionState( - moduleResolutionCaches.node16[0].getPackageJsonInfoCache(), - compilerHosts.node16, - compilerOptions.node16 - ) - ); + getSourceFile(fileName: string): ts.SourceFile | undefined { + return this.compilerHost.getSourceFile(fileName, this.languageVersion); } - function getModuleKindForFile(fileName: string, moduleResolution: "node16"): ModuleKind; - function getModuleKindForFile(fileName: string, moduleResolution: ResolutionOption): ModuleKind | undefined; - function getModuleKindForFile(fileName: string, moduleResolution: ResolutionOption): ModuleKind | undefined { - const kind = getImpliedNodeFormatForFile(fileName, moduleResolution); + getModuleKindForFile(fileName: string): ModuleKind | undefined { + const kind = this.getImpliedNodeFormatForFile(fileName); if (kind) { const extension = ts.getAnyExtensionFromPath(fileName); const isExtension = @@ -147,7 +67,7 @@ export function createMultiCompilerHost(fs: Package): MultiCompilerHost { extension === ts.Extension.Mjs || extension === ts.Extension.Mts || extension === ts.Extension.Dmts; - const reasonPackageJsonInfo = isExtension ? undefined : getPackageScopeForPath(fileName); + const reasonPackageJsonInfo = isExtension ? undefined : this.getPackageScopeForPath(fileName); const reasonFileName = isExtension ? fileName : reasonPackageJsonInfo @@ -162,28 +82,26 @@ export function createMultiCompilerHost(fs: Package): MultiCompilerHost { } } - function resolveModuleName( + resolveModuleName( moduleName: string, containingFile: string, - moduleResolution: ResolutionOption, resolutionMode?: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS, noDtsResolution?: boolean ): ResolveModuleNameResult { - traceCollector.clear(); - const options = compilerOptions[moduleResolution]; + this.traceCollector.clear(); const resolution = ts.resolveModuleName( moduleName, containingFile, - noDtsResolution ? { ...options, noDtsResolution } : options, - compilerHosts[moduleResolution], - moduleResolutionCaches[moduleResolution][+!!noDtsResolution], + noDtsResolution ? { ...this.compilerOptions, noDtsResolution } : this.compilerOptions, + this.compilerHost, + noDtsResolution ? this.noDtsResolutionModuleResolutionCache : this.normalModuleResolutionCache, /*redirectedReference*/ undefined, resolutionMode ); - const trace = traceCollector.read(); + const trace = this.traceCollector.read(); const moduleKey = `${resolutionMode ?? 1}:${moduleName}`; - if (!traceCache[moduleResolution][containingFile]?.[moduleKey]) { - (traceCache[moduleResolution][containingFile] ??= {})[moduleKey] = trace; + if (!this.traceCache[containingFile]?.[moduleKey]) { + (this.traceCache[containingFile] ??= {})[moduleKey] = trace; } return { resolution, @@ -191,24 +109,23 @@ export function createMultiCompilerHost(fs: Package): MultiCompilerHost { }; } - function getTrace( - moduleResolution: ResolutionOption, + getTrace( fromFileName: string, moduleSpecifier: string, - resolutionMode: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS + resolutionMode: ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined ): string[] | undefined { - return traceCache[moduleResolution][fromFileName]?.[`${resolutionMode ?? 1}:${moduleSpecifier}`]; + return this.traceCache[fromFileName]?.[`${resolutionMode ?? 1}:${moduleSpecifier}`]; } - function createProgram(moduleResolution: ResolutionOption, rootNames: string[]): ts.Program { + createProgram(rootNames: string[]): ts.Program { return ts.createProgram({ rootNames, - options: compilerOptions[moduleResolution], - host: compilerHosts[moduleResolution], + options: this.compilerOptions, + host: this.compilerHost, }); } - function createCompilerHost(moduleResolution: ResolutionOption): ts.CompilerHost { + private createCompilerHost(fs: Package): ts.CompilerHost { const sourceFileCache = new Map(); return { ...fs, @@ -223,28 +140,35 @@ export function createMultiCompilerHost(fs: Package): MultiCompilerHost { fileName, content, { - languageVersion, - impliedNodeFormat: getImpliedNodeFormatForFile(fileName, moduleResolution), + languageVersion: this.languageVersion, + impliedNodeFormat: this.getImpliedNodeFormatForFile(fileName), }, /*setParentNodes*/ true ); sourceFileCache.set(path, sourceFile); return sourceFile; }, - getDefaultLibFileName, - getCurrentDirectory, - writeFile, + getDefaultLibFileName: () => "/node_modules/typescript/lib/lib.d.ts", + getCurrentDirectory: () => "/", + writeFile: () => { + throw new Error("Not implemented"); + }, getCanonicalFileName, - useCaseSensitiveFileNames, - getNewLine, - trace: traceCollector.trace, - resolveModuleNameLiterals(moduleLiterals, containingFile, _redirectedReference, options, containingSourceFile) { + useCaseSensitiveFileNames: () => false, + getNewLine: () => "\n", + trace: this.traceCollector.trace, + resolveModuleNameLiterals: ( + moduleLiterals, + containingFile, + _redirectedReference, + options, + containingSourceFile + ) => { return moduleLiterals.map( (literal) => - resolveModuleName( + this.resolveModuleName( literal.text, containingFile, - moduleResolution, ts.getModeForUsageLocation(containingSourceFile, literal), options.noDtsResolution ).resolution @@ -253,19 +177,40 @@ export function createMultiCompilerHost(fs: Package): MultiCompilerHost { }; } - function createTraceCollector() { - const traces: string[] = []; - return { - trace: (message: string) => traces.push(message), - read: () => { - const result = traces.slice(); - clear(); - return result; - }, - clear, - }; - function clear() { - traces.length = 0; - } + private getImpliedNodeFormatForFile(fileName: string): ts.ModuleKind.ESNext | ts.ModuleKind.CommonJS | undefined { + return ts.getImpliedNodeFormatForFile( + toPath(fileName), + this.normalModuleResolutionCache.getPackageJsonInfoCache(), + this.compilerHost, + this.compilerOptions + ); + } + + private getPackageScopeForPath(fileName: string): ts.PackageJsonInfo | undefined { + return ts.getPackageScopeForPath( + fileName, + ts.getTemporaryModuleResolutionState( + // TODO: consider always using the node16 cache because package.json should be a hit + this.normalModuleResolutionCache.getPackageJsonInfoCache(), + this.compilerHost, + this.compilerOptions + ) + ); + } +} + +class TraceCollector { + private traces: string[] = []; + + trace = (message: string) => { + this.traces.push(message); + }; + read() { + const result = this.traces.slice(); + this.clear(); + return result; + } + clear() { + this.traces.length = 0; } }