diff --git a/src/classify-module.ts b/src/classify-module.ts index 291a647..114b420 100644 --- a/src/classify-module.ts +++ b/src/classify-module.ts @@ -21,10 +21,13 @@ const readPJType = cachedMtime( }) ) -export const classifyModule = (fileName: string): PackageJsonType | 'json' => { +export const classifyModule = ( + fileName: string +): PackageJsonType | 'json' => { if (fileName.endsWith('.json')) { return 'json' - } if (fileName.endsWith('.cts') || fileName.endsWith('.cjs')) { + } + if (fileName.endsWith('.cts') || fileName.endsWith('.cjs')) { return 'commonjs' } else if (fileName.endsWith('.mts') || fileName.endsWith('.mjs')) { return 'module' diff --git a/src/service/get-output-typecheck.ts b/src/service/get-output-typecheck.ts index c71bd8d..e5b6948 100644 --- a/src/service/get-output-typecheck.ts +++ b/src/service/get-output-typecheck.ts @@ -1,8 +1,9 @@ // Get output with full type-checking from the LanguageService. import { relative } from 'path' -import type ts from 'typescript' +import ts from 'typescript' import { info, warn } from '../debug.js' +import { readFile } from '../ts-sys-cached.js' import { addRootFile, updateFileVersion } from './file-versions.js' import { getLanguageService } from './language-service.js' import { markFileNameInternal } from './resolve-module-name-literals.js' @@ -27,6 +28,30 @@ export const getOutputTypeCheck = ( markFileNameInternal(fileName) updateFileVersion(fileName, code) + // TODO: It's not clear which fields on dependenciesInfo are relevant + // TODO: Ensure that preProcessFile looks for dependencies recursively + // TODO: Skip the step of putting the fileNames into a Set if it's + // guaranteed that a given file won't be listed multiple times + const { + referencedFiles, + typeReferenceDirectives, + libReferenceDirectives, + importedFiles, + } = ts.preProcessFile(code) + const dependencyFileNames = new Set() + for (const file of [ + ...referencedFiles, + ...typeReferenceDirectives, + ...libReferenceDirectives, + ...importedFiles, + ]) { + dependencyFileNames.add(file.fileName) + } + + for (const dependencyFileName of dependencyFileNames) { + updateFileVersion(dependencyFileName, readFile(fileName) || '') + } + // if we can't get the source file, then return the code un-compiled. // Eg, loading a JS file if allowJs is not set. const programBefore = service.getProgram() diff --git a/src/service/load.ts b/src/service/load.ts index c5c13d2..f5d6f0b 100644 --- a/src/service/load.ts +++ b/src/service/load.ts @@ -3,7 +3,7 @@ import { cachedMtime } from '@isaacs/cached' import { mkdirSync, writeFileSync } from 'fs' import { relative, resolve } from 'path' -import { ParsedCommandLine } from 'typescript' +import { Diagnostic, ParsedCommandLine } from 'typescript' import { info } from '../debug.js' import { getOutputFile } from '../get-output-file.js' import { @@ -31,30 +31,40 @@ export const load = ( const config = tsconfig() if (lastConfig && config !== lastConfig) { - compileTypeCheck.cache.clear() compileTranspileOnly.cache.clear() - compileTypeCheck.mtimeCache.clear() compileTranspileOnly.mtimeCache.clear() } lastConfig = config - // compile to a file on disk, but only if the source has changed. - const compile = typeCheck ? compileTypeCheck : compileTranspileOnly - const cachedMtime = compile.mtimeCache.get(fileName)?.[0] - const newMtime = compile.getMtime(fileName) + let compile: (fileName: string) => { + outputText: string | undefined + diagnostics: Diagnostic[] + } const outFile = getOutputFile(fileName) - const cachedResult = compile.cache.get(fileName) - if ( - cachedMtime && - cachedMtime === newMtime && - fileExists(outFile) && - cachedResult - ) { - // saw this one, and have previous build available - return { - fileName: outFile, - diagnostics: reportAll(cachedResult.diagnostics, pretty), + // TODO: Re-enable caching of type-checked results + if (typeCheck) { + compile = compileTypeCheck + } else { + let cachedCompile = compileTranspileOnly + compile = cachedCompile + + // Skip compiling if the source has not changed + const cachedMtime = cachedCompile.mtimeCache.get(fileName)?.[0] + const newMtime = cachedCompile.getMtime(fileName) + const cachedResult = cachedCompile.cache.get(fileName) + + if ( + cachedMtime && + cachedMtime === newMtime && + fileExists(outFile) && + cachedResult + ) { + // saw this one, and have previous build available + return { + fileName: outFile, + diagnostics: reportAll(cachedResult.diagnostics, pretty), + } } } @@ -82,12 +92,19 @@ export const load = ( } } -const compileTypeCheck = cachedMtime((fileName: string) => { +// It's fine to cache the results of transpilation, as compileTranspileOnly does, +// but it's not safe for this function to perform caching based purely on the +// mtime of a single source file. A dependency may have (for example) +// deleted something that the file was referencing. +// TODO: Cache compile results based on the mtime of all affected files, +// not just the one we're currently getting the results for +// (update cachedMtime() to accept an array of paths) +const compileTypeCheck = (fileName: string) => { const normalizedFileName: string = normalizeSlashes(fileName) /* c8 ignore next */ const content = readFile(fileName) || '' return compile(content, normalizedFileName, true) -}) +} const compileTranspileOnly = cachedMtime((fileName: string) => { const normalizedFileName: string = normalizeSlashes(fileName)