diff --git a/README.md b/README.md index d569d561e..e60b40e3a 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ You can require `ts-node` and register the loader for future requires by using ` **Note:** If you need to use advanced node.js CLI arguments (e.g. `--inspect`), use them with `node -r ts-node/register` instead of the `ts-node` CLI. +#### Developers + +**TS Node** exports a `create()` function that can be used to initialize a TypeScript compiler that isn't registered to `require.extensions`, and it uses the same code as `register`. + ### Mocha ```sh @@ -118,7 +122,7 @@ ts-node --compiler ntypescript --project src/tsconfig.json hello-world.ts ### CLI Options -Supports `--print`, `--eval` and `--require` from [node.js CLI options](https://nodejs.org/api/cli.html). +Supports `--print`, `--eval`, `--require` and `--interactive` similar to the [node.js CLI options](https://nodejs.org/api/cli.html). * `--help` Prints help text * `--version` Prints version information @@ -133,13 +137,15 @@ _Environment variable denoted in parentheses._ * `-C, --compiler [name]` Specify a custom TypeScript compiler (`TS_NODE_COMPILER`, default: `typescript`) * `-D, --ignore-diagnostics [code]` Ignore TypeScript warnings by diagnostic code (`TS_NODE_IGNORE_DIAGNOSTICS`) * `-O, --compiler-options [opts]` JSON object to merge with compiler options (`TS_NODE_COMPILER_OPTIONS`) +* `--dir` Specify working directory for config resolution (`TS_NODE_CWD`, default: `process.cwd()`) +* `--scope` Scope compiler to files within `cwd` (`TS_NODE_SCOPE`, default: `false`) * `--files` Load files from `tsconfig.json` on startup (`TS_NODE_FILES`, default: `false`) * `--pretty` Use pretty diagnostic formatter (`TS_NODE_PRETTY`, default: `false`) * `--skip-project` Skip project config resolution and loading (`TS_NODE_SKIP_PROJECT`, default: `false`) * `--skip-ignore` Skip ignore checks (`TS_NODE_SKIP_IGNORE`, default: `false`) -* `--log-error` Logs errors of types instead of exit the process (`TS_NODE_LOG_ERROR`, default: `false`) * `--build` Emit output files into `.ts-node` directory (`TS_NODE_BUILD`, default: `false`) * `--prefer-ts-exts` Re-order file extensions so that TypeScript imports are preferred (`TS_NODE_PREFER_TS_EXTS`, default: `false`) +* `--log-error` Logs TypeScript errors to stderr instead of throwing exceptions (`TS_NODE_LOG_ERROR`, default: `false`) ### Programmatic Only Options diff --git a/package-lock.json b/package-lock.json index c4974fb7a..786448326 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "8.4.1", + "version": "8.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d5ae6bcbe..d55cf8e1d 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "ts-node", - "version": "8.4.1", + "version": "8.5.0", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { - "ts-node": "dist/bin.js" + "ts-node": "dist/bin.js", + "ts-script": "dist/script.js" }, "files": [ "dist/", diff --git a/src/bin.ts b/src/bin.ts index e0268b216..bacfeed8e 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { join, resolve } from 'path' +import { join, resolve, dirname } from 'path' import { start, Recoverable } from 'repl' import { inspect } from 'util' import Module = require('module') @@ -8,190 +8,261 @@ import arg = require('arg') import { diffLines } from 'diff' import { Script } from 'vm' import { readFileSync, statSync } from 'fs' -import { register, VERSION, DEFAULTS, TSError, parse } from './index' - -const args = arg({ - // Node.js-like options. - '--eval': String, - '--print': Boolean, - '--require': [String], - - // CLI options. - '--files': Boolean, - '--help': Boolean, - '--version': arg.COUNT, - - // Project options. - '--cwd': String, - '--compiler': String, - '--compiler-options': parse, - '--project': String, - '--ignore-diagnostics': [String], - '--ignore': [String], - '--transpile-only': Boolean, - '--pretty': Boolean, - '--skip-project': Boolean, - '--skip-ignore': Boolean, - '--prefer-ts-exts': Boolean, - '--log-error': Boolean, - '--build': Boolean, - - // Aliases. - '-e': '--eval', - '-p': '--print', - '-r': '--require', - '-h': '--help', - '-v': '--version', - '-B': '--build', - '-T': '--transpile-only', - '-I': '--ignore', - '-P': '--project', - '-C': '--compiler', - '-D': '--ignore-diagnostics', - '-O': '--compiler-options' -}, { - stopAtPositional: true -}) - -const { - '--cwd': cwd = DEFAULTS.cwd || process.cwd(), - '--help': help = false, - '--version': version = 0, - '--files': files = DEFAULTS.files, - '--compiler': compiler = DEFAULTS.compiler, - '--compiler-options': compilerOptions = DEFAULTS.compilerOptions, - '--project': project = DEFAULTS.project, - '--ignore-diagnostics': ignoreDiagnostics = DEFAULTS.ignoreDiagnostics, - '--ignore': ignore = DEFAULTS.ignore, - '--transpile-only': transpileOnly = DEFAULTS.transpileOnly, - '--pretty': pretty = DEFAULTS.pretty, - '--skip-project': skipProject = DEFAULTS.skipProject, - '--skip-ignore': skipIgnore = DEFAULTS.skipIgnore, - '--prefer-ts-exts': preferTsExts = DEFAULTS.preferTsExts, - '--log-error': logError = DEFAULTS.logError, - '--build': build = DEFAULTS.build -} = args - -if (help) { - console.log(` -Usage: ts-node [options] [ -e script | script.ts ] [arguments] - -Options: - - -e, --eval [code] Evaluate code - -p, --print Print result of \`--eval\` - -r, --require [path] Require a node module before execution - - -h, --help Print CLI usage - -v, --version Print module version information - - -T, --transpile-only Use TypeScript's faster \`transpileModule\` - -I, --ignore [pattern] Override the path patterns to skip compilation - -P, --project [path] Path to TypeScript JSON project file - -C, --compiler [name] Specify a custom TypeScript compiler - -D, --ignore-diagnostics [code] Ignore TypeScript warnings by diagnostic code - -O, --compiler-options [opts] JSON object to merge with compiler options - - --files Load files from \`tsconfig.json\` on startup - --pretty Use pretty diagnostic formatter - --skip-project Skip reading \`tsconfig.json\` - --skip-ignore Skip \`--ignore\` checks - --prefer-ts-exts Prefer importing TypeScript files over JavaScript files -`) - - process.exit(0) -} - -// Output project information. -if (version === 1) { - console.log(`v${VERSION}`) - process.exit(0) -} - -const code = args['--eval'] -const isPrinted = args['--print'] !== undefined +import { homedir } from 'os' +import { register, VERSION, DEFAULTS, TSError, parse, Register } from './index' /** - * Eval helpers. + * Eval filename for REPL/debug. */ const EVAL_FILENAME = `[eval].ts` -const EVAL_PATH = join(cwd, EVAL_FILENAME) -const EVAL_INSTANCE = { input: '', output: '', version: 0, lines: 0 } - -// Register the TypeScript compiler instance. -const service = register({ - cwd, - build, - files, - pretty, - transpileOnly, - ignore, - project, - preferTsExts, - logError, - skipProject, - skipIgnore, - compiler, - ignoreDiagnostics, - compilerOptions, - readFile: code ? readFileEval : undefined, - fileExists: code ? fileExistsEval : undefined -}) - -// Output project information. -if (version >= 2) { - console.log(`ts-node v${VERSION}`) - console.log(`node ${process.version}`) - console.log(`compiler v${service.ts.version}`) - process.exit(0) + +/** + * Eval state management. + */ +class EvalState { + input = '' + output = '' + version = 0 + lines = 0 + + constructor (public path: string) {} } -// Require specified modules before start-up. -if (args['--require']) (Module as any)._preloadModules(args['--require']) +/** + * Main `bin` functionality. + */ +export function main (argv: string[]) { + const args = arg({ + // Node.js-like options. + '--eval': String, + '--interactive': Boolean, + '--print': Boolean, + '--require': [String], + + // CLI options. + '--help': Boolean, + '--script-mode': Boolean, + '--version': arg.COUNT, + + // Project options. + '--dir': String, + '--files': Boolean, + '--compiler': String, + '--compiler-options': parse, + '--project': String, + '--ignore-diagnostics': [String], + '--ignore': [String], + '--transpile-only': Boolean, + '--pretty': Boolean, + '--skip-project': Boolean, + '--skip-ignore': Boolean, + '--prefer-ts-exts': Boolean, + '--log-error': Boolean, + '--build': Boolean, + + // Aliases. + '-e': '--eval', + '-i': '--interactive', + '-p': '--print', + '-r': '--require', + '-h': '--help', + '-s': '--script-mode', + '-v': '--version', + '-B': '--build', + '-T': '--transpile-only', + '-I': '--ignore', + '-P': '--project', + '-C': '--compiler', + '-D': '--ignore-diagnostics', + '-O': '--compiler-options' + }, { + argv, + stopAtPositional: true + }) -// Prepend `ts-node` arguments to CLI for child processes. -process.execArgv.unshift(__filename, ...process.argv.slice(2, process.argv.length - args._.length)) -process.argv = [process.argv[1]].concat(args._.length ? resolve(cwd, args._[0]) : []).concat(args._.slice(1)) + const { + '--dir': dir = DEFAULTS.dir, + '--help': help = false, + '--script-mode': scriptMode = false, + '--version': version = 0, + '--require': requires = [], + '--eval': code = undefined, + '--print': print = false, + '--interactive': interactive = false, + '--files': files = DEFAULTS.files, + '--compiler': compiler = DEFAULTS.compiler, + '--compiler-options': compilerOptions = DEFAULTS.compilerOptions, + '--project': project = DEFAULTS.project, + '--ignore-diagnostics': ignoreDiagnostics = DEFAULTS.ignoreDiagnostics, + '--ignore': ignore = DEFAULTS.ignore, + '--transpile-only': transpileOnly = DEFAULTS.transpileOnly, + '--pretty': pretty = DEFAULTS.pretty, + '--skip-project': skipProject = DEFAULTS.skipProject, + '--skip-ignore': skipIgnore = DEFAULTS.skipIgnore, + '--prefer-ts-exts': preferTsExts = DEFAULTS.preferTsExts, + '--log-error': logError = DEFAULTS.logError, + '--build': build = DEFAULTS.build + } = args + + if (help) { + console.log(` + Usage: ts-node [options] [ -e script | script.ts ] [arguments] + + Options: + + -e, --eval [code] Evaluate code + -p, --print Print result of \`--eval\` + -r, --require [path] Require a node module before execution + -i, --interactive Opens the REPL even if stdin does not appear to be a terminal + + -h, --help Print CLI usage + -v, --version Print module version information + -s, --script-mode Use cwd from instead of current directory + + -T, --transpile-only Use TypeScript's faster \`transpileModule\` + -I, --ignore [pattern] Override the path patterns to skip compilation + -P, --project [path] Path to TypeScript JSON project file + -C, --compiler [name] Specify a custom TypeScript compiler + -D, --ignore-diagnostics [code] Ignore TypeScript warnings by diagnostic code + -O, --compiler-options [opts] JSON object to merge with compiler options + + --dir Specify working directory for config resolution + --scope Scope compiler to files within \`cwd\` only + --files Load files from \`tsconfig.json\` on startup + --pretty Use pretty diagnostic formatter (usually enabled by default) + --skip-project Skip reading \`tsconfig.json\` + --skip-ignore Skip \`--ignore\` checks + --prefer-ts-exts Prefer importing TypeScript files over JavaScript files + --log-error Logs TypeScript errors to stderr instead of throwing exceptions + `) + + process.exit(0) + } + + // Output project information. + if (version === 1) { + console.log(`v${VERSION}`) + process.exit(0) + } + + const cwd = dir || process.cwd() + const scriptPath = args._.length ? resolve(cwd, args._[0]) : undefined + const state = new EvalState(scriptPath || join(cwd, EVAL_FILENAME)) + + // Register the TypeScript compiler instance. + const service = register({ + dir: getCwd(dir, scriptMode, scriptPath), + files, + pretty, + transpileOnly, + ignore, + preferTsExts, + logError, + project, + skipProject, + skipIgnore, + compiler, + ignoreDiagnostics, + compilerOptions, + readFile: code !== undefined + ? (path: string) => { + if (path === state.path) return state.input + + try { + return readFileSync(path, 'utf8') + } catch (err) {/* Ignore. */} + } + : undefined, + fileExists: code !== undefined + ? (path: string) => { + if (path === state.path) return true + + try { + const stats = statSync(path) + return stats.isFile() || stats.isFIFO() + } catch (err) { + return false + } + } + : undefined + }) -// Execute the main contents (either eval, script or piped). -if (code) { - evalAndExit(code, isPrinted) -} else { - if (args._.length) { - Module.runMain() + // Output project information. + if (version >= 2) { + console.log(`ts-node v${VERSION}`) + console.log(`node ${process.version}`) + console.log(`compiler v${service.ts.version}`) + process.exit(0) + } + + // Create a local module instance based on `cwd`. + const module = new Module(state.path) + module.filename = state.path + module.paths = (Module as any)._nodeModulePaths(cwd) + + // Require specified modules before start-up. + for (const id of requires) module.require(id) + + // Prepend `ts-node` arguments to CLI for child processes. + process.execArgv.unshift(__filename, ...process.argv.slice(2, process.argv.length - args._.length)) + process.argv = [process.argv[1]].concat(scriptPath || []).concat(args._.slice(1)) + + // Execute the main contents (either eval, script or piped). + if (code !== undefined && !interactive) { + evalAndExit(service, state, module, code, print) } else { - // Piping of execution _only_ occurs when no other script is specified. - if (process.stdin.isTTY) { - startRepl() + if (args._.length) { + Module.runMain() } else { - let code = '' - process.stdin.on('data', (chunk: Buffer) => code += chunk) - process.stdin.on('end', () => evalAndExit(code, isPrinted)) + // Piping of execution _only_ occurs when no other script is specified. + if (process.stdin.isTTY) { + startRepl(service, state, code) + } else { + let buffer = code || '' + process.stdin.on('data', (chunk: Buffer) => buffer += chunk) + process.stdin.on('end', () => evalAndExit(service, state, module, buffer, print)) + } + } + } +} + +/** + * Get project path from args. + */ +function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) { + // Validate `--script-mode` usage is correct. + if (scriptMode) { + if (!scriptPath) { + throw new TypeError('Script mode must be used with a script name, e.g. `ts-node -s `') + } + + if (dir) { + throw new TypeError('Script mode cannot be combined with `--dir`') } + + return dirname(scriptPath) } + + return dir } /** * Evaluate a script. */ -function evalAndExit (code: string, isPrinted: boolean) { - const module = new Module(EVAL_FILENAME) - module.filename = EVAL_FILENAME - module.paths = (Module as any)._nodeModulePaths(cwd) +function evalAndExit (service: Register, state: EvalState, module: Module, code: string, isPrinted: boolean) { + let result: any - ;(global as any).__filename = EVAL_FILENAME - ;(global as any).__dirname = cwd + ;(global as any).__filename = module.filename + ;(global as any).__dirname = dirname(module.filename) ;(global as any).exports = module.exports ;(global as any).module = module ;(global as any).require = module.require.bind(module) - let result: any - try { - result = _eval(code) + result = _eval(service, state, code) } catch (error) { if (error instanceof TSError) { - console.error(error.diagnosticText) + console.error(error) process.exit(1) } @@ -206,30 +277,30 @@ function evalAndExit (code: string, isPrinted: boolean) { /** * Evaluate the code snippet. */ -function _eval (input: string) { - const lines = EVAL_INSTANCE.lines +function _eval (service: Register, state: EvalState, input: string) { + const lines = state.lines const isCompletion = !/\n$/.test(input) - const undo = appendEval(input) + const undo = appendEval(state, input) let output: string try { - output = service.compile(EVAL_INSTANCE.input, EVAL_PATH, -lines) + output = service.compile(state.input, state.path, -lines) } catch (err) { undo() throw err } // Use `diff` to check for new JavaScript to execute. - const changes = diffLines(EVAL_INSTANCE.output, output) + const changes = diffLines(state.output, output) if (isCompletion) { undo() } else { - EVAL_INSTANCE.output = output + state.output = output } return changes.reduce((result, change) => { - return change.added ? exec(change.value, EVAL_FILENAME) : result + return change.added ? exec(change.value, state.path) : result }, undefined) } @@ -245,7 +316,17 @@ function exec (code: string, filename: string) { /** * Start a CLI REPL. */ -function startRepl () { +function startRepl (service: Register, state: EvalState, code?: string) { + // Eval incoming code before the REPL starts. + if (code) { + try { + _eval(service, state, `${code}\n`) + } catch (err) { + console.error(err) + process.exit(1) + } + } + const repl = start({ prompt: '> ', input: process.stdin, @@ -255,14 +336,45 @@ function startRepl () { useGlobal: true }) + /** + * Eval code from the REPL. + */ + function replEval (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any) { + let err: Error | null = null + let result: any + + // TODO: Figure out how to handle completion here. + if (code === '.scope') { + callback(err) + return + } + + try { + result = _eval(service, state, code) + } catch (error) { + if (error instanceof TSError) { + // Support recoverable compilations using >= node 6. + if (Recoverable && isRecoverable(error)) { + err = new Recoverable(error) + } else { + console.error(error) + } + } else { + err = error + } + } + + return callback(err, result) + } + // Bookmark the point where we should reset the REPL state. - const resetEval = appendEval('') + const resetEval = appendEval(state, '') function reset () { resetEval() // Hard fix for TypeScript forcing `Object.defineProperty(exports, ...)`. - exec('exports = module.exports', EVAL_FILENAME) + exec('exports = module.exports', state.path) } reset() @@ -276,8 +388,8 @@ function startRepl () { return } - const undo = appendEval(identifier) - const { name, comment } = service.getTypeInfo(EVAL_INSTANCE.input, EVAL_PATH, EVAL_INSTANCE.input.length) + const undo = appendEval(state, identifier) + const { name, comment } = service.getTypeInfo(state.input, state.path, state.input.length) undo() @@ -286,62 +398,43 @@ function startRepl () { repl.displayPrompt() } }) -} -/** - * Eval code from the REPL. - */ -function replEval (code: string, _context: any, _filename: string, callback: (err: Error | null, result?: any) => any) { - let err: Error | null = null - let result: any + // Set up REPL history when available natively via node.js >= 11. + if (repl.setupHistory) { + const historyPath = process.env.TS_NODE_HISTORY || join(homedir(), '.ts_node_repl_history') - // TODO: Figure out how to handle completion here. - if (code === '.scope') { - callback(err) - return - } + repl.setupHistory(historyPath, err => { + if (!err) return - try { - result = _eval(code) - } catch (error) { - if (error instanceof TSError) { - // Support recoverable compilations using >= node 6. - if (Recoverable && isRecoverable(error)) { - err = new Recoverable(error) - } else { - console.error(error.diagnosticText) - } - } else { - err = error - } + console.error(err) + process.exit(1) + }) } - - callback(err, result) } /** * Append to the eval instance and return an undo function. */ -function appendEval (input: string) { - const undoInput = EVAL_INSTANCE.input - const undoVersion = EVAL_INSTANCE.version - const undoOutput = EVAL_INSTANCE.output - const undoLines = EVAL_INSTANCE.lines +function appendEval (state: EvalState, input: string) { + const undoInput = state.input + const undoVersion = state.version + const undoOutput = state.output + const undoLines = state.lines // Handle ASI issues with TypeScript re-evaluation. - if (undoInput.charAt(undoInput.length - 1) === '\n' && /^\s*[\[\(\`]/.test(input) && !/;\s*$/.test(undoInput)) { - EVAL_INSTANCE.input = `${EVAL_INSTANCE.input.slice(0, -1)};\n` + if (undoInput.charAt(undoInput.length - 1) === '\n' && /^\s*[\/\[(`]/.test(input) && !/;\s*$/.test(undoInput)) { + state.input = `${state.input.slice(0, -1)};\n` } - EVAL_INSTANCE.input += input - EVAL_INSTANCE.lines += lineCount(input) - EVAL_INSTANCE.version++ + state.input += input + state.lines += lineCount(input) + state.version++ return function () { - EVAL_INSTANCE.input = undoInput - EVAL_INSTANCE.output = undoOutput - EVAL_INSTANCE.version = undoVersion - EVAL_INSTANCE.lines = undoLines + state.input = undoInput + state.output = undoOutput + state.version = undoVersion + state.lines = undoLines } } @@ -360,31 +453,6 @@ function lineCount (value: string) { return count } -/** - * Get the file text, checking for eval first. - */ -function readFileEval (path: string) { - if (path === EVAL_PATH) return EVAL_INSTANCE.input - - try { - return readFileSync(path, 'utf8') - } catch (err) {/* Ignore. */} -} - -/** - * Get whether the file exists. - */ -function fileExistsEval (path: string) { - if (path === EVAL_PATH) return true - - try { - const stats = statSync(path) - return stats.isFile() || stats.isFIFO() - } catch (err) { - return false - } -} - const RECOVERY_CODES: Set = new Set([ 1003, // "Identifier expected." 1005, // "')' expected." @@ -401,3 +469,7 @@ const RECOVERY_CODES: Set = new Set([ function isRecoverable (error: TSError) { return error.diagnosticCodes.every(code => RECOVERY_CODES.has(code)) } + +if (require.main === module) { + main(process.argv.slice(2)) +} diff --git a/src/index.spec.ts b/src/index.spec.ts index 6b8349b73..c1402be7e 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -4,12 +4,12 @@ import { join } from 'path' import semver = require('semver') import ts = require('typescript') import proxyquire = require('proxyquire') -import { register, VERSION } from './index' +import { register, create, VERSION } from './index' const TEST_DIR = join(__dirname, '../tests') -const EXEC_PATH = join(__dirname, '../dist/bin') const PROJECT = join(TEST_DIR, 'tsconfig.json') -const BIN_EXEC = `node "${EXEC_PATH}" --project "${PROJECT}"` +const BIN_EXEC = `node "${join(__dirname, '../dist/bin')}" --project "${PROJECT}"` +const SCRIPT_EXEC = `node "${join(__dirname, '../dist/script')}"` const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/ @@ -61,6 +61,26 @@ describe('ts-node', function () { }) }) + it('should provide registered information globally', function (done) { + exec(`${BIN_EXEC} tests/env`, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.equal('object\n') + + return done() + }) + }) + + it('should provide registered information on register', function (done) { + exec(`node -r ../register env.ts`, { + cwd: TEST_DIR + }, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.equal('object\n') + + return done() + }) + }) + if (semver.gte(ts.version, '1.8.0')) { it('should allow js', function (done) { exec( @@ -315,22 +335,100 @@ describe('ts-node', function () { return done() }) }) + + if (semver.gte(ts.version, '2.7.0')) { + it('should support script mode', function (done) { + exec(`${SCRIPT_EXEC} tests/scope/a/log`, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.equal('.ts\n') + + return done() + }) + }) + } }) describe('register', function () { - register({ + const registered = register({ project: PROJECT, compilerOptions: { jsx: 'preserve' } }) + const moduleTestPath = require.resolve('../tests/module') + + afterEach(() => { + // Re-enable project after every test. + registered.enabled(true) + }) + it('should be able to require typescript', function () { - const m = require('../tests/module') + const m = require(moduleTestPath) expect(m.example('foo')).to.equal('FOO') }) + it('should support dynamically disabling', function () { + delete require.cache[moduleTestPath] + + expect(registered.enabled(false)).to.equal(false) + expect(() => require(moduleTestPath)).to.throw(/Unexpected token/) + + delete require.cache[moduleTestPath] + + expect(registered.enabled()).to.equal(false) + expect(() => require(moduleTestPath)).to.throw(/Unexpected token/) + + delete require.cache[moduleTestPath] + + expect(registered.enabled(true)).to.equal(true) + expect(() => require(moduleTestPath)).to.not.throw() + + delete require.cache[moduleTestPath] + + expect(registered.enabled()).to.equal(true) + expect(() => require(moduleTestPath)).to.not.throw() + }) + + if (semver.gte(ts.version, '2.7.0')) { + it('should support compiler scopes', function () { + const calls: string[] = [] + + registered.enabled(false) + + const compilers = [ + register({ dir: join(TEST_DIR, 'scope/a'), scope: true }), + register({ dir: join(TEST_DIR, 'scope/b'), scope: true }) + ] + + compilers.forEach(c => { + const old = c.compile + c.compile = (code, fileName, lineOffset) => { + calls.push(fileName) + + return old(code, fileName, lineOffset) + } + }) + + try { + expect(require('../tests/scope/a').ext).to.equal('.ts') + expect(require('../tests/scope/b').ext).to.equal('.ts') + } finally { + compilers.forEach(c => c.enabled(false)) + } + + expect(calls).to.deep.equal([ + join(TEST_DIR, 'scope/a/index.ts'), + join(TEST_DIR, 'scope/b/index.ts') + ]) + + delete require.cache[moduleTestPath] + + expect(() => require(moduleTestPath)).to.throw() + }) + } + it('should compile through js and ts', function () { const m = require('../tests/complex') @@ -398,4 +496,13 @@ describe('ts-node', function () { }) }) }) + + describe('create', () => { + it('should create generic compiler instances', () => { + const service = create({ compilerOptions: { target: 'es5' }, skipProject: true }) + const output = service.compile('const x = 10', 'test.ts') + + expect(output).to.contain('var x = 10;') + }) + }) }) diff --git a/src/index.ts b/src/index.ts index 6c16894d2..45c77af58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,22 @@ import { BaseError } from 'make-error' import * as util from 'util' import * as _ts from 'typescript' +/** + * Registered `ts-node` instance information. + */ +export const REGISTER_INSTANCE = Symbol.for('ts-node.register.instance') + +/** + * Expose `REGISTER_INSTANCE` information on node.js `process`. + */ +declare global { + namespace NodeJS { + interface Process { + [REGISTER_INSTANCE]?: Register + } + } +} + /** * @internal */ @@ -53,11 +69,12 @@ export interface TSCommon { export const VERSION = require('../package.json').version /** - * Registration options. + * Options for creating a new TypeScript compiler instance. */ -export interface Options { - cwd?: string | null +export interface CreateOptions { + dir?: string build?: boolean | null + scope?: boolean | null pretty?: boolean | null transpileOnly?: boolean | null logError?: boolean | null @@ -75,6 +92,13 @@ export interface Options { transformers?: _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers) } +/** + * Options for registering a TypeScript compiler instance globally. + */ +export interface RegisterOptions extends CreateOptions { + preferTsExts?: boolean | null +} + /** * Track the project information. */ @@ -95,8 +119,10 @@ export interface TypeInfo { /** * Default register options. */ -export const DEFAULTS: Options = { - cwd: process.env.TS_NODE_CWD, +export const DEFAULTS: RegisterOptions = { + dir: process.env.TS_NODE_DIR, + build: yn(process.env.TS_NODE_BUILD), + scope: yn(process.env.TS_NODE_SCOPE), files: yn(process.env.TS_NODE_FILES), pretty: yn(process.env.TS_NODE_PRETTY), compiler: process.env.TS_NODE_COMPILER, @@ -108,8 +134,7 @@ export const DEFAULTS: Options = { preferTsExts: yn(process.env.TS_NODE_PREFER_TS_EXTS), ignoreDiagnostics: split(process.env.TS_NODE_IGNORE_DIAGNOSTICS), transpileOnly: yn(process.env.TS_NODE_TRANSPILE_ONLY), - logError: yn(process.env.TS_NODE_LOG_ERROR), - build: yn(process.env.TS_NODE_BUILD) + logError: yn(process.env.TS_NODE_LOG_ERROR) } /** @@ -167,9 +192,10 @@ export class TSError extends BaseError { * Return type for registering `ts-node`. */ export interface Register { - cwd: string - extensions: string[] ts: TSCommon + config: _ts.ParsedCommandLine + enabled (enabled?: boolean): boolean + ignored (fileName: string): boolean compile (code: string, fileName: string, lineOffset?: number): string getTypeInfo (code: string, fileName: string, position: number): TypeInfo } @@ -190,12 +216,32 @@ function cachedLookup (fn: (arg: string) => T): (arg: string) => T { } /** - * Register TypeScript compiler. + * Register TypeScript compiler instance onto node.js */ -export function register (opts: Options = {}): Register { +export function register (opts: RegisterOptions = {}): Register { const options = { ...DEFAULTS, ...opts } const originalJsHandler = require.extensions['.js'] // tslint:disable-line + const service = create(options) + const extensions = ['.ts'] + // Enable additional extensions when JSX or `allowJs` is enabled. + if (service.config.options.jsx) extensions.push('.tsx') + if (service.config.options.allowJs) extensions.push('.js') + if (service.config.options.jsx && service.config.options.allowJs) extensions.push('.jsx') + + // Expose registered instance globally. + process[REGISTER_INSTANCE] = service + + // Register the extensions. + registerExtensions(options.preferTsExts, extensions, service, originalJsHandler) + + return service +} + +/** + * Create TypeScript compiler instance. + */ +export function create (options: CreateOptions = {}): Register { const ignoreDiagnostics = [ 6059, // "'rootDir' is expected to contain all source files." 18002, // "The 'files' list in config file is empty." @@ -206,7 +252,8 @@ export function register (opts: Options = {}): Register { const ignore = options.skipIgnore ? [] : (options.ignore || ['/node_modules/']).map(str => new RegExp(str)) // Require the TypeScript compiler and configuration. - const cwd = options.cwd || process.cwd() + const cwd = options.dir ? resolve(options.dir) : process.cwd() + const isScoped = options.scope ? (fileName: string) => relative(cwd, fileName).charAt(0) !== '.' : () => true const transpileOnly = options.transpileOnly === true const compiler = require.resolve(options.compiler || 'typescript', { paths: [cwd, __dirname] }) const ts: typeof _ts = require(compiler) @@ -215,7 +262,6 @@ export function register (opts: Options = {}): Register { const fileExists = options.fileExists || ts.sys.fileExists const config = readConfig(cwd, ts, fileExists, readFile, options) const configDiagnosticList = filterDiagnostics(config.errors, ignoreDiagnostics) - const extensions = ['.ts'] const outputCache = new Map() const diagnosticHost: _ts.FormatDiagnosticsHost = { @@ -233,7 +279,7 @@ export function register (opts: Options = {}): Register { }) const formatDiagnostics = process.stdout.isTTY || options.pretty - ? ts.formatDiagnosticsWithColorAndContext + ? (ts.formatDiagnosticsWithColorAndContext || ts.formatDiagnostics) : ts.formatDiagnostics function createTSError (diagnostics: ReadonlyArray<_ts.Diagnostic>) { @@ -256,11 +302,6 @@ export function register (opts: Options = {}): Register { // Render the configuration errors. if (configDiagnosticList.length) reportTSError(configDiagnosticList) - // Enable additional extensions when JSX or `allowJs` is enabled. - if (config.options.jsx) extensions.push('.tsx') - if (config.options.allowJs) extensions.push('.js') - if (config.options.jsx && config.options.allowJs) extensions.push('.jsx') - /** * Get the extension for a transpiled file. */ @@ -467,12 +508,11 @@ export function register (opts: Options = {}): Register { return output } - const register: Register = { cwd, compile, getTypeInfo, extensions, ts } + let active = true + const enabled = (enabled?: boolean) => enabled === undefined ? active : (active = !!enabled) + const ignored = (fileName: string) => !active || !isScoped(fileName) || shouldIgnore(fileName, ignore) - // Register the extensions. - registerExtensions(options.preferTsExts, extensions, ignore, register, originalJsHandler) - - return register + return { ts, config, compile, getTypeInfo, ignored, enabled } } /** @@ -485,7 +525,7 @@ function shouldIgnore (filename: string, ignore: RegExp[]) { } /** - * "Refreshes" an extension on `require.extentions`. + * "Refreshes" an extension on `require.extensions`. * * @param {string} ext */ @@ -501,13 +541,12 @@ function reorderRequireExtension (ext: string) { function registerExtensions ( preferTsExts: boolean | null | undefined, extensions: string[], - ignore: RegExp[], register: Register, originalJsHandler: (m: NodeModule, filename: string) => any ) { // Register new extensions. for (const ext of extensions) { - registerExtension(ext, ignore, register, originalJsHandler) + registerExtension(ext, register, originalJsHandler) } if (preferTsExts) { @@ -523,16 +562,13 @@ function registerExtensions ( */ function registerExtension ( ext: string, - ignore: RegExp[], register: Register, originalHandler: (m: NodeModule, filename: string) => any ) { const old = require.extensions[ext] || originalHandler // tslint:disable-line require.extensions[ext] = function (m: any, filename) { // tslint:disable-line - if (shouldIgnore(filename, ignore)) { - return old(m, filename) - } + if (register.ignored(filename)) return old(m, filename) const _compile = m._compile @@ -579,7 +615,7 @@ function readConfig ( ts: TSCommon, fileExists: (path: string) => boolean, readFile: (path: string) => string | undefined, - options: Options + options: CreateOptions ): _ts.ParsedCommandLine { let config: any = { compilerOptions: {} } let basePath = normalizeSlashes(cwd) diff --git a/src/script.ts b/src/script.ts new file mode 100644 index 000000000..66e1113ee --- /dev/null +++ b/src/script.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from './bin' + +main(['--script-mode', ...process.argv.slice(2)]) diff --git a/tests/env.ts b/tests/env.ts new file mode 100644 index 000000000..8a3b78aba --- /dev/null +++ b/tests/env.ts @@ -0,0 +1 @@ +console.log(typeof process[Symbol.for('ts-node.register.instance')]) diff --git a/tests/scope/a/index.ts b/tests/scope/a/index.ts new file mode 100644 index 000000000..af2b0fed2 --- /dev/null +++ b/tests/scope/a/index.ts @@ -0,0 +1,3 @@ +import path from 'path' + +export const ext = path.extname(__filename) diff --git a/tests/scope/a/log.ts b/tests/scope/a/log.ts new file mode 100644 index 000000000..bf20f0de2 --- /dev/null +++ b/tests/scope/a/log.ts @@ -0,0 +1,3 @@ +import { ext } from './index' + +console.log(ext) diff --git a/tests/scope/a/tsconfig.json b/tests/scope/a/tsconfig.json new file mode 100644 index 000000000..2f9804271 --- /dev/null +++ b/tests/scope/a/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/tests/scope/b/index.ts b/tests/scope/b/index.ts new file mode 100644 index 000000000..af2b0fed2 --- /dev/null +++ b/tests/scope/b/index.ts @@ -0,0 +1,3 @@ +import path from 'path' + +export const ext = path.extname(__filename) diff --git a/tests/scope/b/tsconfig.json b/tests/scope/b/tsconfig.json new file mode 100644 index 000000000..2f9804271 --- /dev/null +++ b/tests/scope/b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 12fb856f1..561005005 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "es2015", "jsx": "react", "noEmit": true, // Global type definitions.