Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler): use Program as fallback for language service #1381

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 8 additions & 8 deletions e2e/__tests__/__snapshots__/logger.test.ts.snap
Expand Up @@ -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 <cwd>/Hello.ts",
"[level:20] processing <cwd>/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",
]
`;

Expand Down Expand Up @@ -67,15 +67,15 @@ 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 <cwd>/Hello.ts",
"[level:20] processing <cwd>/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",
"[level:20] calling babel-jest processor",
]
`;
Expand Down Expand Up @@ -111,15 +111,15 @@ 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 <cwd>/Hello.ts",
"[level:20] processing <cwd>/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",
"[level:20] calling babel-jest processor",
]
`;
Expand Down Expand Up @@ -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 <cwd>/Hello.ts",
"[level:20] processing <cwd>/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",
]
`;

Expand Down
5 changes: 5 additions & 0 deletions 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";
Expand Down
113 changes: 98 additions & 15 deletions src/compiler.spec.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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<Config.ProjectConfig>
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()
})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
",
]
`)
}
})
})
76 changes: 59 additions & 17 deletions src/compiler.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -175,43 +184,76 @@ 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),
}),
)
}

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) => {
Expand Down