diff --git a/README.md b/README.md index 5bf0568ef..4430270ec 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ ts-node --compiler ntypescript --project src --ignoreWarnings 2304 hello-world.t * **--ignoreWarnings, -i** Set an array of TypeScript diagnostic codes to ignore (also `process.env.TS_NODE_IGNORE_WARNINGS`) * **--disableWarnings, -d** Ignore all TypeScript errors (also `process.env.TS_NODE_DISABLE_WARNINGS`) * **--compilerOptions, -o** Set compiler options using JSON (E.g. `--compilerOptions '{"target":"es6"}'`) (also `process.env.TS_NODE_COMPILER_OPTIONS`) +* **--fast, -f** Use TypeScript's `transpileModule` mode (no type checking, but faster compilation) (also `process.env.TS_NODE_FAST`) ### Programmatic Usage diff --git a/src/bin.ts b/src/bin.ts index d4534b07e..43871e381 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -8,11 +8,12 @@ import minimist = require('minimist') import chalk = require('chalk') import { diffLines } from 'diff' import { createScript } from 'vm' -import { register, VERSION, getFile, getVersion, TSError } from './index' +import { register, VERSION, getFile, getVersion, getFileExists, TSError } from './index' interface Argv { eval?: string print?: string + fast?: boolean version?: boolean help?: boolean compiler?: string @@ -25,10 +26,11 @@ interface Argv { } const strings = ['eval', 'print', 'compiler', 'project', 'ignoreWarnings'] -const booleans = ['help', 'version', 'disableWarnings', 'noProject'] +const booleans = ['help', 'fast', 'version', 'disableWarnings', 'noProject'] const aliases: { [key: string]: string[] } = { help: ['h'], + fast: ['f'], version: ['v'], eval: ['e'], print: ['p'], @@ -45,6 +47,11 @@ let stop = process.argv.length function isFlagOnly (arg: string) { const name = arg.replace(/^--?/, '') + // The value is part of this argument. + if (/=/.test(name)) { + return true + } + for (const bool of booleans) { if (name === bool) { return true @@ -139,6 +146,8 @@ const isPrinted = argv.print != null const service = register({ getFile: isEval ? getFileEval : getFile, getVersion: isEval ? getVersionEval : getVersion, + getFileExists: isEval ? getFileExistsEval : getFileExists, + fast: argv.fast, compiler: argv.compiler, ignoreWarnings: list(argv.ignoreWarnings), project: argv.project, @@ -360,3 +369,10 @@ function getFileEval (fileName: string) { function getVersionEval (fileName: string) { return fileName === EVAL_PATH ? String(evalFile.version) : getVersion(fileName) } + +/** + * Get whether the file exists. + */ +function getFileExistsEval (fileName: string) { + return fileName === EVAL_PATH ? true : getFileExists(fileName) +} diff --git a/src/index.ts b/src/index.ts index 0c34fc856..0c5b96f88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,14 +15,14 @@ const oldHandlers: { [key: string]: any } = {} * Common TypeScript interfaces between versions. */ export interface TSCommon { - sys: any + sys: typeof TS.sys ScriptSnapshot: typeof TS.ScriptSnapshot displayPartsToString: typeof TS.displayPartsToString createLanguageService: typeof TS.createLanguageService getDefaultLibFilePath: typeof TS.getDefaultLibFilePath getPreEmitDiagnostics: typeof TS.getPreEmitDiagnostics flattenDiagnosticMessageText: typeof TS.flattenDiagnosticMessageText - transpile: typeof TS.transpile + transpileModule: typeof TS.transpileModule // TypeScript 1.5+, 1.7+ added `fileExists` parameter. findConfigFile (path: string, fileExists?: (path: string) => boolean): string @@ -55,6 +55,7 @@ export const VERSION = pkg.version * Registration options. */ export interface Options { + fast?: boolean compiler?: string noProject?: boolean project?: string @@ -62,6 +63,7 @@ export interface Options { disableWarnings?: boolean getFile?: (fileName: string) => string getVersion?: (fileName: string) => string + getFileExists?: (fileName: string) => boolean compilerOptions?: any } @@ -80,7 +82,7 @@ function readConfig (options: Options, cwd: string, ts: TSCommon) { } if (result.error) { - throw new TSError([formatDiagnostic(result.error, ts)]) + throw new TSError([formatDiagnostic(result.error, ts, cwd)]) } result.config.compilerOptions = extend( @@ -118,17 +120,27 @@ interface Project { version: number } +/** + * Information retrieved from type info check. + */ +export interface TypeInfo { + name: string + comment: string +} + /** * Default register options. */ const DEFAULT_OPTIONS: Options = { getFile, getVersion, + getFileExists, disableWarnings: process.env.TS_NODE_DISABLE_WARNINGS, compiler: process.env.TS_NODE_COMPILER, project: process.env.TS_NODE_PROJECT || process.cwd(), noProject: process.env.TS_NODE_NO_PROJECT, - ignoreWarnings: process.env.TS_NODE_IGNORE_WARNINGS + ignoreWarnings: process.env.TS_NODE_IGNORE_WARNINGS, + fast: process.env.TS_NODE_FAST } /** @@ -148,12 +160,24 @@ export function register (opts?: Options) { JSON.parse(options.compilerOptions) : options.compilerOptions + // Install source map support and read from cache. + sourceMapSupport.install({ + environment: 'node', + retrieveFile (fileName: string) { + if (project.files[fileName]) { + return getOutput(fileName) + } + } + } as any) + + // Require the TypeScript compiler and configuration. const ts: typeof TS = require(options.compiler) const config = readConfig(options, cwd, ts) + const diagnostics = formatDiagnostics(config.errors, cwd, ts, options) // Render the configuration errors and exit the script. - if (!options.disableWarnings && config.errors.length) { - throw new TSError(config.errors.map((d: any) => formatDiagnostic(d, ts))) + if (diagnostics.length) { + throw new TSError(diagnostics) } // Add all files into the file hash. @@ -161,79 +185,118 @@ export function register (opts?: Options) { project.files[fileName] = true } - const serviceHost = { - getScriptFileNames: () => Object.keys(project.files), - getProjectVersion: () => String(project.version), - getScriptVersion: (fileName: string) => versionFile(fileName), - getScriptSnapshot (fileName: string) { - const contents = options.getFile(fileName) + /** + * Create the basic required function using transpile mode. + */ + let getOutput = function (fileName: string) { + const contents = options.getFile(fileName) + const { outputText, diagnostics } = ts.transpileModule(contents, { + compilerOptions: config.options, + fileName, + reportDiagnostics: true + }) - return contents == null ? undefined : ts.ScriptSnapshot.fromString(contents) - }, - getNewLine: () => EOL, - getCurrentDirectory: () => cwd, - getCompilationSettings: () => config.options, - getDefaultLibFileName: (options: any) => ts.getDefaultLibFilePath(config.options) + const diagnosticList = diagnostics ? formatDiagnostics(diagnostics, cwd, ts, options) : [] + + if (diagnosticList.length) { + throw new TSError(diagnosticList) + } + + return outputText } - const service = ts.createLanguageService(serviceHost) + let compile = function (fileName: string) { + return getOutput(fileName) + } - // Install source map support and read from cache. - sourceMapSupport.install({ - environment: 'node', - retrieveFile (fileName: string) { - if (project.files[fileName]) { - return getOutput(fileName) - } + let getTypeInfo = function (fileName: string, position: number): TypeInfo { + throw new TypeError(`No type information available under "--fast" mode`) + } + + /** + * Use language services when the fast option is disabled. + */ + if (!options.fast) { + const serviceHost = { + getScriptFileNames: () => Object.keys(project.files), + getProjectVersion: () => String(project.version), + getScriptVersion: (fileName: string) => versionFile(fileName), + getScriptSnapshot (fileName: string) { + if (!options.getFileExists(fileName)) { + return undefined + } + + return ts.ScriptSnapshot.fromString(options.getFile(fileName)) + }, + getNewLine: () => EOL, + getCurrentDirectory: () => cwd, + getCompilationSettings: () => config.options, + getDefaultLibFileName: (options: any) => ts.getDefaultLibFilePath(config.options) } - } as any) - function addAndVersionFile (fileName: string) { - // Add files to the hash before compilation. - project.files[fileName] = true + const service = ts.createLanguageService(serviceHost) + + const addAndVersionFile = function (fileName: string) { + // Add files to the hash before compilation. + project.files[fileName] = true + + const currentVersion = project.versions[fileName] + const newVersion = versionFile(fileName) + + // Increment the project version for file changes. + if (currentVersion !== newVersion) { + project.version++ + } - const currentVersion = project.versions[fileName] - const newVersion = versionFile(fileName) + return newVersion + } - // Increment the project version for file changes. - if (currentVersion !== newVersion) { - project.version++ + const versionFile = function (fileName: string) { + const version = options.getVersion(fileName) + project.versions[fileName] = version + return version } - return newVersion - } + getOutput = function (fileName: string) { + const output = service.getEmitOutput(fileName) - function versionFile (fileName: string) { - const version = options.getVersion(fileName) - project.versions[fileName] = version - return version - } + // Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`. + const diagnostics = service.getCompilerOptionsDiagnostics() + .concat(service.getSyntacticDiagnostics(fileName)) + .concat(service.getSemanticDiagnostics(fileName)) - function getOutput (fileName: string) { - const output = service.getEmitOutput(fileName) - const diagnostics = getDiagnostics(service, fileName, options, ts) + const diagnosticList = formatDiagnostics(diagnostics, cwd, ts, options) - if (output.emitSkipped) { - diagnostics.push(`${relative(cwd, fileName)}: Emit skipped`) + if (output.emitSkipped) { + diagnosticList.push(`${relative(cwd, fileName)}: Emit skipped`) + } + + if (diagnosticList.length) { + throw new TSError(diagnosticList) + } + + return output.outputFiles[0].text } - if (diagnostics.length) { - throw new TSError(diagnostics) + compile = function (fileName: string) { + addAndVersionFile(fileName) + + return getOutput(fileName) } - return output.outputFiles[0].text - } + getTypeInfo = function (fileName: string, position: number) { + addAndVersionFile(fileName) - function compile (fileName: string) { - addAndVersionFile(fileName) + const info = service.getQuickInfoAtPosition(fileName, position) + const name = ts.displayPartsToString(info ? info.displayParts : []) + const comment = ts.displayPartsToString(info ? info.documentation : []) - return getOutput(fileName) + return { name, comment } + } } function loader (m: any, fileName: string) { - addAndVersionFile(fileName) - - return m._compile(getOutput(fileName), fileName) + return m._compile(compile(fileName), fileName) } function shouldIgnore (filename: string) { @@ -254,16 +317,6 @@ export function register (opts?: Options) { } } - function getTypeInfo (fileName: string, position: number) { - addAndVersionFile(fileName) - - const info = service.getQuickInfoAtPosition(fileName, position) - const name = ts.displayPartsToString(info ? info.displayParts : []) - const comment = ts.displayPartsToString(info ? info.documentation : []) - - return { name, comment } - } - registerExtension('.ts') registerExtension('.tsx') @@ -282,39 +335,46 @@ export function getVersion (fileName: string): string { } /** - * Get the file from the file system. + * Check if the file exists. */ -export function getFile (fileName: string): string { +export function getFileExists (fileName: string): boolean { try { - return readFileSync(fileName, 'utf8') - } catch (error) { - return + const stats = statSync(fileName) + + return stats.isFile() || stats.isFIFO() + } catch (err) { + return false } } /** - * Get file diagnostics from a TypeScript language service. + * Get the file from the file system. + */ +export function getFile (fileName: string): string { + return readFileSync(fileName, 'utf8') +} + +/** + * Format an array of diagnostics. */ -function getDiagnostics (service: TS.LanguageService, fileName: string, options: Options, ts: TSCommon) { +function formatDiagnostics (diagnostics: TS.Diagnostic[], cwd: string, ts: TSCommon, options: Options) { if (options.disableWarnings) { return [] } - return service.getCompilerOptionsDiagnostics() - .concat(service.getSyntacticDiagnostics(fileName)) - .concat(service.getSemanticDiagnostics(fileName)) + return diagnostics .filter(function (diagnostic) { return options.ignoreWarnings.indexOf(diagnostic.code) === -1 }) .map(function (diagnostic) { - return formatDiagnostic(diagnostic, ts) + return formatDiagnostic(diagnostic, ts, cwd) }) } /** * Format a diagnostic object into a string. */ -function formatDiagnostic (diagnostic: any, ts: TSCommon, cwd: string = '.'): string { +function formatDiagnostic (diagnostic: TS.Diagnostic, ts: TSCommon, cwd: string): string { const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') if (diagnostic.file) {