From 6a1b24caad8e59ee991624d24795aca51623fc48 Mon Sep 17 00:00:00 2001 From: Ahn Date: Mon, 10 Feb 2020 17:27:05 +0700 Subject: [PATCH 1/2] fix(compiler): use Program as fallback for language service --- src/__snapshots__/compiler.spec.ts.snap | 5 ++ src/compiler.spec.ts | 113 ++++++++++++++++++++---- src/compiler.ts | 76 ++++++++++++---- 3 files changed, 162 insertions(+), 32 deletions(-) diff --git a/src/__snapshots__/compiler.spec.ts.snap b/src/__snapshots__/compiler.spec.ts.snap index f6496bce1d..1d67190525 100644 --- a/src/__snapshots__/compiler.spec.ts.snap +++ b/src/__snapshots__/compiler.spec.ts.snap @@ -1,5 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Program should compile using Program as fallback: compile-error 1`] = ` +"Unable to require \`.d.ts\` file. +This is usually the result of a faulty configuration or import. Make sure there is a \`.js\`, \`.json\` or another executable extension available alongside \`test.ts\`." +`; + exports[`allowJs should compile js file 1`] = ` ===[ FILE: src/compiler.spec.ts.test.js ]======================================= "use strict"; diff --git a/src/compiler.spec.ts b/src/compiler.spec.ts index 4217605c88..1aa3c17f4f 100644 --- a/src/compiler.spec.ts +++ b/src/compiler.spec.ts @@ -2,6 +2,7 @@ import { Config } from '@jest/types' import { LogLevels } from 'bs-logger' import { removeSync, writeFileSync } from 'fs-extra' +import { LanguageService, ModuleKind, ScriptTarget } from 'typescript' import * as fakers from './__helpers__/fakers' import { logTargetMock } from './__helpers__/mocks' @@ -29,9 +30,43 @@ function makeCompiler({ pretty: false, } const cs = new ConfigSet(fakers.jestConfig(jestConfig, tsJestConfig), parentConfig) + return createCompiler(cs) } +function makeFallbackCompiler({ + jestConfig, + tsJestConfig, + parentConfig, +}: { + jestConfig?: Partial + tsJestConfig?: TsJestGlobalOptions + parentConfig?: TsJestGlobalOptions +} = {}) { + const compiler = makeCompiler({ jestConfig, tsJestConfig, parentConfig }) + const languageService: LanguageService = compiler.ts.createLanguageService({ + getScriptFileNames: jest.fn(), + getScriptVersion: jest.fn(), + getScriptSnapshot: jest.fn(), + getCurrentDirectory: () => '.', + getDefaultLibFileName: jest.fn(), + getCompilationSettings: () => { + return { + target: ScriptTarget.ES2018, + module: ModuleKind.CommonJS, + lib: ['dom', 'es2018'], + } + }, + }) + compiler.ts.createLanguageService = jest.fn().mockReturnValue(languageService) + languageService.getEmitOutput = jest.fn().mockReturnValue({ + outputFiles: [], + emitSkipped: false, + }) + + return compiler +} + beforeEach(() => { logTarget.clear() }) @@ -83,21 +118,21 @@ describe('cache', () => { it('should use the cache', () => { const compiled1 = compiler.compile(source, __filename) expect(logTarget.filteredLines(LogLevels.debug, Infinity)).toMatchInlineSnapshot(` -Array [ - "[level:20] readThrough(): cache miss -", - "[level:20] getOutput(): compiling using language service -", - "[level:20] updateMemoryCache() -", - "[level:20] visitSourceFileNode(): hoisting -", - "[level:20] getOutput(): computing diagnostics -", - "[level:20] readThrough(): writing caches -", -] -`) + Array [ + "[level:20] readThrough(): cache miss + ", + "[level:20] getOutput(): compiling using language service + ", + "[level:20] updateMemoryCache() + ", + "[level:20] visitSourceFileNode(): hoisting + ", + "[level:20] getOutput(): computing diagnostics from language service + ", + "[level:20] readThrough(): writing caches + ", + ] + `) logTarget.clear() const compiled2 = compiler.compile(source, __filename) @@ -161,3 +196,51 @@ console.log(val.p1/* <== that */) }) }) }) + +describe('Program', () => { + // These preparation steps are needed to make the test work correctly + const tmp = tempDir('compiler') + let compiler = makeFallbackCompiler({ + jestConfig: { cache: true, cacheDirectory: tmp }, + tsJestConfig: { tsConfig: false }, + }) + const source = 'console.log("hello")' + it('should compile using Program as fallback', () => { + try { + compiler = makeFallbackCompiler({ tsJestConfig: { tsConfig: false } }) + compiler.compile(source, 'test.ts') + } catch (e) { + expect(e.message).toMatchSnapshot('compile-error') + expect(logTarget.filteredLines(LogLevels.debug, Infinity)).toMatchInlineSnapshot(` + Array [ + "[level:20] backporting config + ", + "[level:20] normalized jest config + ", + "[level:20] normalized ts-jest config + ", + "[level:20] creating typescript compiler (language service) + ", + "[level:20] file caching disabled + ", + "[level:20] normalized typescript config + ", + "[level:20] creating language service + ", + "[level:20] readThrough(): no cache + ", + "[level:20] getOutput(): compiling using language service + ", + "[level:20] updateMemoryCache() + ", + "[level:20] getOutput(): creating Program as fallback for language service + ", + "[level:20] getOutput(): compiling using Program + ", + "[level:20] getOutput(): computing diagnostics from Program emit result + ", + ] + `) + } + }) +}) diff --git a/src/compiler.ts b/src/compiler.ts index a11acf3f6a..1bb9340ecd 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -36,6 +36,15 @@ import { readFileSync, writeFileSync } from 'fs' import memoize = require('lodash.memoize') import mkdirp = require('mkdirp') import { basename, extname, join, normalize, relative } from 'path' +import { + Diagnostic, + EmitOutput, + EmitResult, + LanguageService, + LanguageServiceHost, + OutputFile, + Program, +} from 'typescript' import { ConfigSet } from './config/config-set' import { MemoryCache, TsCompiler, TypeInfo } from './types' @@ -133,7 +142,7 @@ export function createCompiler(configs: ConfigSet): TsCompiler { [LogContexts.logLevel]: LogLevels.trace, } - const serviceHost = { + const serviceHost: LanguageServiceHost = { getScriptFileNames: () => Object.keys(memoryCache.versions), getScriptVersion: (fileName: string) => { const normalizedFileName = normalize(fileName) @@ -175,35 +184,68 @@ export function createCompiler(configs: ConfigSet): TsCompiler { } logger.debug('creating language service') - const service = ts.createLanguageService(serviceHost) - + const service: LanguageService = ts.createLanguageService(serviceHost) getOutput = (code: string, fileName: string /*, lineOffset = 0 */) => { logger.debug({ fileName }, 'getOutput(): compiling using language service') // Must set memory cache before attempting to read file. updateMemoryCache(code, fileName) - - const output = service.getEmitOutput(fileName) - + // get compiled js, source map, diagnostics information, etc... + let emitOutput: EmitOutput = service.getEmitOutput(fileName) + let emitResult: EmitResult + /** + * Fallback to Program when language service cannot compile. This is a workaround for the issue with composite + * project. Perhaps we should switch to use Program instead of language service + */ + if (!emitOutput.outputFiles.length) { + logger.debug({ fileName }, 'getOutput(): creating Program as fallback for language service') + // Create a Program with an in-memory emit + const createdFiles: OutputFile[] = [] + const host = ts.createCompilerHost(compilerOptions) + host.writeFile = (compiledFileName: string, contents: string, writeByteOrderMark: boolean) => { + createdFiles.push({ + name: compiledFileName, + text: contents, + writeByteOrderMark, + }) + } + // Prepare and emit the d.ts files + const program: Program = ts.createProgram({ + rootNames: [fileName], + host, + projectReferences: configs.tsconfig.references, + options: compilerOptions, + }) + logger.debug({ fileName }, 'getOutput(): compiling using Program') + emitResult = program.emit(undefined, undefined, undefined, false, configs.tsCustomTransformers) + emitOutput = { + ...emitOutput, + outputFiles: createdFiles, + } + } if (configs.shouldReportDiagnostic(fileName)) { - logger.debug({ fileName }, 'getOutput(): computing diagnostics') // Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`. - const diagnostics = service - .getCompilerOptionsDiagnostics() - .concat(service.getSyntacticDiagnostics(fileName)) - .concat(service.getSemanticDiagnostics(fileName)) - + let diagnostics: Diagnostic[] + // @ts-ignore + if (emitResult) { + logger.debug({ fileName }, 'getOutput(): computing diagnostics from Program emit result') + diagnostics = [...emitResult.diagnostics] + } else { + logger.debug({ fileName }, 'getOutput(): computing diagnostics from language service') + diagnostics = service + .getCompilerOptionsDiagnostics() + .concat(service.getSyntacticDiagnostics(fileName)) + .concat(service.getSemanticDiagnostics(fileName)) + } // will raise or just warn diagnostics depending on config configs.raiseDiagnostics(diagnostics, fileName, logger) } - /* istanbul ignore next (this should never happen but is kept for security) */ - if (output.emitSkipped) { + if (emitOutput.emitSkipped) { throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`) } - // Throw an error when requiring `.d.ts` files. /* istanbul ignore next (this should never happen but is kept for security) */ - if (output.outputFiles.length === 0) { + if (!emitOutput.outputFiles.length) { throw new TypeError( interpolate(Errors.UnableToRequireDefinitionFile, { file: basename(fileName), @@ -211,7 +253,7 @@ export function createCompiler(configs: ConfigSet): TsCompiler { ) } - return [output.outputFiles[1].text, output.outputFiles[0].text] + return [emitOutput.outputFiles[1].text, emitOutput.outputFiles[0].text] } getTypeInfo = (code: string, fileName: string, position: number) => { From ad1abe9701aec2d9b38d85aa6e819aca012a135f Mon Sep 17 00:00:00 2001 From: Ahn Date: Mon, 10 Feb 2020 17:35:47 +0700 Subject: [PATCH 2/2] fix(test): update snapshot for logger test --- e2e/__tests__/__snapshots__/logger.test.ts.snap | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/e2e/__tests__/__snapshots__/logger.test.ts.snap b/e2e/__tests__/__snapshots__/logger.test.ts.snap index 82c1d4ab21..83d3fbcd13 100644 --- a/e2e/__tests__/__snapshots__/logger.test.ts.snap +++ b/e2e/__tests__/__snapshots__/logger.test.ts.snap @@ -26,14 +26,14 @@ Array [ "[level:20] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from language service", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", "[level:20] readThrough(): no cache", "[level:20] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from language service", ] `; @@ -67,7 +67,7 @@ Array [ "[level:20] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from 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] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from language service", "[level:20] calling babel-jest processor", ] `; @@ -111,7 +111,7 @@ Array [ "[level:20] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from 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] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from language service", "[level:20] calling babel-jest processor", ] `; @@ -150,14 +150,14 @@ Array [ "[level:20] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from language service", "[level:20] computing cache key for /Hello.ts", "[level:20] processing /Hello.ts", "[level:20] readThrough(): no cache", "[level:20] getOutput(): compiling using language service", "[level:20] updateMemoryCache()", "[level:20] visitSourceFileNode(): hoisting", - "[level:20] getOutput(): computing diagnostics", + "[level:20] getOutput(): computing diagnostics from language service", ] `;