diff --git a/package-lock.json b/package-lock.json index 340f86d..69172fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,6 +101,11 @@ "object-keys": "^1.0.8" } }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, "es-abstract": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", @@ -455,6 +460,28 @@ "strip-bom": "^2.0.0" } }, + "ts-node": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.0.tgz", + "integrity": "sha512-klJsfswHP0FuOLsvBZ/zzCfUvakOSSxds78mVeK7I+qP76YWtxf16hEZsp3U+b0kIo82R5UatGFeblYMqabb2Q==", + "requires": { + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "typescript": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.1.tgz", diff --git a/package.json b/package.json index f5fbf67..ff19084 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,9 @@ "@types/node": "^10.3.0" }, "dependencies": { - "arrify": "^1.0.1", - "make-error": "^1.3.4", - "mkdirp": "^0.5.1", "smartfile": "^4.2.28", - "source-map-support": "^0.5.6", - "typescript": "^2.9.1", - "yn": "^2.0.0" + "ts-node": "^7.0.0", + "typescript": "^2.9.1" }, "private": false } diff --git a/ts/index.ts b/ts/index.ts index 36301bc..37f7b79 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import * as tsNode from './tsnode'; +import * as tsNode from 'ts-node'; tsNode.register({ compilerOptions: { diff --git a/ts/tsnode.ts b/ts/tsnode.ts deleted file mode 100644 index 2f412eb..0000000 --- a/ts/tsnode.ts +++ /dev/null @@ -1,613 +0,0 @@ -import { relative, basename, extname, resolve, dirname, join } from 'path' -import { readFileSync, writeFileSync } from 'fs' -import { EOL, tmpdir, homedir } from 'os' -import sourceMapSupport = require('source-map-support') -import mkdirp = require('mkdirp') -import crypto = require('crypto') -import yn = require('yn') -import arrify = require('arrify') -import bufferFrom = require('buffer-from') -import { BaseError } from 'make-error' -import * as util from 'util' -import * as _ts from 'typescript' - -/** - * @internal - */ -export const INSPECT_CUSTOM = util.inspect.custom || 'inspect' - -/** - * Debugging `ts-node`. - */ -const shouldDebug = yn(process.env.TS_NODE_DEBUG) -const debug = shouldDebug ? console.log.bind(console, 'ts-node') : () => undefined -const debugFn = shouldDebug ? - (key: string, fn: (arg: T) => U) => { - return (x: T) => { - debug(key, x) - return fn(x) - } - } : - (_: string, fn: (arg: T) => U) => fn - -/** - * Common TypeScript interfaces between versions. - */ -export interface TSCommon { - version: typeof _ts.version - 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 - transpileModule: typeof _ts.transpileModule - ModuleKind: typeof _ts.ModuleKind - ScriptTarget: typeof _ts.ScriptTarget - findConfigFile: typeof _ts.findConfigFile - readConfigFile: typeof _ts.readConfigFile - parseJsonConfigFileContent: typeof _ts.parseJsonConfigFileContent - formatDiagnostics: typeof _ts.formatDiagnostics - formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext -} - -/** - * Export the current version. - */ -export const VERSION = require('../package.json').version - -/** - * Registration options. - */ -export interface Options { - pretty?: boolean | null - typeCheck?: boolean | null - transpileOnly?: boolean | null - files?: boolean | null - cache?: boolean | null - cacheDirectory?: string - compiler?: string - ignore?: string | string[] - project?: string - skipIgnore?: boolean | null - skipProject?: boolean | null - compilerOptions?: object - ignoreDiagnostics?: number | string | Array - readFile?: (path: string) => string | undefined - fileExists?: (path: string) => boolean - transformers?: _ts.CustomTransformers -} - -/** - * Track the project information. - */ -interface MemoryCache { - contents: { [path: string]: string | undefined } - versions: { [path: string]: number | undefined } - outputs: { [path: string]: string } -} - -/** - * Information retrieved from type info check. - */ -export interface TypeInfo { - name: string - comment: string -} - -/** - * Default register options. - */ -export const DEFAULTS: Options = { - files: yn(process.env['TS_NODE_FILES']), - cache: yn(process.env['TS_NODE_CACHE'], { default: true }), - pretty: yn(process.env['TS_NODE_PRETTY']), - cacheDirectory: process.env['TS_NODE_CACHE_DIRECTORY'], - compiler: process.env['TS_NODE_COMPILER'], - compilerOptions: parse(process.env['TS_NODE_COMPILER_OPTIONS']), - ignore: split(process.env['TS_NODE_IGNORE']), - project: process.env['TS_NODE_PROJECT'], - skipIgnore: yn(process.env['TS_NODE_SKIP_IGNORE']), - skipProject: yn(process.env['TS_NODE_SKIP_PROJECT']), - ignoreDiagnostics: split(process.env['TS_NODE_IGNORE_DIAGNOSTICS']), - typeCheck: yn(process.env['TS_NODE_TYPE_CHECK']), - transpileOnly: yn(process.env['TS_NODE_TRANSPILE_ONLY']) -} - -/** - * Default TypeScript compiler options required by `ts-node`. - */ -const DEFAULT_COMPILER_OPTIONS = { - sourceMap: true, - inlineSourceMap: false, - inlineSources: true, - declaration: false, - noEmit: false, - outDir: '$$ts-node$$' -} - -/** - * Split a string array of values. - */ -export function split (value: string | undefined) { - return typeof value === 'string' ? value.split(/ *, */g) : undefined -} - -/** - * Parse a string as JSON. - */ -export function parse (value: string | undefined): object | undefined { - return typeof value === 'string' ? JSON.parse(value) : undefined -} - -/** - * Replace backslashes with forward slashes. - */ -export function normalizeSlashes (value: string): string { - return value.replace(/\\/g, '/') -} - -/** - * TypeScript diagnostics error. - */ -export class TSError extends BaseError { - name = 'TSError' - - constructor (public diagnosticText: string, public diagnosticCodes: number[]) { - super(`тип Unable to compile TypeScript:\n${diagnosticText}`) - } - - /** - * @internal - */ - [INSPECT_CUSTOM] () { - return this.diagnosticText - } -} - -/** - * Return type for registering `ts-node`. - */ -export interface Register { - cwd: string - extensions: string[] - cachedir: string - ts: TSCommon - compile (code: string, fileName: string, lineOffset?: number): string - getTypeInfo (code: string, fileName: string, position: number): TypeInfo -} - -/** - * Return a default temp directory based on home directory of user. - */ -function getTmpDir (): string { - const hash = crypto.createHash('sha256').update(homedir(), 'utf8').digest('hex') - - return join(tmpdir(), `ts-node-${hash}`) -} - -/** - * Register TypeScript compiler. - */ -export function register (opts: Options = {}): Register { - const options = Object.assign({}, DEFAULTS, opts) - const cacheDirectory = options.cacheDirectory || getTmpDir() - const originalJsHandler = require.extensions['.js'] - - const ignoreDiagnostics = arrify(options.ignoreDiagnostics).concat([ - 6059, // "'rootDir' is expected to contain all source files." - 18002, // "The 'files' list in config file is empty." - 18003 // "No inputs were found in config file." - ]).map(Number) - - const memoryCache: MemoryCache = { - contents: Object.create(null), - versions: Object.create(null), - outputs: Object.create(null) - } - - const ignore = options.skipIgnore ? [] : arrify( - options.ignore || '/node_modules/' - ).map(str => new RegExp(str)) - - // Install source map support and read from memory cache. - sourceMapSupport.install({ - environment: 'node', - retrieveFile (path: string) { - return memoryCache.outputs[path] - } - }) - - // Require the TypeScript compiler and configuration. - const cwd = process.cwd() - const { compilerOptions, project, skipProject } = options - const compiler = options.compiler || 'typescript' - const typeCheck = options.typeCheck === true || options.transpileOnly !== true - const ts: typeof _ts = require(compiler) - const transformers = options.transformers || undefined - const readFile = options.readFile || ts.sys.readFile - const fileExists = options.fileExists || ts.sys.fileExists - const config = readConfig(cwd, ts, fileExists, readFile, compilerOptions, project, skipProject) - const configDiagnosticList = filterDiagnostics(config.errors, ignoreDiagnostics) - const extensions = ['.ts', '.tsx'] - const fileNames = options.files ? config.fileNames : [] - - const cachedir = join( - resolve(cwd, cacheDirectory), - getCompilerDigest({ - version: ts.version, - options: config.options, - fileNames, - typeCheck, - ignoreDiagnostics, - compiler - }) - ) - - const diagnosticHost: _ts.FormatDiagnosticsHost = { - getNewLine: () => EOL, - getCurrentDirectory: () => cwd, - getCanonicalFileName: (path) => path - } - - const formatDiagnostics = options.pretty - ? ts.formatDiagnosticsWithColorAndContext - : ts.formatDiagnostics - - function createTSError (diagnostics: ReadonlyArray<_ts.Diagnostic>) { - const diagnosticText = formatDiagnostics(diagnostics, diagnosticHost) - const diagnosticCodes = diagnostics.map(x => x.code) - return new TSError(diagnosticText, diagnosticCodes) - } - - // Render the configuration errors and exit the script. - if (configDiagnosticList.length) throw createTSError(configDiagnosticList) - - // Enable `allowJs` when flag is set. - if (config.options.allowJs) { - extensions.push('.js') - extensions.push('.jsx') - } - - // Initialize files from TypeScript into project. - for (const path of fileNames) memoryCache.versions[path] = 1 - - /** - * Get the extension for a transpiled file. - */ - const getExtension = config.options.jsx === ts.JsxEmit.Preserve ? - ((path: string) => /\.[tj]sx$/.test(path) ? '.jsx' : '.js') : - ((_: string) => '.js') - - /** - * Create the basic required function using transpile mode. - */ - let getOutput = function (code: string, fileName: string, lineOffset = 0): SourceOutput { - const result = ts.transpileModule(code, { - fileName, - transformers, - compilerOptions: config.options, - reportDiagnostics: true - }) - - const diagnosticList = result.diagnostics ? - filterDiagnostics(result.diagnostics, ignoreDiagnostics) : - [] - - if (diagnosticList.length) throw createTSError(diagnosticList) - - return [result.outputText, result.sourceMapText as string] - } - - let getTypeInfo = function (_code: string, _fileName: string, _position: number): TypeInfo { - throw new TypeError(`Type information is unavailable without "--type-check"`) - } - - // Use full language services when the fast option is disabled. - if (typeCheck) { - // Set the file contents into cache. - const updateMemoryCache = function (code: string, fileName: string) { - if (memoryCache.contents[fileName] !== code) { - memoryCache.contents[fileName] = code - memoryCache.versions[fileName] = (memoryCache.versions[fileName] || 0) + 1 - } - } - - // Create the compiler host for type checking. - const serviceHost = { - getScriptFileNames: () => Object.keys(memoryCache.versions), - getScriptVersion: (fileName: string) => { - const version = memoryCache.versions[fileName] - - // We need to return `undefined` and not a string here because TypeScript will use - // `getScriptVersion` and compare against their own version - which can be `undefined`. - // If we don't return `undefined` it results in `undefined === "undefined"` and run - // `createProgram` again (which is very slow). Using a `string` assertion here to avoid - // TypeScript errors from the function signature (expects `(x: string) => string`). - return version === undefined ? undefined as any as string : String(version) - }, - getScriptSnapshot (fileName: string) { - // Read contents into TypeScript memory cache. - if (!Object.prototype.hasOwnProperty.call(memoryCache.contents, fileName)) { - memoryCache.contents[fileName] = readFile(fileName) - } - - const contents = memoryCache.contents[fileName] - if (contents === undefined) return - return ts.ScriptSnapshot.fromString(contents) - }, - fileExists: debugFn('fileExists', fileExists), - readFile: debugFn('readFile', readFile), - readDirectory: debugFn('readDirectory', ts.sys.readDirectory), - getDirectories: debugFn('getDirectories', ts.sys.getDirectories), - directoryExists: debugFn('directoryExists', ts.sys.directoryExists), - getNewLine: () => EOL, - getCurrentDirectory: () => cwd, - getCompilationSettings: () => config.options, - getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options), - getCustomTransformers: () => transformers - } - - const service = ts.createLanguageService(serviceHost) - - getOutput = function (code: string, fileName: string, lineOffset: number = 0) { - // Must set memory cache before attempting to read file. - updateMemoryCache(code, fileName) - - const output = service.getEmitOutput(fileName) - - // Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`. - const diagnostics = service.getCompilerOptionsDiagnostics() - .concat(service.getSyntacticDiagnostics(fileName)) - .concat(service.getSemanticDiagnostics(fileName)) - - const diagnosticList = filterDiagnostics(diagnostics, ignoreDiagnostics) - - if (diagnosticList.length) throw createTSError(diagnosticList) - - if (output.emitSkipped) { - throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`) - } - - // Throw an error when requiring `.d.ts` files. - if (output.outputFiles.length === 0) { - return ['', '']; - } - return [output.outputFiles[1].text, output.outputFiles[0].text] - } - - getTypeInfo = function (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 : []) - - return { name, comment } - } - } - - const compile = readThrough(cachedir, options.cache === true, memoryCache, getOutput, getExtension) - const register: Register = { cwd, compile, getTypeInfo, extensions, cachedir, ts } - - // Register the extensions. - extensions.forEach(extension => { - registerExtension(extension, ignore, register, originalJsHandler) - }) - - return register -} - -/** - * Check if the filename should be ignored. - */ -function shouldIgnore (filename: string, ignore: RegExp[]) { - const relname = normalizeSlashes(filename) - - return ignore.some(x => x.test(relname)) -} - -/** - * Register the extension for node. - */ -function registerExtension ( - ext: string, - ignore: RegExp[], - register: Register, - originalHandler: (m: NodeModule, filename: string) => any -) { - const old = require.extensions[ext] || originalHandler - - require.extensions[ext] = function (m: any, filename) { - if (shouldIgnore(filename, ignore)) { - return old(m, filename) - } - - const _compile = m._compile - - m._compile = function (code: string, fileName: string) { - debug('module._compile', fileName) - - return _compile.call(this, register.compile(code, fileName), fileName) - } - - return old(m, filename) - } -} - -/** - * Do post-processing on config options to support `ts-node`. - */ -function fixConfig (ts: TSCommon, config: _ts.ParsedCommandLine) { - // Delete options that *should not* be passed through. - delete config.options.out - delete config.options.outFile - delete config.options.declarationDir - delete config.options.declarationMap - delete config.options.emitDeclarationOnly - - // Target ES5 output by default (instead of ES3). - if (config.options.target === undefined) { - config.options.target = ts.ScriptTarget.ES5 - } - - // Target CommonJS modules by default (instead of magically switching to ES6 when the target is ES6). - if (config.options.module === undefined) { - config.options.module = ts.ModuleKind.CommonJS - } - - return config -} - -/** - * Load TypeScript configuration. - */ -function readConfig ( - cwd: string, - ts: TSCommon, - fileExists: (path: string) => boolean, - readFile: (path: string) => string | undefined, - compilerOptions?: object, - project?: string | null, - noProject?: boolean | null -): _ts.ParsedCommandLine { - let config = { compilerOptions: {} } - let basePath = normalizeSlashes(cwd) - let configFileName: string | undefined = undefined - - // Read project configuration when available. - if (!noProject) { - configFileName = project - ? normalizeSlashes(resolve(cwd, project)) - : ts.findConfigFile(normalizeSlashes(cwd), fileExists) - - if (configFileName) { - const result = ts.readConfigFile(configFileName, readFile) - - // Return diagnostics. - if (result.error) { - return { errors: [result.error], fileNames: [], options: {} } - } - - config = result.config - basePath = normalizeSlashes(dirname(configFileName)) - } - } - - // Override default configuration options `ts-node` requires. - config.compilerOptions = Object.assign({}, config.compilerOptions, compilerOptions, DEFAULT_COMPILER_OPTIONS) - - return fixConfig(ts, ts.parseJsonConfigFileContent(config, ts.sys, basePath, undefined, configFileName)) -} - -/** - * Internal source output. - */ -type SourceOutput = [string, string] - -/** - * Wrap the function with caching. - */ -function readThrough ( - cachedir: string, - shouldCache: boolean, - memoryCache: MemoryCache, - compile: (code: string, fileName: string, lineOffset?: number) => SourceOutput, - getExtension: (fileName: string) => string -) { - if (shouldCache === false) { - return function (code: string, fileName: string, lineOffset?: number) { - debug('readThrough', fileName) - - const [value, sourceMap] = compile(code, fileName, lineOffset) - const output = updateOutput(value, fileName, sourceMap, getExtension) - - memoryCache.outputs[fileName] = output - - return output - } - } - - // Make sure the cache directory exists before continuing. - mkdirp.sync(cachedir) - - return function (code: string, fileName: string, lineOffset?: number) { - debug('readThrough', fileName) - - const cachePath = join(cachedir, getCacheName(code, fileName)) - const extension = getExtension(fileName) - const outputPath = `${cachePath}${extension}` - - try { - const output = readFileSync(outputPath, 'utf8') - if (isValidCacheContent(output)) { - memoryCache.outputs[fileName] = output - return output - } - } catch (err) {/* Ignore. */} - - const [value, sourceMap] = compile(code, fileName, lineOffset) - const output = updateOutput(value, fileName, sourceMap, getExtension) - - memoryCache.outputs[fileName] = output - writeFileSync(outputPath, output) - - return output - } -} - -/** - * Update the output remapping the source map. - */ -function updateOutput (outputText: string, fileName: string, sourceMap: string, getExtension: (fileName: string) => string) { - const base64Map = bufferFrom(updateSourceMap(sourceMap, fileName), 'utf8').toString('base64') - const sourceMapContent = `data:application/json;charset=utf-8;base64,${base64Map}` - const sourceMapLength = `${basename(fileName)}.map`.length + (getExtension(fileName).length - extname(fileName).length) - - return outputText.slice(0, -sourceMapLength) + sourceMapContent -} - -/** - * Update the source map contents for improved output. - */ -function updateSourceMap (sourceMapText: string, fileName: string) { - const sourceMap = JSON.parse(sourceMapText) - sourceMap.file = fileName - sourceMap.sources = [fileName] - delete sourceMap.sourceRoot - return JSON.stringify(sourceMap) -} - -/** - * Get the file name for the cache entry. - */ -function getCacheName (sourceCode: string, fileName: string) { - return crypto.createHash('sha256') - .update(extname(fileName), 'utf8') - .update('\x00', 'utf8') - .update(sourceCode, 'utf8') - .digest('hex') -} - -/** - * Ensure the given cached content is valid by sniffing for a base64 encoded '}' - * at the end of the content, which should exist if there is a valid sourceMap present. - */ -function isValidCacheContent (contents: string) { - return /(?:9|0=|Q==)$/.test(contents.slice(-3)) -} - -/** - * Create a hash of the current configuration. - */ -function getCompilerDigest (obj: object) { - return crypto.createHash('sha256').update(JSON.stringify(obj), 'utf8').digest('hex') -} - -/** - * Filter diagnostics. - */ -function filterDiagnostics (diagnostics: _ts.Diagnostic[], ignore: number[]) { - return diagnostics.filter(x => ignore.indexOf(x.code) === -1) -}