Skip to content

Commit

Permalink
Incremental compiler support (#895)
Browse files Browse the repository at this point in the history
  • Loading branch information
blakeembrey committed Oct 12, 2019
1 parent 11707a4 commit cb4b748
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -18,7 +18,7 @@ script:
env:
- NODE=6 TYPESCRIPT=typescript@latest
- NODE=stable TYPESCRIPT=typescript@latest
- NODE=stable TYPESCRIPT=typescript@2.0
- NODE=stable TYPESCRIPT=typescript@2.7
- NODE=stable TYPESCRIPT=typescript@next

node_js:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -5,7 +5,7 @@
[![Build status][travis-image]][travis-url]
[![Test coverage][coveralls-image]][coveralls-url]

> TypeScript execution and REPL for node.js, with source map support. **Works with `typescript@>=2.0`**.
> TypeScript execution and REPL for node.js, with source map support. **Works with `typescript@>=2.7`**.
## Installation

Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -21,7 +21,7 @@
"prepare": "npm run build"
},
"engines": {
"node": ">=4.2.0"
"node": ">=6.0.0"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -49,7 +49,7 @@
"@types/chai": "^4.0.4",
"@types/diff": "^4.0.2",
"@types/mocha": "^5.0.0",
"@types/node": "^12.0.2",
"@types/node": "^12.7.12",
"@types/proxyquire": "^1.3.28",
"@types/react": "^16.0.2",
"@types/semver": "^6.0.0",
Expand All @@ -64,10 +64,10 @@
"semver": "^6.1.0",
"tslint": "^5.11.0",
"tslint-config-standard": "^8.0.1",
"typescript": "^3.6.3"
"typescript": "^3.6.4"
},
"peerDependencies": {
"typescript": ">=2.0"
"typescript": ">=2.7"
},
"dependencies": {
"arg": "^4.1.0",
Expand Down
3 changes: 2 additions & 1 deletion src/bin.ts
Expand Up @@ -278,7 +278,8 @@ function startRepl () {

undo()

repl.outputStream.write(`${name}\n${comment ? `${comment}\n` : ''}`)
if (name) repl.outputStream.write(`${name}\n`)
if (comment) repl.outputStream.write(`${comment}\n`)
repl.displayPrompt()
}
})
Expand Down
182 changes: 124 additions & 58 deletions src/index.ts
Expand Up @@ -79,11 +79,8 @@ export interface Options {
*/
class MemoryCache {
fileContents = new Map<string, string>()
fileVersions = new Map<string, number>()

constructor (public rootFileNames: string[] = []) {
for (const fileName of rootFileNames) this.fileVersions.set(fileName, 1)
}
constructor (public rootFileNames: string[]) {}
}

/**
Expand Down Expand Up @@ -122,7 +119,7 @@ const TS_NODE_COMPILER_OPTIONS = {
inlineSources: true,
declaration: false,
noEmit: false,
outDir: '$$ts-node$$'
outDir: '.ts-node'
}

/**
Expand Down Expand Up @@ -284,83 +281,113 @@ export function register (opts: Options = {}): Register {

const getCustomTransformers = () => {
if (typeof transformers === 'function') {
const program = service.getProgram()
return program ? transformers(program) : undefined
return transformers(builderProgram.getProgram())
}

return transformers
}

// Create the compiler host for type checking.
const serviceHost: _ts.LanguageServiceHost = {
getScriptFileNames: () => memoryCache.rootFileNames,
getScriptVersion: (fileName: string) => {
const version = memoryCache.fileVersions.get(fileName)
return version === undefined ? '' : version.toString()
const sys = {
...ts.sys,
readFile: (fileName: string) => {
const cacheContents = memoryCache.fileContents.get(fileName)
if (cacheContents !== undefined) return cacheContents
return cachedReadFile(fileName)
},
getScriptSnapshot (fileName: string) {
let contents = memoryCache.fileContents.get(fileName)

// Read contents into TypeScript memory cache.
if (contents === undefined) {
contents = cachedReadFile(fileName)
if (contents === undefined) return

memoryCache.fileVersions.set(fileName, 1)
memoryCache.fileContents.set(fileName, contents)
}

return ts.ScriptSnapshot.fromString(contents)
},
readFile: cachedReadFile,
readDirectory: cachedLookup(debugFn('readDirectory', ts.sys.readDirectory)),
getDirectories: cachedLookup(debugFn('getDirectories', ts.sys.getDirectories)),
fileExists: cachedLookup(debugFn('fileExists', fileExists)),
directoryExists: cachedLookup(debugFn('directoryExists', ts.sys.directoryExists)),
getNewLine: () => ts.sys.newLine,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getCurrentDirectory: () => cwd,
getCompilationSettings: () => config.options,
getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options),
getCustomTransformers: getCustomTransformers
resolvePath: cachedLookup(debugFn('resolvePath', ts.sys.resolvePath)),
realpath: ts.sys.realpath ? cachedLookup(debugFn('realpath', ts.sys.realpath)) : undefined,
getCurrentDirectory: () => cwd
}

const registry = ts.createDocumentRegistry(ts.sys.useCaseSensitiveFileNames, cwd)
const service = ts.createLanguageService(serviceHost, registry)
const host: _ts.CompilerHost = ts.createIncrementalCompilerHost
? ts.createIncrementalCompilerHost(config.options, sys)
: {
...sys,
getSourceFile: (fileName, languageVersion) => {
const contents = sys.readFile(fileName)
if (contents === undefined) return
return ts.createSourceFile(fileName, contents, languageVersion)
},
getDefaultLibLocation: () => normalizeSlashes(dirname(compiler)),
getDefaultLibFileName: () => normalizeSlashes(join(dirname(compiler), ts.getDefaultLibFileName(config.options))),
getCanonicalFileName: sys.useCaseSensitiveFileNames ? x => x : x => x.toLowerCase(),
getNewLine: () => sys.newLine,
useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames
}

// Fallback for older TypeScript releases without incremental API.
let builderProgram = ts.createIncrementalProgram
? ts.createIncrementalProgram({
rootNames: memoryCache.rootFileNames.slice(),
options: config.options,
host: host,
configFileParsingDiagnostics: config.errors,
projectReferences: config.projectReferences
})
: ts.createEmitAndSemanticDiagnosticsBuilderProgram(
memoryCache.rootFileNames.slice(),
config.options,
host,
undefined,
config.errors,
config.projectReferences
)

// Set the file contents into cache manually.
const updateMemoryCache = (contents: string, fileName: string) => {
const fileVersion = memoryCache.fileVersions.get(fileName) || 0
const sourceFile = builderProgram.getSourceFile(fileName)

// Add to `rootFiles` when discovered for the first time.
if (fileVersion === 0) memoryCache.rootFileNames.push(fileName)
memoryCache.fileContents.set(fileName, contents)

// Avoid incrementing cache when nothing has changed.
if (memoryCache.fileContents.get(fileName) === contents) return
// Add to `rootFiles` when discovered by compiler for the first time.
if (sourceFile === undefined) {
memoryCache.rootFileNames.push(fileName)
}

memoryCache.fileVersions.set(fileName, fileVersion + 1)
memoryCache.fileContents.set(fileName, contents)
// Update program when file changes.
if (sourceFile === undefined || sourceFile.text !== contents) {
builderProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram(
memoryCache.rootFileNames.slice(),
config.options,
host,
builderProgram,
config.errors,
config.projectReferences
)
}
}

getOutput = (code: string, fileName: string) => {
updateMemoryCache(code, fileName)
const output: [string, string] = ['', '']

const output = service.getEmitOutput(fileName)
updateMemoryCache(code, fileName)

// Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`.
const diagnostics = service.getSemanticDiagnostics(fileName)
.concat(service.getSyntacticDiagnostics(fileName))
const sourceFile = builderProgram.getSourceFile(fileName)
if (!sourceFile) throw new TypeError(`Unable to read file: ${fileName}`)

const diagnostics = ts.getPreEmitDiagnostics(builderProgram.getProgram(), sourceFile)
const diagnosticList = filterDiagnostics(diagnostics, ignoreDiagnostics)

if (diagnosticList.length) reportTSError(diagnosticList)

if (output.emitSkipped) {
const result = builderProgram.emit(sourceFile, (path, file) => {
if (path.endsWith('.map')) {
output[1] = file
} else {
output[0] = file
}
}, undefined, undefined, getCustomTransformers())

if (result.emitSkipped) {
throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`)
}

// Throw an error when requiring `.d.ts` files.
if (output.outputFiles.length === 0) {
if (output[0] === '') {
throw new TypeError(
'Unable to require `.d.ts` file.\n' +
'This is usually the result of a faulty configuration or import. ' +
Expand All @@ -370,17 +397,34 @@ export function register (opts: Options = {}): Register {
)
}

return [output.outputFiles[1].text, output.outputFiles[0].text]
return output
}

getTypeInfo = (code: string, fileName: string, position: number) => {
updateMemoryCache(code, fileName)

const info = service.getQuickInfoAtPosition(fileName, position)
const name = ts.displayPartsToString(info ? info.displayParts : [])
const comment = ts.displayPartsToString(info ? info.documentation : [])
const sourceFile = builderProgram.getSourceFile(fileName)
if (!sourceFile) throw new TypeError(`Unable to read file: ${fileName}`)

const node = getTokenAtPosition(ts, sourceFile, position)
const checker = builderProgram.getProgram().getTypeChecker()
const type = checker.getTypeAtLocation(node)
const documentation = type.symbol ? type.symbol.getDocumentationComment(checker) : []

return { name, comment }
// Invalid type.
if (!type.symbol) return { name: '', comment: '' }

return {
name: checker.typeToString(type),
comment: ts.displayPartsToString(documentation)
}
}

if (config.options.incremental) {
process.on('exit', () => {
// Emits `.tsbuildinfo` to filesystem.
(builderProgram.getProgram() as any).emitBuildInfo()
})
}
} else {
if (typeof transformers === 'function') {
Expand Down Expand Up @@ -507,8 +551,6 @@ function fixConfig (ts: TSCommon, config: _ts.ParsedCommandLine) {
delete config.options.declarationDir
delete config.options.declarationMap
delete config.options.emitDeclarationOnly
delete config.options.tsBuildInfoFile
delete config.options.incremental

// Target ES5 output by default (instead of ES3).
if (config.options.target === undefined) {
Expand Down Expand Up @@ -598,6 +640,30 @@ function updateSourceMap (sourceMapText: string, fileName: string) {
/**
* Filter diagnostics.
*/
function filterDiagnostics (diagnostics: _ts.Diagnostic[], ignore: number[]) {
function filterDiagnostics (diagnostics: readonly _ts.Diagnostic[], ignore: number[]) {
return diagnostics.filter(x => ignore.indexOf(x.code) === -1)
}

/**
* Get token at file position.
*
* Reference: https://github.com/microsoft/TypeScript/blob/fcd9334f57d85b73dd66ad2d21c02e84822f4841/src/services/utilities.ts#L705-L731
*/
function getTokenAtPosition (ts: typeof _ts, sourceFile: _ts.SourceFile, position: number): _ts.Node {
let current: _ts.Node = sourceFile

outer: while (true) {
for (const child of current.getChildren(sourceFile)) {
const start = child.getFullStart()
if (start > position) break

const end = child.getEnd()
if (position <= end) {
current = child
continue outer
}
}

return current
}
}

0 comments on commit cb4b748

Please sign in to comment.