diff --git a/.travis.yml b/.travis.yml index 7674badb1..e63194f85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/README.md b/README.md index 737fd694a..5e5afdbde 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index d00eb3f99..c4974fb7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,9 +23,9 @@ "dev": true }, "@types/node": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", - "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "version": "12.7.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz", + "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==", "dev": true }, "@types/proxyquire": { @@ -1668,9 +1668,9 @@ "dev": true }, "typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", - "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", "dev": true }, "ua-parser-js": { diff --git a/package.json b/package.json index 9dba034b9..d5ae6bcbe 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "prepare": "npm run build" }, "engines": { - "node": ">=4.2.0" + "node": ">=6.0.0" }, "repository": { "type": "git", @@ -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", @@ -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", diff --git a/src/bin.ts b/src/bin.ts index dd6e01586..79f700278 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -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() } }) diff --git a/src/index.ts b/src/index.ts index f9500ce5a..f246847e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,11 +79,8 @@ export interface Options { */ class MemoryCache { fileContents = new Map() - fileVersions = new Map() - constructor (public rootFileNames: string[] = []) { - for (const fileName of rootFileNames) this.fileVersions.set(fileName, 1) - } + constructor (public rootFileNames: string[]) {} } /** @@ -122,7 +119,7 @@ const TS_NODE_COMPILER_OPTIONS = { inlineSources: true, declaration: false, noEmit: false, - outDir: '$$ts-node$$' + outDir: '.ts-node' } /** @@ -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. ' + @@ -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') { @@ -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) { @@ -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 + } +}