diff --git a/README.md b/README.md index cb9994de8..ec75beb53 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ ts-node -p -e '"Hello, world!"' # Pipe scripts to execute with TypeScript. echo 'console.log("Hello, world!")' | ts-node -# Equivalent to ts-node --script-mode -ts-node-script scripts.ts +# Equivalent to ts-node --cwd-mode +ts-node-cwd scripts.ts # Equivalent to ts-node --transpile-only ts-node-transpile-only scripts.ts @@ -57,17 +57,15 @@ ts-node-transpile-only scripts.ts ### Shebang ```typescript -#!/usr/bin/env ts-node-script +#!/usr/bin/env ts-node console.log("Hello, world!") ``` -`ts-node-script` is recommended because it enables `--script-mode`, discovering `tsconfig.json` relative to the script's location instead of `process.cwd()`. This makes scripts more portable. - Passing CLI arguments via shebang is allowed on Mac but not Linux. For example, the following will fail on Linux: ``` -#!/usr/bin/env ts-node --script-mode --transpile-only --files +#!/usr/bin/env ts-node --transpile-only --files // This shebang is not portable. It only works on Mac ``` @@ -152,11 +150,14 @@ When node.js has an extension registered (via `require.extensions`), it will use ## Loading `tsconfig.json` -**Typescript Node** loads `tsconfig.json` automatically. Use `--skip-project` to skip loading the `tsconfig.json`. +**Typescript Node** finds and loads `tsconfig.json` automatically. Use `--skip-project` to skip loading the `tsconfig.json`. Use `--project` to explicitly specify the path to a `tsconfig.json` + +When searching, it is resolved using [the same search behavior as `tsc`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). By default, this search is performed relative to the directory containing the entrypoint script. In `--cwd-mode` or if no entrypoint is specified -- for example when using the REPL -- the search is performed relative to `--cwd` / `process.cwd()`, which matches the behavior of `tsc`. -It is resolved relative to `--dir` using [the same search behavior as `tsc`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). In `--script-mode`, this is the directory containing the script. Otherwise it is resolved relative to `process.cwd()`, which matches the behavior of `tsc`. +For example: -Use `--project` to specify the path to your `tsconfig.json`, ignoring `--dir`. +* if you run `ts-node ./src/app/index.ts`, we will automatically use `./src/tsconfig.json`. +* if you run `ts-node`, we will automatically use `./tsconfig.json`. **Tip**: You can use `ts-node` together with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths) to load modules according to the `paths` section in `tsconfig.json`. @@ -176,7 +177,8 @@ ts-node --compiler ntypescript --project src/tsconfig.json hello-world.ts * `-h, --help` Prints the help text * `-v, --version` Prints the version. `-vv` prints node and typescript compiler versions, too -* `-s, --script-mode` Resolve config relative to the directory of the passed script instead of the current directory. Changes default of `--dir` +* `-c, --cwd-mode` Resolve config relative to the current directory instead of the directory of the entrypoint script. +* `--script-mode` Resolve config relative to the directory of the entrypoint script. This is the default behavior. ### CLI and Programmatic Options @@ -189,8 +191,7 @@ _The name of the environment variable and the option's default value are denoted * `-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()`, or `dirname(scriptPath)` if `--script-mode`) -* `--scope` Scope compiler to files within `cwd` (`TS_NODE_SCOPE`, default: `false`) +* `--cwd` Behave as if invoked within this working directory. (`TS_NODE_CWD`, default: `process.cwd()`) * `--files` Load `files`, `include` and `exclude` 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`) @@ -201,6 +202,9 @@ _The name of the environment variable and the option's default value are denoted ### Programmatic-only Options +* `scope` Scope compiler to files within `scopeDir`. Files outside this directory will be ignored. (default: `false`) +* `scopeDir` Sets directory for `scope`. Defaults to tsconfig `rootDir`, directory containing `tsconfig.json`, or `cwd` +* `projectSearchDir` Search for TypeScript config file (`tsconfig.json`) in this or parent directories. * `transformers` `_ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers)`: An object with transformers or a factory function that accepts a program and returns a transformers object to pass to TypeScript. Factory function cannot be used with `transpileOnly` flag * `readFile`: Custom TypeScript-compatible file reading function * `fileExists`: Custom TypeScript-compatible file existence function @@ -219,7 +223,7 @@ Most options can be specified by a `"ts-node"` object in `tsconfig.json` using t } ``` -Our bundled [JSON schema](https://unpkg.com/browse/ts-node@8.8.2/tsconfig.schema.json) lists all compatible options. +Our bundled [JSON schema](https://unpkg.com/browse/ts-node@latest/tsconfig.schema.json) lists all compatible options. ## SyntaxError diff --git a/package.json b/package.json index fbee06d37..866591fe1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "./dist/bin-transpile.js": "./dist/bin-transpile.js", "./dist/bin-script": "./dist/bin-script.js", "./dist/bin-script.js": "./dist/bin-script.js", + "./dist/bin-cwd": "./dist/bin-cwd.js", + "./dist/bin-cwd.js": "./dist/bin-cwd.js", "./register": "./register/index.js", "./register/files": "./register/files.js", "./register/transpile-only": "./register/transpile-only.js", @@ -27,6 +29,7 @@ "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-script": "dist/bin-script.js", + "ts-node-cwd": "dist/bin-cwd.js", "ts-node-transpile-only": "dist/bin-transpile.js" }, "files": [ diff --git a/src/bin-cwd.ts b/src/bin-cwd.ts new file mode 100644 index 000000000..fb2c1e63b --- /dev/null +++ b/src/bin-cwd.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from './bin' + +main(undefined, { '--cwd-mode': true }) diff --git a/src/bin.ts b/src/bin.ts index 9ec856798..d07a124e8 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { join, resolve, dirname } from 'path' +import { join, resolve, dirname, parse as parsePath } from 'path' import { inspect } from 'util' import Module = require('module') import arg = require('arg') @@ -10,7 +10,7 @@ import { createRepl, ReplService } from './repl' -import { VERSION, TSError, parse, register } from './index' +import { VERSION, TSError, parse, register, createRequire } from './index' /** * Main `bin` functionality. @@ -27,11 +27,12 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re // CLI options. '--help': Boolean, + '--cwd-mode': Boolean, '--script-mode': Boolean, '--version': arg.COUNT, // Project options. - '--dir': String, + '--cwd': String, '--files': Boolean, '--compiler': String, '--compiler-options': parse, @@ -62,7 +63,8 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re '-P': '--project', '-C': '--compiler', '-D': '--ignore-diagnostics', - '-O': '--compiler-options' + '-O': '--compiler-options', + '--dir': '--cwd' }, { argv, stopAtPositional: true @@ -73,9 +75,10 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re // Anything passed to `register()` can be `undefined`; `create()` will apply // defaults. const { - '--dir': dir, + '--cwd': cwdArg, '--help': help = false, - '--script-mode': scriptMode = false, + '--script-mode': scriptMode, + '--cwd-mode': cwdMode, '--version': version = 0, '--require': argsRequire = [], '--eval': code = undefined, @@ -111,7 +114,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re -h, --help Print CLI usage -v, --version Print module version information - -s, --script-mode Use cwd from instead of current directory + --cwd-mode Use current directory instead of for config resolution -T, --transpile-only Use TypeScript's faster \`transpileModule\` -H, --compiler-host Use TypeScript's compiler host API @@ -121,8 +124,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re -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 + --cwd Behave as if invoked within this working directory. --files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup --pretty Use pretty diagnostic formatter (usually enabled by default) --skip-project Skip reading \`tsconfig.json\` @@ -140,7 +142,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re process.exit(0) } - const cwd = dir || process.cwd() + const cwd = cwdArg || process.cwd() /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ const scriptPath = args._.length ? resolve(cwd, args._[0]) : undefined const state = new EvalState(scriptPath || join(cwd, EVAL_FILENAME)) @@ -149,7 +151,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re // Register the TypeScript compiler instance. const service = register({ - dir: getCwd(dir, scriptMode, scriptPath), + cwd, emit, files, pretty, @@ -159,6 +161,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re ignore, preferTsExts, logError, + projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), project, skipProject, skipIgnore, @@ -211,19 +214,21 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re } /** - * Get project path from args. + * Get project search 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`') - } - +function getProjectSearchDir (cwd?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPath?: string) { + // Validate `--script-mode` / `--cwd-mode` / `--cwd` usage is correct. + if (scriptMode && cwdMode) { + throw new TypeError('--cwd-mode cannot be combined with --script-mode') + } + if (scriptMode && !scriptPath) { + throw new TypeError('--script-mode must be used with a script name, e.g. `ts-node --script-mode `') + } + const doScriptMode = + scriptMode === true ? true + : cwdMode === true ? false + : !!scriptPath + if (doScriptMode) { // Use node's own resolution behavior to ensure we follow symlinks. // scriptPath may omit file extension or point to a directory with or without package.json. // This happens before we are registered, so we tell node's resolver to consider ts, tsx, and jsx files. @@ -240,7 +245,7 @@ function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) { } } try { - return dirname(require.resolve(scriptPath)) + return dirname(requireResolveNonCached(scriptPath!)) } finally { for (const ext of extsTemporarilyInstalled) { delete require.extensions[ext] // tslint:disable-line @@ -248,7 +253,32 @@ function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) { } } - return dir + return cwd +} + +const guaranteedNonexistentDirectoryPrefix = resolve(__dirname, 'doesnotexist') +let guaranteedNonexistentDirectorySuffix = 0 + +/** + * require.resolve an absolute path, tricking node into *not* caching the results. + * Necessary so that we do not pollute require.resolve cache prior to installing require.extensions + * + * Is a terrible hack, because node does not expose the necessary cache invalidation APIs + * https://stackoverflow.com/questions/59865584/how-to-invalidate-cached-require-resolve-results + */ +function requireResolveNonCached (absoluteModuleSpecifier: string) { + // node 10 and 11 fallback: The trick below triggers a node 10 & 11 bug + // On those node versions, pollute the require cache instead. This is a deliberate + // ts-node limitation that will *rarely* manifest, and will not matter once node 10 + // is end-of-life'd on 2021-04-30 + const isSupportedNodeVersion = parseInt(process.versions.node.split('.')[0], 10) >= 12 + if (!isSupportedNodeVersion) return require.resolve(absoluteModuleSpecifier) + + const { dir, base } = parsePath(absoluteModuleSpecifier) + const relativeModuleSpecifier = `./${base}` + + const req = createRequire(join(dir, 'imaginaryUncacheableRequireResolveScript')) + return req.resolve(relativeModuleSpecifier, { paths: [`${ guaranteedNonexistentDirectoryPrefix }${ guaranteedNonexistentDirectorySuffix++ }`, ...req.resolve.paths(relativeModuleSpecifier) || []] }) } /** diff --git a/src/index.spec.ts b/src/index.spec.ts index 164a76c2c..37f9be53d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -31,6 +31,7 @@ const TEST_DIR = join(__dirname, '../tests') const PROJECT = join(TEST_DIR, 'tsconfig.json') const BIN_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node') const BIN_SCRIPT_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-script') +const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd') const SOURCE_MAP_REGEXP = /\/\/# sourceMappingURL=data:application\/json;charset=utf\-8;base64,[\w\+]+=*$/ @@ -76,6 +77,8 @@ describe('ts-node', function () { testsDirRequire.resolve('ts-node/dist/bin-transpile.js') testsDirRequire.resolve('ts-node/dist/bin-script') testsDirRequire.resolve('ts-node/dist/bin-script.js') + testsDirRequire.resolve('ts-node/dist/bin-cwd') + testsDirRequire.resolve('ts-node/dist/bin-cwd.js') // Must be `require()`able obviously testsDirRequire.resolve('ts-node/register') @@ -467,8 +470,10 @@ describe('ts-node', function () { }) }) + const preferTsExtsEntrypoint = semver.gte(process.version, '12.0.0') ? 'import-order/compiled' : 'import-order/require-compiled' it('should import ts before js when --prefer-ts-exts flag is present', function (done) { - exec(`${cmd} --prefer-ts-exts import-order/compiled`, function (err, stdout) { + + exec(`${cmd} --prefer-ts-exts ${preferTsExtsEntrypoint}`, function (err, stdout) { expect(err).to.equal(null) expect(stdout).to.equal('Hello, TypeScript!\n') @@ -477,7 +482,7 @@ describe('ts-node', function () { }) it('should import ts before js when TS_NODE_PREFER_TS_EXTS env is present', function (done) { - exec(`${cmd} import-order/compiled`, { env: { ...process.env, TS_NODE_PREFER_TS_EXTS: 'true' } }, function (err, stdout) { + exec(`${cmd} ${preferTsExtsEntrypoint}`, { env: { ...process.env, TS_NODE_PREFER_TS_EXTS: 'true' } }, function (err, stdout) { expect(err).to.equal(null) expect(stdout).to.equal('Hello, TypeScript!\n') @@ -533,17 +538,49 @@ describe('ts-node', function () { }) if (semver.gte(ts.version, '2.7.0')) { - it('should support script mode', function (done) { - exec(`${BIN_SCRIPT_PATH} scope/a/log`, function (err, stdout) { + it('should locate tsconfig relative to entry-point by default', function (done) { + exec(`${BIN_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.match(/plugin-a/) + + return done() + }) + }) + it('should locate tsconfig relative to entry-point via ts-node-script', function (done) { + exec(`${BIN_SCRIPT_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.match(/plugin-a/) + + return done() + }) + }) + it('should locate tsconfig relative to entry-point with --script-mode', function (done) { + exec(`${BIN_PATH} --script-mode ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.match(/plugin-a/) + + return done() + }) + }) + it('should locate tsconfig relative to cwd via ts-node-cwd', function (done) { + exec(`${BIN_CWD_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.match(/plugin-b/) + + return done() + }) + }) + it('should locate tsconfig relative to cwd in --cwd-mode', function (done) { + exec(`${BIN_PATH} --cwd-mode ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { expect(err).to.equal(null) - expect(stdout).to.equal('.ts\n') + expect(stdout).to.match(/plugin-b/) return done() }) }) - it('should read tsconfig relative to realpath, not symlink, in scriptMode', function (done) { + it('should locate tsconfig relative to realpath, not symlink, when entrypoint is a symlink', function (done) { if (lstatSync(join(TEST_DIR, 'main-realpath/symlink/symlink.tsx')).isSymbolicLink()) { - exec(`${BIN_SCRIPT_PATH} main-realpath/symlink/symlink.tsx`, function (err, stdout) { + exec(`${BIN_PATH} main-realpath/symlink/symlink.tsx`, function (err, stdout) { expect(err).to.equal(null) expect(stdout).to.equal('') @@ -636,7 +673,7 @@ describe('ts-node', function () { }) it('should transpile files inside a node_modules directory when not ignored', function (done) { - exec(`${cmdNoProject} --script-mode from-node-modules/from-node-modules`, function (err, stdout, stderr) { + exec(`${cmdNoProject} from-node-modules/from-node-modules`, function (err, stdout, stderr) { if (err) return done(`Unexpected error: ${err}\nstdout:\n${stdout}\nstderr:\n${stderr}`) expect(JSON.parse(stdout)).to.deep.equal({ external: { @@ -656,11 +693,11 @@ describe('ts-node', function () { describe('should respect maxNodeModulesJsDepth', function () { it('for unscoped modules', function (done) { - exec(`${cmdNoProject} --script-mode maxnodemodulesjsdepth`, function (err, stdout, stderr) { + exec(`${cmdNoProject} maxnodemodulesjsdepth`, function (err, stdout, stderr) { expect(err).to.not.equal(null) expect(stderr.replace(/\r\n/g, '\n')).to.contain( 'TSError: ⨯ Unable to compile TypeScript:\n' + - "other.ts(4,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + + "maxnodemodulesjsdepth/other.ts(4,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' ) done() @@ -668,11 +705,11 @@ describe('ts-node', function () { }) it('for @scoped modules', function (done) { - exec(`${cmdNoProject} --script-mode maxnodemodulesjsdepth-scoped`, function (err, stdout, stderr) { + exec(`${cmdNoProject} maxnodemodulesjsdepth-scoped`, function (err, stdout, stderr) { expect(err).to.not.equal(null) expect(stderr.replace(/\r\n/g, '\n')).to.contain( 'TSError: ⨯ Unable to compile TypeScript:\n' + - "other.ts(7,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + + "maxnodemodulesjsdepth-scoped/other.ts(7,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' ) done() @@ -734,8 +771,8 @@ describe('ts-node', function () { registered.enabled(false) const compilers = [ - register({ dir: join(TEST_DIR, 'scope/a'), scope: true }), - register({ dir: join(TEST_DIR, 'scope/b'), scope: true }) + register({ projectSearchDir: join(TEST_DIR, 'scope/a'), scopeDir: join(TEST_DIR, 'scope/a'), scope: true }), + register({ projectSearchDir: join(TEST_DIR, 'scope/a'), scopeDir: join(TEST_DIR, 'scope/b'), scope: true }) ] compilers.forEach(c => { diff --git a/src/index.ts b/src/index.ts index bae0f5263..b716611c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,8 @@ import { fileURLToPath } from 'url' import type * as _ts from 'typescript' import { Module, createRequire as nodeCreateRequire, createRequireFromPath as nodeCreateRequireFromPath } from 'module' import type _createRequire from 'create-require' -// tslint:disable-next-line -const createRequire = nodeCreateRequire ?? nodeCreateRequireFromPath ?? require('create-require') as typeof _createRequire +// tslint:disable-next-line:deprecation +export const createRequire = nodeCreateRequire ?? nodeCreateRequireFromPath ?? require('create-require') as typeof _createRequire export { createRepl, CreateReplOptions, ReplService } from './repl' @@ -56,8 +56,11 @@ export const env = process.env as ProcessEnv */ export interface ProcessEnv { TS_NODE_DEBUG?: string + TS_NODE_CWD?: string + /** @deprecated */ TS_NODE_DIR?: string TS_NODE_EMIT?: string + /** @deprecated */ TS_NODE_SCOPE?: string TS_NODE_FILES?: string TS_NODE_PRETTY?: string @@ -153,10 +156,16 @@ export const VERSION = require('../package.json').version */ export interface CreateOptions { /** - * Specify working directory for config resolution. + * Behave as if invoked within this working directory. Roughly equivalent to `cd $dir && ts-node ...` * * @default process.cwd() */ + cwd?: string + /** + * Legacy alias for `cwd` + * + * @deprecated use `projectSearchDir` or `cwd` + */ dir?: string /** * Emit output files into `.ts-node` directory. @@ -165,11 +174,15 @@ export interface CreateOptions { */ emit?: boolean /** - * Scope compiler to files within `cwd`. + * Scope compiler to files within `scopeDir`. * * @default false */ scope?: boolean + /** + * @default First of: `tsconfig.json` "rootDir" if specified, directory containing `tsconfig.json`, or cwd if no `tsconfig.json` is loaded. + */ + scopeDir?: string /** * Use pretty diagnostic formatter. * @@ -189,7 +202,7 @@ export interface CreateOptions { */ typeCheck?: boolean /** - * Use TypeScript's compiler host API. + * Use TypeScript's compiler host API instead of the language service API. * * @default false */ @@ -201,7 +214,9 @@ export interface CreateOptions { */ logError?: boolean /** - * Load files from `tsconfig.json` on startup. + * Load "files" and "include" from `tsconfig.json` on startup. + * + * Default is to override `tsconfig.json` "files" and "include" to only include the entrypoint script. * * @default false */ @@ -213,16 +228,26 @@ export interface CreateOptions { */ compiler?: string /** - * Override the path patterns to skip compilation. + * Paths which should not be compiled. * - * @default /node_modules/ - * @docsDefault "/node_modules/" + * Each string in the array is converted to a regular expression via `new RegExp()` and tested against source paths prior to compilation. + * + * Source paths are normalized to posix-style separators, relative to the directory containing `tsconfig.json` or to cwd if no `tsconfig.json` is loaded. + * + * Default is to ignore all node_modules subdirectories. + * + * @default ["(?:^|/)node_modules/"] */ ignore?: string[] /** - * Path to TypeScript JSON project file. + * Path to TypeScript config file or directory containing a `tsconfig.json`. + * Similar to the `tsc --project` flag: https://www.typescriptlang.org/docs/handbook/compiler-options.html */ project?: string + /** + * Search for TypeScript config file (`tsconfig.json`) in this or parent directories. + */ + projectSearchDir?: string /** * Skip project config resolution and loading. * @@ -230,13 +255,13 @@ export interface CreateOptions { */ skipProject?: boolean /** - * Skip ignore check. + * Skip ignore check, so that compilation will be attempted for all files with matching extensions. * * @default false */ skipIgnore?: boolean /** - * JSON object to merge with compiler options. + * JSON object to merge with TypeScript `compilerOptions`. * * @allOf [{"$ref": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json#definitions/compilerOptionsDefinition/properties/compilerOptions"}] */ @@ -248,7 +273,7 @@ export interface CreateOptions { /** * Modules to require, like node's `--require` flag. * - * If specified in tsconfig.json, the modules will be resolved relative to the tsconfig.json file. + * If specified in `tsconfig.json`, the modules will be resolved relative to the `tsconfig.json` file. * * If specified programmatically, each input string should be pre-resolved to an absolute path for * best results. @@ -272,6 +297,8 @@ export interface RegisterOptions extends CreateOptions { /** * Re-order file extensions so that TypeScript imports are preferred. * + * For example, when both `index.js` and `index.ts` exist, enabling this option causes `require('./index')` to resolve to `index.ts` instead of `index.js` + * * @default false */ preferTsExts?: boolean @@ -287,6 +314,11 @@ export interface TsConfigOptions extends Omit {} /** @@ -315,9 +347,9 @@ export interface TypeInfo { * variables. */ export const DEFAULTS: RegisterOptions = { - dir: env.TS_NODE_DIR, + cwd: env.TS_NODE_CWD ?? env.TS_NODE_DIR, // tslint:disable-line:deprecation emit: yn(env.TS_NODE_EMIT), - scope: yn(env.TS_NODE_SCOPE), + scope: yn(env.TS_NODE_SCOPE), // tslint:disable-line:deprecation files: yn(env.TS_NODE_FILES), pretty: yn(env.TS_NODE_PRETTY), compiler: env.TS_NODE_COMPILER, @@ -461,34 +493,35 @@ export function register (opts: RegisterOptions = {}): Service { * Create TypeScript compiler instance. */ export function create (rawOptions: CreateOptions = {}): Service { - const dir = rawOptions.dir ?? DEFAULTS.dir + const cwd = resolve(rawOptions.cwd ?? rawOptions.dir ?? DEFAULTS.cwd ?? process.cwd()) // tslint:disable-line:deprecation const compilerName = rawOptions.compiler ?? DEFAULTS.compiler - const cwd = dir ? resolve(dir) : process.cwd() /** * Load the typescript compiler. It is required to load the tsconfig but might - * be changed by the tsconfig, so we sometimes have to do this twice. + * be changed by the tsconfig, so we have to do this twice. */ - function loadCompiler (name: string | undefined) { - const compiler = require.resolve(name || 'typescript', { paths: [cwd, __dirname] }) + function loadCompiler (name: string | undefined, relativeToPath: string) { + const compiler = require.resolve(name || 'typescript', { paths: [relativeToPath, __dirname] }) const ts: typeof _ts = require(compiler) return { compiler, ts } } // Compute minimum options to read the config file. - let { compiler, ts } = loadCompiler(compilerName) + let { compiler, ts } = loadCompiler(compilerName, rawOptions.projectSearchDir ?? rawOptions.project ?? cwd) // Read config file and merge new options between env and CLI options. - const { config, options: tsconfigOptions } = readConfig(cwd, ts, rawOptions) + const { configFilePath, config, options: tsconfigOptions } = readConfig(cwd, ts, rawOptions) const options = assign({}, DEFAULTS, tsconfigOptions || {}, rawOptions) options.require = [ ...tsconfigOptions.require || [], ...rawOptions.require || [] ] - // If `compiler` option changed based on tsconfig, re-load the compiler. - if (options.compiler !== compilerName) { - ({ compiler, ts } = loadCompiler(options.compiler)) + // Re-load the compiler in case it has changed. + // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a + // different compiler than we did above, even if the name has not changed. + if (configFilePath) { + ({ compiler, ts } = loadCompiler(options.compiler, configFilePath)) } const readFile = options.readFile || ts.sys.readFile @@ -508,8 +541,11 @@ export function create (rawOptions: CreateOptions = {}): Service { content: string }>() - const isScoped = options.scope ? (relname: string) => relname.charAt(0) !== '.' : () => true - const shouldIgnore = createIgnore(options.skipIgnore ? [] : ( + const configFileDirname = configFilePath ? dirname(configFilePath) : null + const scopeDir = options.scopeDir ?? config.options.rootDir ?? configFileDirname ?? cwd + const ignoreBaseDir = configFileDirname ?? cwd + const isScoped = options.scope ? (fileName: string) => relative(scopeDir, fileName).charAt(0) !== '.' : () => true + const shouldIgnore = createIgnore(ignoreBaseDir, options.skipIgnore ? [] : ( options.ignore || ['(?:^|/)node_modules/'] ).map(str => new RegExp(str))) @@ -1012,8 +1048,7 @@ export function create (rawOptions: CreateOptions = {}): Service { if (!active) return true const ext = extname(fileName) if (extensions.tsExtensions.includes(ext) || extensions.jsExtensions.includes(ext)) { - const relname = relative(cwd, fileName) - return !isScoped(relname) || shouldIgnore(relname) + return !isScoped(fileName) || shouldIgnore(fileName) } return true } @@ -1024,8 +1059,9 @@ export function create (rawOptions: CreateOptions = {}): Service { /** * Check if the filename should be ignored. */ -function createIgnore (ignore: RegExp[]) { - return (relname: string) => { +function createIgnore (ignoreBaseDir: string, ignore: RegExp[]) { + return (fileName: string) => { + const relname = relative(ignoreBaseDir, fileName) const path = normalizeSlashes(relname) return ignore.some(x => x.test(path)) @@ -1058,7 +1094,7 @@ function registerExtensions ( } if (preferTsExts) { - // tslint:disable-next-line + // tslint:disable-next-line:deprecation const preferredExtensions = new Set([...extensions, ...Object.keys(require.extensions)]) for (const ext of preferredExtensions) reorderRequireExtension(ext) @@ -1128,6 +1164,8 @@ function readConfig ( ts: TSCommon, rawOptions: CreateOptions ): { + // Path of tsconfig file + configFilePath: string | undefined, // Parsed TypeScript configuration. config: _ts.ParsedCommandLine // Options pulled from `tsconfig.json`. @@ -1135,7 +1173,8 @@ function readConfig ( } { let config: any = { compilerOptions: {} } let basePath = cwd - let configFileName: string | undefined = undefined + let configFilePath: string | undefined = undefined + const projectSearchDir = resolve(cwd, rawOptions.projectSearchDir ?? cwd) const { fileExists = ts.sys.fileExists, @@ -1146,23 +1185,24 @@ function readConfig ( // Read project configuration when available. if (!skipProject) { - configFileName = project + configFilePath = project ? resolve(cwd, project) - : ts.findConfigFile(cwd, fileExists) + : ts.findConfigFile(projectSearchDir, fileExists) - if (configFileName) { - const result = ts.readConfigFile(configFileName, readFile) + if (configFilePath) { + const result = ts.readConfigFile(configFilePath, readFile) // Return diagnostics. if (result.error) { return { + configFilePath, config: { errors: [result.error], fileNames: [], options: {} }, options: {} } } config = result.config - basePath = dirname(configFileName) + basePath = dirname(configFilePath) } } @@ -1191,17 +1231,17 @@ function readConfig ( readFile, readDirectory: ts.sys.readDirectory, useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames - }, basePath, undefined, configFileName)) + }, basePath, undefined, configFilePath)) if (tsconfigOptions.require) { // Modules are found relative to the tsconfig file, not the `dir` option - const tsconfigRelativeRequire = createRequire(configFileName!) + const tsconfigRelativeRequire = createRequire(configFilePath!) tsconfigOptions.require = tsconfigOptions.require.map((path: string) => { return tsconfigRelativeRequire.resolve(path) }) } - return { config: fixedConfig, options: tsconfigOptions } + return { configFilePath, config: fixedConfig, options: tsconfigOptions } } /** diff --git a/tests/cwd-and-script-mode/a/index.ts b/tests/cwd-and-script-mode/a/index.ts new file mode 100644 index 000000000..f5fbb60fd --- /dev/null +++ b/tests/cwd-and-script-mode/a/index.ts @@ -0,0 +1,7 @@ +export {} +// Type assertion to please TS 2.7 +const register = process[(Symbol as any).for('ts-node.register.instance')] +console.log(JSON.stringify({ + options: register.options, + config: register.config +})) diff --git a/tests/cwd-and-script-mode/a/tsconfig.json b/tests/cwd-and-script-mode/a/tsconfig.json new file mode 100644 index 000000000..8e2e88090 --- /dev/null +++ b/tests/cwd-and-script-mode/a/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "plugins": [{ + "name": "plugin-a" + }] + } +} diff --git a/tests/cwd-and-script-mode/b/index.ts b/tests/cwd-and-script-mode/b/index.ts new file mode 100644 index 000000000..f5fbb60fd --- /dev/null +++ b/tests/cwd-and-script-mode/b/index.ts @@ -0,0 +1,7 @@ +export {} +// Type assertion to please TS 2.7 +const register = process[(Symbol as any).for('ts-node.register.instance')] +console.log(JSON.stringify({ + options: register.options, + config: register.config +})) diff --git a/tests/cwd-and-script-mode/b/tsconfig.json b/tests/cwd-and-script-mode/b/tsconfig.json new file mode 100644 index 000000000..0c761dd31 --- /dev/null +++ b/tests/cwd-and-script-mode/b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "plugins": [{ + "name": "plugin-b" + }] + } +} diff --git a/tests/import-order/require-compiled.js b/tests/import-order/require-compiled.js new file mode 100644 index 000000000..3977135dd --- /dev/null +++ b/tests/import-order/require-compiled.js @@ -0,0 +1,2 @@ +// indirectly load ./compiled in node < 12 (soon to be end-of-life'd) +require('./compiled')