From 3e8efbec16fd8575ee219a0350b8eb61c0056ce6 Mon Sep 17 00:00:00 2001 From: Ahn Date: Sun, 19 Apr 2020 16:45:56 +0200 Subject: [PATCH] fix(compiler): make `projectReferences` work with `LanguageService` (#1541) --- .../simple/dependency/package.json | 2 +- .../simple/with-dependency/package.json | 6 +- .../yarn-workspace-composite/.gitignore | 1 + .../yarn-workspace-composite/package.json | 8 +- .../yarn-workspace-composite/tsconfig.json | 1 + .../__snapshots__/logger.test.ts.snap | 18 +-- src/compiler/compiler-utils.spec.ts | 3 +- src/compiler/compiler-utils.ts | 48 ++++++- src/compiler/instance.ts | 17 ++- src/compiler/language-service.spec.ts | 53 ++++++- src/compiler/language-service.ts | 131 +++++++++++------- src/compiler/program.spec.ts | 2 +- src/compiler/program.ts | 8 +- src/compiler/transpiler.spec.ts | 70 +++++++--- src/compiler/transpiler.ts | 67 +++------ src/types.ts | 1 + 16 files changed, 284 insertions(+), 152 deletions(-) create mode 100644 e2e/__external-repos__/yarn-workspace-composite/.gitignore diff --git a/e2e/__external-repos__/simple/dependency/package.json b/e2e/__external-repos__/simple/dependency/package.json index 677e33b012..772ef01de1 100644 --- a/e2e/__external-repos__/simple/dependency/package.json +++ b/e2e/__external-repos__/simple/dependency/package.json @@ -2,7 +2,7 @@ "name": "dependency", "version": "0.0.1", "peerDependencies": { - "typescript": "^3.7.5" + "typescript": "^3.8.3" }, "main": "./index.ts" } diff --git a/e2e/__external-repos__/simple/with-dependency/package.json b/e2e/__external-repos__/simple/with-dependency/package.json index d0c64b6486..5b9df80d9c 100644 --- a/e2e/__external-repos__/simple/with-dependency/package.json +++ b/e2e/__external-repos__/simple/with-dependency/package.json @@ -36,12 +36,12 @@ } }, "devDependencies": { - "typescript": "^3.7.5" + "typescript": "^3.8.3" }, "dependencies": { - "@types/jest": "^25.1.2", + "@types/jest": "^25.2.1", "dependency": "file:../dependency", - "jest": "^25.1.0" + "jest": "^25.3.0" }, "main": "./index.ts" } diff --git a/e2e/__external-repos__/yarn-workspace-composite/.gitignore b/e2e/__external-repos__/yarn-workspace-composite/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/e2e/__external-repos__/yarn-workspace-composite/.gitignore @@ -0,0 +1 @@ +/target diff --git a/e2e/__external-repos__/yarn-workspace-composite/package.json b/e2e/__external-repos__/yarn-workspace-composite/package.json index c23b857449..fcf77af3b7 100644 --- a/e2e/__external-repos__/yarn-workspace-composite/package.json +++ b/e2e/__external-repos__/yarn-workspace-composite/package.json @@ -6,11 +6,11 @@ "packages/*" ], "scripts": { - "test": "jest --no-cache" + "test": "yarn tsc -b packages/my-app/tsconfig.json && jest --no-cache" }, "devDependencies": { - "@types/jest": "^25.1.2", - "jest": "^25.1.0", - "typescript": "~3.7.5" + "@types/jest": "^25.2.1", + "jest": "^25.3.0", + "typescript": "~3.8.3" } } diff --git a/e2e/__external-repos__/yarn-workspace-composite/tsconfig.json b/e2e/__external-repos__/yarn-workspace-composite/tsconfig.json index fff6928601..61da5551f7 100644 --- a/e2e/__external-repos__/yarn-workspace-composite/tsconfig.json +++ b/e2e/__external-repos__/yarn-workspace-composite/tsconfig.json @@ -9,6 +9,7 @@ "noImplicitAny": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, + "outDir": "./target/", "lib": [ "dom", "es2018" ] diff --git a/e2e/__tests__/__snapshots__/logger.test.ts.snap b/e2e/__tests__/__snapshots__/logger.test.ts.snap index 05837d39d7..f1998d5850 100644 --- a/e2e/__tests__/__snapshots__/logger.test.ts.snap +++ b/e2e/__tests__/__snapshots__/logger.test.ts.snap @@ -20,20 +20,20 @@ Array [ "[level:20] normalized typescript config", "[level:20] processing /Hello.spec.ts", "[level:20] file caching disabled", - "[level:20] compileUsingLanguageService(): create typescript compiler", + "[level:20] initializeLanguageServiceInstance(): create typescript compiler", "[level:20] compileUsingLanguageService(): creating language service", "[level:20] readThrough(): no cache", "[level:20] compileFn(): compiling using language service", "[level:20] updateMemoryCache(): update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] diagnoseFn(): computing diagnostics for /Hello.spec.ts using language service", + "[level:20] compileFn(): computing diagnostics for /Hello.spec.ts using language service", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", "[level:20] readThrough(): no cache", "[level:20] compileFn(): compiling using language service", "[level:20] updateMemoryCache(): update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] diagnoseFn(): computing diagnostics for /Hello.ts using language service", + "[level:20] compileFn(): computing diagnostics for /Hello.ts using language service", ] `; @@ -61,13 +61,13 @@ Array [ "[level:20] patching babel-jest", "[level:20] checking version of babel-jest: OK", "[level:20] file caching disabled", - "[level:20] compileUsingLanguageService(): create typescript compiler", + "[level:20] initializeLanguageServiceInstance(): create typescript compiler", "[level:20] compileUsingLanguageService(): creating language service", "[level:20] readThrough(): no cache", "[level:20] compileFn(): compiling using language service", "[level:20] updateMemoryCache(): update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] diagnoseFn(): computing diagnostics for /Hello.spec.ts using language service", + "[level:20] compileFn(): computing diagnostics for /Hello.spec.ts using language service", "[level:20] calling babel-jest processor", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", @@ -75,7 +75,7 @@ Array [ "[level:20] compileFn(): compiling using language service", "[level:20] updateMemoryCache(): update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] diagnoseFn(): computing diagnostics for /Hello.ts using language service", + "[level:20] compileFn(): computing diagnostics for /Hello.ts using language service", "[level:20] calling babel-jest processor", ] `; @@ -105,13 +105,13 @@ Array [ "[level:20] patching babel-jest", "[level:20] checking version of babel-jest: OK", "[level:20] file caching disabled", - "[level:20] compileUsingLanguageService(): create typescript compiler", + "[level:20] initializeLanguageServiceInstance(): create typescript compiler", "[level:20] compileUsingLanguageService(): creating language service", "[level:20] readThrough(): no cache", "[level:20] compileFn(): compiling using language service", "[level:20] updateMemoryCache(): update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] diagnoseFn(): computing diagnostics for /Hello.spec.ts using language service", + "[level:20] compileFn(): computing diagnostics for /Hello.spec.ts using language service", "[level:20] calling babel-jest processor", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", @@ -119,7 +119,7 @@ Array [ "[level:20] compileFn(): compiling using language service", "[level:20] updateMemoryCache(): update memory cache for language service", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] diagnoseFn(): computing diagnostics for /Hello.ts using language service", + "[level:20] compileFn(): computing diagnostics for /Hello.ts using language service", "[level:20] calling babel-jest processor", ] `; diff --git a/src/compiler/compiler-utils.spec.ts b/src/compiler/compiler-utils.spec.ts index 9f4d9b88b9..e53f1687ee 100644 --- a/src/compiler/compiler-utils.spec.ts +++ b/src/compiler/compiler-utils.spec.ts @@ -4,7 +4,7 @@ import { resolve } from 'path' import { makeCompiler } from '../__helpers__/fakers' import { tempDir } from '../__helpers__/path' -import { MemoryCache } from '../types' +import { MemoryCache, TSFile } from '../types' import { cacheResolvedModules, getResolvedModulesCache } from './compiler-utils' @@ -13,6 +13,7 @@ const memoryCache: MemoryCache = { versions: Object.create(null), outputs: Object.create(null), resolvedModules: Object.create(null), + files: new Map(), } describe('cacheResolvedModules', () => { diff --git a/src/compiler/compiler-utils.ts b/src/compiler/compiler-utils.ts index 022e99ba7c..7685a9bee7 100644 --- a/src/compiler/compiler-utils.ts +++ b/src/compiler/compiler-utils.ts @@ -4,8 +4,9 @@ import micromatch = require('micromatch') import { dirname, join, normalize, relative, resolve } from 'path' import * as _ts from 'typescript' +import { ConfigSet } from '../config/config-set' import { EXTENSION_REGEX, JSON_REGEX, TS_TSX_REGEX } from '../constants' -import { MemoryCache, TSFiles } from '../types' +import { MemoryCache, SourceOutput, TSFiles } from '../types' import { sha1 } from '../util/sha1' /** @@ -145,12 +146,11 @@ function getOutputJavaScriptFileName(inputFileName: string, projectReference: _t } /** - * @internal * Gets the output JS file path for an input file governed by a composite project. * Pulls from the cache if it exists; computes and caches the result otherwise. */ /* istanbul ignore next (we leave this for e2e) */ -export function getAndCacheOutputJSFileName( +function getAndCacheOutputJSFileName( inputFileName: string, projectReference: _ts.ResolvedProjectReference, files: TSFiles, @@ -170,3 +170,45 @@ export function getAndCacheOutputJSFileName( return outputFileName } + +/** + * @internal + */ +/* istanbul ignore next (we leave this for e2e) */ +export function getCompileResultFromReferencedProject( + fileName: string, + configs: ConfigSet, + files: TSFiles, + referencedProject: _ts.ResolvedProjectReference, +): SourceOutput { + const [relativeProjectConfigPath, relativeFilePath] = [ + configs.resolvePath(referencedProject.sourceFile.fileName), + configs.resolvePath(fileName), + ] + if (referencedProject.commandLine.options.outFile !== undefined) { + throw new Error( + `The referenced project at ${relativeProjectConfigPath} is using ` + + `the outFile' option, which is not supported with ts-jest.`, + ) + } + + const jsFileName = getAndCacheOutputJSFileName(fileName, referencedProject, files) + const relativeJSFileName = configs.resolvePath(jsFileName) + if (!configs.compilerModule.sys.fileExists(jsFileName)) { + throw new Error( + // tslint:disable-next-line:prefer-template + `Could not find output JavaScript file for input ` + + `${relativeFilePath} (looked at ${relativeJSFileName}).\n` + + `The input file is part of a project reference located at ` + + `${relativeProjectConfigPath}, so ts-jest is looking for the ` + + 'project’s pre-built output on disk. Try running `tsc --build` ' + + 'to build project references.', + ) + } + + const mapFileName = `${jsFileName}.map` + const outputText = configs.compilerModule.sys.readFile(jsFileName) + const sourceMapText = configs.compilerModule.sys.readFile(mapFileName) + + return [outputText!, sourceMapText!] +} diff --git a/src/compiler/instance.ts b/src/compiler/instance.ts index 7150cf15cb..bf5a9a6e60 100644 --- a/src/compiler/instance.ts +++ b/src/compiler/instance.ts @@ -35,7 +35,7 @@ import mkdirp = require('mkdirp') import { basename, extname, join, normalize } from 'path' import { ConfigSet } from '../config/config-set' -import { CompileFn, CompilerInstance, MemoryCache, TsCompiler } from '../types' +import { CompileFn, CompilerInstance, MemoryCache, TSFile, TsCompiler } from '../types' import { sha1 } from '../util/sha1' import { getResolvedModulesCache } from './compiler-utils' @@ -168,6 +168,7 @@ export const createCompilerInstance = (configs: ConfigSet): TsCompiler => { versions: Object.create(null), outputs: Object.create(null), resolvedModules: Object.create(null), + files: new Map(), } // Enable `allowJs` when flag is set. if (compilerOptions.allowJs) { @@ -175,7 +176,13 @@ export const createCompilerInstance = (configs: ConfigSet): TsCompiler => { extensions.push('.jsx') } // Initialize files from TypeScript into project. - for (const path of fileNames) memoryCache.versions[normalize(path)] = 1 + for (const path of fileNames) { + const normalizedFilePath = normalize(path) + memoryCache.versions[normalizedFilePath] = 1 + memoryCache.files.set(normalizedFilePath, { + version: 0, + }) + } /** * Get the extension for a transpiled file. */ @@ -187,10 +194,10 @@ export const createCompilerInstance = (configs: ConfigSet): TsCompiler => { if (!tsJest.isolatedModules) { // Use language services by default compilerInstance = !tsJest.compilerHost - ? initializeLanguageServiceInstance(configs, logger, memoryCache) - : initializeProgramInstance(configs, logger, memoryCache) + ? initializeLanguageServiceInstance(configs, memoryCache, logger) + : initializeProgramInstance(configs, memoryCache, logger) } else { - compilerInstance = initializeTranspilerInstance(configs, logger) + compilerInstance = initializeTranspilerInstance(configs, memoryCache, logger) } const compile = compileAndCacheResult(cachedir, memoryCache, compilerInstance.compileFn, getExtension, logger) diff --git a/src/compiler/language-service.spec.ts b/src/compiler/language-service.spec.ts index 2607105a4b..cb4e0abb39 100644 --- a/src/compiler/language-service.spec.ts +++ b/src/compiler/language-service.spec.ts @@ -40,7 +40,7 @@ describe('Language service', () => { ", "[level:20] visitSourceFileNode(): hoisting ", - "[level:20] diagnoseFn(): computing diagnostics for test-cache.ts using language service + "[level:20] compileFn(): computing diagnostics for test-cache.ts using language service ", "[level:20] readThrough(): writing caches ", @@ -63,6 +63,57 @@ describe('Language service', () => { removeSync(fileName) }) + it('should get compile result from referenced project when there is a built reference project', () => { + const tmp = tempDir('compiler') + const compiler = makeCompiler({ + jestConfig: { cache: true, cacheDirectory: tmp }, + tsJestConfig: { tsConfig: false }, + }) + const source = 'console.log("hello")' + const fileName = 'test-reference-project.ts' + const getAndCacheProjectReferenceSpy = jest + .spyOn(compilerUtils, 'getAndCacheProjectReference') + .mockReturnValueOnce({} as any) + jest + .spyOn(compilerUtils, 'getCompileResultFromReferencedProject') + .mockImplementationOnce(() => [ + source, + '{"version":3,"file":"test-reference-project.js","sourceRoot":"","sources":["test-reference-project.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA","sourcesContent":["console.log(\\"hello\\")"]}', + ]) + writeFileSync(fileName, source, 'utf8') + + compiler.compile(source, fileName) + + expect(getAndCacheProjectReferenceSpy).toHaveBeenCalled() + expect(compilerUtils.getCompileResultFromReferencedProject).toHaveBeenCalled() + + jest.restoreAllMocks() + removeSync(fileName) + }) + + it('should get compile result from language service when there is no referenced project', () => { + const tmp = tempDir('compiler') + const compiler = makeCompiler({ + jestConfig: { cache: true, cacheDirectory: tmp }, + tsJestConfig: { tsConfig: false }, + }) + const source = 'console.log("hello")' + const fileName = 'test-no-reference-project.ts' + const getAndCacheProjectReferenceSpy = jest + .spyOn(compilerUtils, 'getAndCacheProjectReference') + .mockReturnValueOnce(undefined) + jest.spyOn(compilerUtils, 'getCompileResultFromReferencedProject') + writeFileSync(fileName, source, 'utf8') + + compiler.compile(source, fileName) + + expect(getAndCacheProjectReferenceSpy).toHaveBeenCalled() + expect(compilerUtils.getCompileResultFromReferencedProject).not.toHaveBeenCalled() + + jest.restoreAllMocks() + removeSync(fileName) + }) + it('should cache resolved modules for test file with testMatchPatterns from jest config when match', () => { // tslint:disable-next-line:no-empty const spy = jest.spyOn(compilerUtils, 'cacheResolvedModules').mockImplementationOnce(() => {}) diff --git a/src/compiler/language-service.ts b/src/compiler/language-service.ts index 0add135448..1f462e2aef 100644 --- a/src/compiler/language-service.ts +++ b/src/compiler/language-service.ts @@ -8,7 +8,13 @@ import { LINE_FEED } from '../constants' import { CompilerInstance, MemoryCache, SourceOutput } from '../types' import { Errors, interpolate } from '../util/messages' -import { cacheResolvedModules, hasOwn, isTestFile } from './compiler-utils' +import { + cacheResolvedModules, + getAndCacheProjectReference, + getCompileResultFromReferencedProject, + hasOwn, + isTestFile, +} from './compiler-utils' function doTypeChecking(configs: ConfigSet, fileName: string, service: _ts.LanguageService, logger: Logger) { if (configs.shouldReportDiagnostic(fileName)) { @@ -24,15 +30,15 @@ function doTypeChecking(configs: ConfigSet, fileName: string, service: _ts.Langu */ export const initializeLanguageServiceInstance = ( configs: ConfigSet, - logger: Logger, memoryCache: MemoryCache, + logger: Logger, ): CompilerInstance => { - logger.debug('compileUsingLanguageService(): create typescript compiler') + logger.debug('initializeLanguageServiceInstance(): create typescript compiler') const ts = configs.compilerModule const cwd = configs.cwd const cacheDir = configs.tsCacheDir - const { options } = configs.typescript + const { options, projectReferences } = configs.typescript const serviceHostTraceCtx = { namespace: 'ts:serviceHost', call: null, @@ -43,6 +49,12 @@ export const initializeLanguageServiceInstance = ( const updateMemoryCache = (contents: string, fileName: string) => { logger.debug({ fileName }, `updateMemoryCache(): update memory cache for language service`) + const file = memoryCache.files.get(fileName) + /* istanbul ignore next (covered by e2e) */ + if (file && file?.text !== contents) { + file.version++ + file.text = contents + } let shouldIncrementProjectVersion = false const fileVersion = memoryCache.versions[fileName] ?? 0 const isFileInCache = fileVersion !== 0 @@ -63,6 +75,7 @@ export const initializeLanguageServiceInstance = ( } const serviceHost: _ts.LanguageServiceHost = { getProjectVersion: () => String(projectVersion), + getProjectReferences: () => projectReferences, getScriptFileNames: () => Object.keys(memoryCache.versions), getScriptVersion: (fileName: string) => { const normalizedFileName = normalize(fileName) @@ -103,7 +116,7 @@ export const initializeLanguageServiceInstance = ( } logger.debug('compileUsingLanguageService(): creating language service') - const service: _ts.LanguageService = ts.createLanguageService(serviceHost) + const service: _ts.LanguageService = ts.createLanguageService(serviceHost, ts.createDocumentRegistry()) return { compileFn: (code: string, fileName: string): SourceOutput => { @@ -112,56 +125,68 @@ export const initializeLanguageServiceInstance = ( logger.debug({ normalizedFileName }, 'compileFn(): compiling using language service') // Must set memory cache before attempting to read file. updateMemoryCache(code, normalizedFileName) - const output: _ts.EmitOutput = service.getEmitOutput(normalizedFileName) - // Do type checking by getting TypeScript diagnostics - logger.debug(`diagnoseFn(): computing diagnostics for ${normalizedFileName} using language service`) - - doTypeChecking(configs, normalizedFileName, service, logger) - /** - * We don't need the following logic with no cache run because no cache always gives correct typing - */ - if (cacheDir) { - if (isTestFile(configs.testMatchPatterns, normalizedFileName)) { - cacheResolvedModules(normalizedFileName, code, memoryCache, service.getProgram()!, cacheDir, logger) - } else { - /* istanbul ignore next (covered by e2e) */ - Object.entries(memoryCache.resolvedModules) - .filter(entry => { - /** - * When imported modules change, we only need to check whether the test file is compiled previously or not - * base on memory cache. By checking memory cache, we can avoid repeatedly doing type checking against - * test file for 1st time run after clearing cache because - */ - return ( - entry[1].modulePaths.find(modulePath => modulePath === normalizedFileName) && - !hasOwn.call(memoryCache.outputs, entry[0]) - ) - }) - .forEach(entry => { - logger.debug( - `diagnoseFn(): computing diagnostics for test file that imports ${normalizedFileName} using language service`, - ) - - const testFileName = entry[0] - updateMemoryCache(entry[1].testFileContent, testFileName) - doTypeChecking(configs, testFileName, service, logger) - }) + const referencedProject = getAndCacheProjectReference( + normalizedFileName, + service.getProgram()!, + memoryCache.files, + projectReferences, + ) + if (referencedProject !== undefined) { + logger.debug({ normalizedFileName }, 'compileFn(): get compile result from referenced project') + + return getCompileResultFromReferencedProject(normalizedFileName, configs, memoryCache.files, referencedProject) + } else { + const output: _ts.EmitOutput = service.getEmitOutput(normalizedFileName) + // Do type checking by getting TypeScript diagnostics + logger.debug(`compileFn(): computing diagnostics for ${normalizedFileName} using language service`) + + doTypeChecking(configs, normalizedFileName, service, logger) + /** + * We don't need the following logic with no cache run because no cache always gives correct typing + */ + if (cacheDir) { + if (isTestFile(configs.testMatchPatterns, normalizedFileName)) { + cacheResolvedModules(normalizedFileName, code, memoryCache, service.getProgram()!, cacheDir, logger) + } else { + /* istanbul ignore next (covered by e2e) */ + Object.entries(memoryCache.resolvedModules) + .filter(entry => { + /** + * When imported modules change, we only need to check whether the test file is compiled previously or not + * base on memory cache. By checking memory cache, we can avoid repeatedly doing type checking against + * test file for 1st time run after clearing cache because + */ + return ( + entry[1].modulePaths.find(modulePath => modulePath === normalizedFileName) && + !hasOwn.call(memoryCache.outputs, entry[0]) + ) + }) + .forEach(entry => { + logger.debug( + `compileFn(): computing diagnostics for test file that imports ${normalizedFileName} using language service`, + ) + + const testFileName = entry[0] + updateMemoryCache(entry[1].testFileContent, testFileName) + doTypeChecking(configs, testFileName, service, logger) + }) + } + } + /* istanbul ignore next (this should never happen but is kept for security) */ + if (output.emitSkipped) { + throw new TypeError(`${relative(cwd, normalizedFileName)}: Emit skipped for language service`) + } + // Throw an error when requiring `.d.ts` files. + if (!output.outputFiles.length) { + throw new TypeError( + interpolate(Errors.UnableToRequireDefinitionFile, { + file: basename(normalizedFileName), + }), + ) } - } - /* istanbul ignore next (this should never happen but is kept for security) */ - if (output.emitSkipped) { - throw new TypeError(`${relative(cwd, normalizedFileName)}: Emit skipped for language service`) - } - // Throw an error when requiring `.d.ts` files. - if (!output.outputFiles.length) { - throw new TypeError( - interpolate(Errors.UnableToRequireDefinitionFile, { - file: basename(normalizedFileName), - }), - ) - } - return [output.outputFiles[1].text, output.outputFiles[0].text] + return [output.outputFiles[1].text, output.outputFiles[0].text] + } }, program: service.getProgram(), } diff --git a/src/compiler/program.spec.ts b/src/compiler/program.spec.ts index c29c981bc0..4b6a097c5c 100644 --- a/src/compiler/program.spec.ts +++ b/src/compiler/program.spec.ts @@ -39,7 +39,7 @@ describe('cache', () => { ", "[level:20] visitSourceFileNode(): hoisting ", - "[level:20] diagnoseFn(): computing diagnostics for test-cache-incremental-program.ts using incremental program + "[level:20] compileFn(): computing diagnostics for test-cache-incremental-program.ts using incremental program ", "[level:20] readThrough(): writing caches ", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 5b6de5b66f..46e6259cad 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -24,10 +24,10 @@ function doTypeChecking(configs: ConfigSet, fileName: string, program: _ts.Progr */ export const initializeProgramInstance = ( configs: ConfigSet, - logger: Logger, memoryCache: MemoryCache, + logger: Logger, ): CompilerInstance => { - logger.debug('compileUsingProgram(): create typescript compiler') + logger.debug('initializeProgramInstance(): create typescript compiler') const ts = configs.compilerModule const cwd = configs.cwd @@ -117,7 +117,7 @@ export const initializeProgramInstance = ( customTransformers, ) // Do type checking by getting TypeScript diagnostics - logger.debug(`diagnoseFn(): computing diagnostics for ${normalizedFileName} using incremental program`) + logger.debug(`compileFn(): computing diagnostics for ${normalizedFileName} using incremental program`) doTypeChecking(configs, normalizedFileName, program, logger) /** @@ -142,7 +142,7 @@ export const initializeProgramInstance = ( }) .forEach(entry => { logger.debug( - `diagnoseFn(): computing diagnostics for test file that imports ${normalizedFileName} using incremental program`, + `compileFn(): computing diagnostics for test file that imports ${normalizedFileName} using incremental program`, ) doTypeChecking(configs, entry[0], program, logger) diff --git a/src/compiler/transpiler.spec.ts b/src/compiler/transpiler.spec.ts index aaa5a92375..e79bf0cb48 100644 --- a/src/compiler/transpiler.spec.ts +++ b/src/compiler/transpiler.spec.ts @@ -1,11 +1,14 @@ import { LogLevels } from 'bs-logger' import { removeSync, writeFileSync } from 'fs-extra' +import * as _ts from 'typescript' import { makeCompiler } from '../__helpers__/fakers' import { logTargetMock } from '../__helpers__/mocks' import ProcessedSource from '../__helpers__/processed-source' import { TS_JEST_OUT_DIR } from '../config/config-set' +import * as compilerUtils from './compiler-utils' + const logTarget = logTargetMock() describe('Transpiler', () => { @@ -19,7 +22,7 @@ describe('Transpiler', () => { it('should compile using transpileModule and not use cache', () => { const compiler = makeCompiler({ tsJestConfig: { ...baseTsJestConfig, tsConfig: false } }) - const spy = jest.spyOn(require('typescript'), 'transpileModule') + const spy = jest.spyOn(_ts, 'transpileModule') logTarget.clear() const compiled = compiler.compile('export default 42', __filename) @@ -30,7 +33,7 @@ describe('Transpiler', () => { Array [ "[level:20] readThrough(): no cache ", - "[level:20] getOutput(): compiling as isolated module + "[level:20] compileFn(): compiling as isolated module ", "[level:20] visitSourceFileNode(): hoisting ", @@ -40,38 +43,65 @@ describe('Transpiler', () => { spy.mockRestore() }) - it('should call createProgram() with projectReferences when there are projectReferences from tsconfig', () => { - const compiler = makeCompiler({ - tsJestConfig: { - ...baseTsJestConfig, - tsConfig: 'src/__mocks__/tsconfig-project-references.json', - }, - }) - const programSpy = jest.spyOn(require('typescript'), 'createProgram') - logTarget.clear() - compiler.compile('export default 42', __filename) + it( + 'should call createProgram() with projectReferences, call getAndCacheProjectReference()' + + ' and getCompileResultFromReferenceProject() when there are projectReferences from tsconfig', + () => { + const programSpy = jest.spyOn(_ts, 'createProgram') + const source = 'console.log("hello")' + const fileName = 'isolated-test-reference-project.ts' + const getAndCacheProjectReferenceSpy = jest + .spyOn(compilerUtils, 'getAndCacheProjectReference') + .mockReturnValueOnce({} as any) + jest + .spyOn(compilerUtils, 'getCompileResultFromReferencedProject') + .mockImplementationOnce(() => [ + source, + '{"version":3,"file":"isolated-test-reference-project.js","sourceRoot":"","sources":["isolated-test-reference-project.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA","sourcesContent":["console.log(\\"hello\\")"]}', + ]) + writeFileSync(fileName, source) + const compiler = makeCompiler({ + tsJestConfig: { + ...baseTsJestConfig, + tsConfig: 'src/__mocks__/tsconfig-project-references.json', + }, + }) + compiler.compile(source, fileName) - expect(programSpy).toHaveBeenCalled() - expect((programSpy.mock.calls[0][1] as any).configFilePath).toContain('tsconfig-project-references.json') + expect(programSpy).toHaveBeenCalled() + expect((programSpy.mock.calls[0][0] as any).options.configFilePath).toContain('tsconfig-project-references.json') + expect(getAndCacheProjectReferenceSpy).toHaveBeenCalled() + expect(compilerUtils.getCompileResultFromReferencedProject).toHaveBeenCalled() - programSpy.mockRestore() - }) + jest.restoreAllMocks() + removeSync(fileName) + }, + ) it('should call createProgram() without projectReferences when there are no projectReferences from tsconfig', () => { + const programSpy = jest.spyOn(_ts, 'createProgram') + const source = 'console.log("hello")' + const fileName = 'isolated-test-reference-project-1.ts' + const getAndCacheProjectReferenceSpy = jest + .spyOn(compilerUtils, 'getAndCacheProjectReference') + .mockReturnValueOnce(undefined) + jest.spyOn(compilerUtils, 'getCompileResultFromReferencedProject') + writeFileSync(fileName, source, 'utf8') const compiler = makeCompiler({ tsJestConfig: { ...baseTsJestConfig, tsConfig: false, }, }) - const programSpy = jest.spyOn(require('typescript'), 'createProgram') - logTarget.clear() - compiler.compile('export default 42', __filename) + compiler.compile(source, fileName) expect(programSpy).toHaveBeenCalled() expect((programSpy.mock.calls[0][1] as any).configFilePath).toBeUndefined() + expect(getAndCacheProjectReferenceSpy).toHaveBeenCalled() + expect(compilerUtils.getCompileResultFromReferencedProject).not.toHaveBeenCalled() - programSpy.mockRestore() + jest.restoreAllMocks() + removeSync(fileName) }) it('should compile js file for allowJs true', () => { diff --git a/src/compiler/transpiler.ts b/src/compiler/transpiler.ts index 9c49f184d4..626e20761a 100644 --- a/src/compiler/transpiler.ts +++ b/src/compiler/transpiler.ts @@ -1,38 +1,33 @@ import { Logger } from 'bs-logger' -import { readFileSync } from 'fs' import { normalize } from 'path' import * as _ts from 'typescript' import { ConfigSet } from '../config/config-set' -import { CompilerInstance, SourceOutput, TSFile, TSFiles } from '../types' +import { CompilerInstance, MemoryCache, SourceOutput } from '../types' -import { getAndCacheOutputJSFileName, getAndCacheProjectReference } from './compiler-utils' +import { getAndCacheProjectReference, getCompileResultFromReferencedProject } from './compiler-utils' /** * @internal */ -export const initializeTranspilerInstance = (configs: ConfigSet, logger: Logger): CompilerInstance => { - logger.debug('compileUsingTranspileModule(): create typescript compiler') +export const initializeTranspilerInstance = ( + configs: ConfigSet, + memoryCache: MemoryCache, + logger: Logger, +): CompilerInstance => { + logger.debug('initializeTranspilerInstance(): create typescript compiler') const { options, projectReferences, fileNames } = configs.typescript - const files: TSFiles = new Map() const ts = configs.compilerModule - fileNames.forEach(filePath => { - const normalizedFilePath = normalize(filePath) - files.set(normalizedFilePath, { - text: readFileSync(normalizedFilePath, 'utf-8'), - version: 0, - }) - }) const program = projectReferences ? ts.createProgram({ rootNames: fileNames, options, projectReferences, }) - : ts.createProgram([], options) + : ts.createProgram(fileNames, options) const updateFileInCache = (contents: string, filePath: string) => { - const file = files.get(filePath) + const file = memoryCache.files.get(filePath) if (file && file.text !== contents) { file.version++ file.text = contents @@ -43,43 +38,21 @@ export const initializeTranspilerInstance = (configs: ConfigSet, logger: Logger) compileFn: (code: string, fileName: string): SourceOutput => { const normalizedFileName = normalize(fileName) - logger.debug({ normalizedFileName }, 'getOutput(): compiling as isolated module') - updateFileInCache(code, normalizedFileName) - const referencedProject = getAndCacheProjectReference(normalizedFileName, program, files, projectReferences) + const referencedProject = getAndCacheProjectReference( + normalizedFileName, + program, + memoryCache.files, + projectReferences, + ) /* istanbul ignore next (referencedProject object is too complex to mock so we leave this for e2e) */ if (referencedProject !== undefined) { - const [relativeProjectConfigPath, relativeFilePath] = [ - configs.resolvePath(referencedProject.sourceFile.fileName), - configs.resolvePath(normalizedFileName), - ] - if (referencedProject.commandLine.options.outFile !== undefined) { - throw new Error( - `The referenced project at ${relativeProjectConfigPath} is using ` + - `the outFile' option, which is not supported with ts-jest.`, - ) - } + logger.debug({ normalizedFileName }, 'compileFn(): get compile result from referenced project') - const jsFileName = getAndCacheOutputJSFileName(normalizedFileName, referencedProject, files) - const relativeJSFileName = configs.resolvePath(jsFileName) - if (!ts.sys.fileExists(jsFileName)) { - throw new Error( - // tslint:disable-next-line:prefer-template - `Could not find output JavaScript file for input ` + - `${relativeFilePath} (looked at ${relativeJSFileName}).\n` + - `The input file is part of a project reference located at ` + - `${relativeProjectConfigPath}, so ts-jest is looking for the ` + - 'project’s pre-built output on disk. Try running `tsc --build` ' + - 'to build project references.', - ) - } - - const mapFileName = `${jsFileName}.map` - const outputText = ts.sys.readFile(jsFileName) - const sourceMapText = ts.sys.readFile(mapFileName) - - return [outputText!, sourceMapText!] + return getCompileResultFromReferencedProject(normalizedFileName, configs, memoryCache.files, referencedProject) } else { + logger.debug({ normalizedFileName }, 'compileFn(): compiling as isolated module') + const result: _ts.TranspileOutput = ts.transpileModule(code, { fileName: normalizedFileName, transformers: configs.tsCustomTransformers, diff --git a/src/types.ts b/src/types.ts index cb0df60049..4e31d00a4a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -200,6 +200,7 @@ export interface MemoryCache { modulePaths: string[] } } + files: TSFiles } /** * @internal