From 121f65485aea8aec7892e904c833fae28953fbf0 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 19 Nov 2020 00:21:31 -0500 Subject: [PATCH 01/20] make --script-mode the default; add --cwd-mode to switch back to old behavior --- src/bin.ts | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index bac650106..389a81156 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -43,6 +43,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re // CLI options. '--help': Boolean, + '--cwd-mode': Boolean, '--script-mode': Boolean, '--version': arg.COUNT, @@ -91,7 +92,8 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re const { '--dir': dir, '--help': help = false, - '--script-mode': scriptMode = false, + '--script-mode': scriptMode, + '--cwd-mode': cwdMode, '--version': version = 0, '--require': argsRequire = [], '--eval': code = undefined, @@ -127,7 +129,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 @@ -163,7 +165,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), + dir: getDir(dir, scriptMode, cwdMode, scriptPath), emit, files, pretty, @@ -243,17 +245,26 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re /** * 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`') - } - +function getDir (dir?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPath?: string) { + // Validate `--script-mode` / `--cwd-mode` / `--dir` usage is correct. + if (dir && scriptMode) { + throw new TypeError('--script-mode cannot be combined with --dir') + } + if (dir && cwdMode) { + throw new TypeError('--cwd-mode cannot be combined with --dir') + } + 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 + : dir !== undefined ? 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. @@ -270,7 +281,7 @@ function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) { } } try { - return dirname(require.resolve(scriptPath)) + return dirname(require.resolve(scriptPath!)) } finally { for (const ext of extsTemporarilyInstalled) { delete require.extensions[ext] // tslint:disable-line From 5ed1d4aabf79c78e52aca14540fe2d1369d19477 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 17 Jan 2021 16:34:50 -0500 Subject: [PATCH 02/20] Fix bug where --script-mode entrypoint require.resolve poisons the require.resolve cache; causes entrypoint to resolve incorrectly when --prefer-ts-exts is used --- src/bin.ts | 23 +++++++++++++++++++++-- src/index.ts | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 38d40af0d..2b720e0dc 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node +import * as Path from 'path' import { join, resolve, dirname } from 'path' import { inspect } from 'util' import Module = require('module') @@ -10,7 +11,7 @@ import { createRepl, ReplService } from './repl' -import { VERSION, TSError, parse, register } from './index' +import { VERSION, TSError, parse, register, createRequire } from './index' /** * Main `bin` functionality. @@ -251,7 +252,7 @@ function getDir (dir?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPa } } try { - return dirname(require.resolve(scriptPath!)) + return dirname(requireResolveNonCached(scriptPath!)) } finally { for (const ext of extsTemporarilyInstalled) { delete require.extensions[ext] // tslint:disable-line @@ -262,6 +263,24 @@ function getDir (dir?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPa return dir } +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) { + const {dir, base} = Path.parse(absoluteModuleSpecifier) + const relativeModuleSpecifier = `.${Path.sep}${base}` + + const req = createRequire(join(dir, 'imaginaryUncacheableRequireResolveScript')) + return req.resolve(relativeModuleSpecifier, {paths: [`${ guaranteedNonexistentDirectoryPrefix }${ guaranteedNonexistentDirectorySuffix++ }`, ...req.resolve.paths(relativeModuleSpecifier) || []]}) +} + /** * Evaluate a script. */ diff --git a/src/index.ts b/src/index.ts index 47b986374..9c8946b6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ 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 +export const createRequire = nodeCreateRequire ?? nodeCreateRequireFromPath ?? require('create-require') as typeof _createRequire export { createRepl, CreateReplOptions, ReplService } from './repl' From ad9cf74722bf8a4281fc0f2198de132af8c46d45 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 31 Jan 2021 19:23:24 -0500 Subject: [PATCH 03/20] WIP TODO amend / rewrite this commit --- src/bin.ts | 29 ++++++++++++----------------- src/index.ts | 48 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 2b720e0dc..63257d0f1 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -33,7 +33,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re '--version': arg.COUNT, // Project options. - '--dir': String, + '--cwd': String, '--files': Boolean, '--compiler': String, '--compiler-options': parse, @@ -64,7 +64,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 @@ -75,7 +76,7 @@ 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, '--cwd-mode': cwdMode, @@ -124,7 +125,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 + --cwd Behave as if invoked within this working directory. --scope Scope compiler to files within \`cwd\` only --files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup --pretty Use pretty diagnostic formatter (usually enabled by default) @@ -143,7 +144,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)) @@ -152,7 +153,8 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re // Register the TypeScript compiler instance. const service = register({ - dir: getDir(dir, scriptMode, cwdMode, scriptPath), + cwd, + projectSearchPath: getProjectSearchPath(cwd, scriptMode, cwdMode, scriptPath), emit, files, pretty, @@ -214,16 +216,10 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re } /** - * Get project path from args. + * Get project search path from args. */ -function getDir (dir?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPath?: string) { - // Validate `--script-mode` / `--cwd-mode` / `--dir` usage is correct. - if (dir && scriptMode) { - throw new TypeError('--script-mode cannot be combined with --dir') - } - if (dir && cwdMode) { - throw new TypeError('--cwd-mode cannot be combined with --dir') - } +function getProjectSearchPath (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') } @@ -233,7 +229,6 @@ function getDir (dir?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPa const doScriptMode = scriptMode === true ? true : cwdMode === true ? false - : dir !== undefined ? false : !!scriptPath if (doScriptMode) { // Use node's own resolution behavior to ensure we follow symlinks. @@ -260,7 +255,7 @@ function getDir (dir?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPa } } - return dir + return cwd } const guaranteedNonexistentDirectoryPrefix = resolve(__dirname, 'doesnotexist') diff --git a/src/index.ts b/src/index.ts index 13d524bba..0d166a46b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,6 +56,8 @@ export const env = process.env as ProcessEnv */ export interface ProcessEnv { TS_NODE_DEBUG?: string + TS_NODE_CWD?: string + /** @deprecated legacy alias to TS_NODE_CWD */ TS_NODE_DIR?: string TS_NODE_EMIT?: string TS_NODE_SCOPE?: string @@ -153,10 +155,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 `projectSearchPath` or `cwd` + */ dir?: string /** * Emit output files into `.ts-node` directory. @@ -189,7 +197,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 +209,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 +223,24 @@ export interface CreateOptions { */ compiler?: string /** - * Override the path patterns to skip compilation. + * Paths which should not be compiled. + * + * Each string in the array is converted to a regular expression via `new RegExp()` and tested against source paths prior to compilation. * - * @default /node_modules/ - * @docsDefault "/node_modules/" + * 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. + */ + projectSearchPath?: string /** * Skip project config resolution and loading. * @@ -230,13 +248,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 +266,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 +290,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 +307,8 @@ export interface TsConfigOptions extends Omit {} /** @@ -315,7 +337,7 @@ export interface TypeInfo { * variables. */ export const DEFAULTS: RegisterOptions = { - dir: env.TS_NODE_DIR, + cwd: env.TS_NODE_CWD ?? env.TS_NODE_DIR, emit: yn(env.TS_NODE_EMIT), scope: yn(env.TS_NODE_SCOPE), files: yn(env.TS_NODE_FILES), @@ -458,9 +480,9 @@ 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 ?? DEFAULTS.cwd ?? process.cwd()) const compilerName = rawOptions.compiler ?? DEFAULTS.compiler - const cwd = dir ? resolve(dir) : process.cwd() + const projectSearchPath = resolve(cwd, rawOptions.projectSearchPath ?? cwd) /** * Load the typescript compiler. It is required to load the tsconfig but might From 09aa6a3d21a730025f61f54ac375aa8c404f98c1 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 10 Feb 2021 17:31:59 -0500 Subject: [PATCH 04/20] wip --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 0d166a46b..2ab7f8010 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,7 +57,6 @@ export const env = process.env as ProcessEnv export interface ProcessEnv { TS_NODE_DEBUG?: string TS_NODE_CWD?: string - /** @deprecated legacy alias to TS_NODE_CWD */ TS_NODE_DIR?: string TS_NODE_EMIT?: string TS_NODE_SCOPE?: string From 8f846bd55806213782db9470c8018aef5dd7fb08 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 12 Feb 2021 20:04:31 -0500 Subject: [PATCH 05/20] WIP --- package.json | 4 +- src/bin.ts | 15 ++++--- src/index.spec.ts | 7 ++-- src/index.ts | 99 +++++++++++++++++++++++++++++++++-------------- 4 files changed, 84 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index ec831c6fc..b2987cf12 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,9 @@ "build-tsc": "tsc", "build-configSchema": "typescript-json-schema --topRef --refs --validationKeywords allOf --out tsconfig.schema.json tsconfig.json TsConfigSchema && node --require ./register ./scripts/create-merged-schema", "build-pack": "node ./scripts/build-pack.js", - "test-spec": "mocha dist/**/*.spec.js -R spec --bail", + "test-spec": "mocha dist/**/*.spec.js -R spec", "test-cov": "nyc mocha -- \"dist/**/*.spec.js\" -R spec --bail", - "test": "npm run build && npm run lint && npm run test-cov --", + "test": "npm run build-tsc && npm run build-pack && npm run test-spec --", "coverage-report": "nyc report --reporter=lcov", "prepare": "npm run build-nopack" }, diff --git a/src/bin.ts b/src/bin.ts index 63257d0f1..113504960 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node -import * as Path from 'path' -import { join, resolve, dirname } from 'path' +import { join, resolve, dirname, parse as parsePath, sep as pathSep } from 'path' import { inspect } from 'util' import Module = require('module') import arg = require('arg') @@ -126,7 +125,6 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re -O, --compiler-options [opts] JSON object to merge with compiler options --cwd Behave as if invoked within this working directory. - --scope Scope compiler to files within \`cwd\` only --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\` @@ -151,10 +149,10 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re const replService = createRepl({ state }) const { evalAwarePartialHost } = replService + console.error([{__filename, cwd, scriptMode, cwdMode, scriptPath}, getProjectSearchPath(cwd, scriptMode, cwdMode, scriptPath)]) // Register the TypeScript compiler instance. const service = register({ cwd, - projectSearchPath: getProjectSearchPath(cwd, scriptMode, cwdMode, scriptPath), emit, files, pretty, @@ -164,6 +162,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re ignore, preferTsExts, logError, + projectSearchPath: getProjectSearchPath(cwd, scriptMode, cwdMode, scriptPath), project, skipProject, skipIgnore, @@ -268,12 +267,12 @@ let guaranteedNonexistentDirectorySuffix = 0 * 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) { - const {dir, base} = Path.parse(absoluteModuleSpecifier) - const relativeModuleSpecifier = `.${Path.sep}${base}` +function requireResolveNonCached (absoluteModuleSpecifier: string) { + const { dir, base } = parsePath(absoluteModuleSpecifier) + const relativeModuleSpecifier = `.${pathSep}${base}` const req = createRequire(join(dir, 'imaginaryUncacheableRequireResolveScript')) - return req.resolve(relativeModuleSpecifier, {paths: [`${ guaranteedNonexistentDirectoryPrefix }${ guaranteedNonexistentDirectorySuffix++ }`, ...req.resolve.paths(relativeModuleSpecifier) || []]}) + return req.resolve(relativeModuleSpecifier, { paths: [`${ guaranteedNonexistentDirectoryPrefix }${ guaranteedNonexistentDirectorySuffix++ }`, ...req.resolve.paths(relativeModuleSpecifier) || []] }) } /** diff --git a/src/index.spec.ts b/src/index.spec.ts index 0f7e0f908..111b22570 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -107,7 +107,8 @@ describe('ts-node', function () { }) }) it('shows version of compiler via -vv', function (done) { - exec(`${cmdNoProject} -vv`, function (err, stdout) { + exec(`${cmdNoProject} -vv`, function (err, stdout, stderr) { + console.log(stderr) expect(err).to.equal(null) expect(stdout.trim()).to.equal( `ts-node v${ testsDirRequire('ts-node/package').version }\n` + @@ -724,8 +725,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({ scopeDir: join(TEST_DIR, 'scope/a'), scope: true }), + register({ scopeDir: join(TEST_DIR, 'scope/b'), scope: true }) ] compilers.forEach(c => { diff --git a/src/index.ts b/src/index.ts index 2ab7f8010..3dfff19f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ 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 +// tslint:disable-next-line:deprecation export const createRequire = nodeCreateRequire ?? nodeCreateRequireFromPath ?? require('create-require') as typeof _createRequire export { createRepl, CreateReplOptions, ReplService } from './repl' @@ -57,8 +57,10 @@ 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 @@ -172,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. * @@ -226,6 +232,8 @@ export interface CreateOptions { * * 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/"] @@ -308,6 +316,9 @@ export interface TsConfigOptions extends Omit {} /** @@ -336,9 +347,9 @@ export interface TypeInfo { * variables. */ export const DEFAULTS: RegisterOptions = { - cwd: env.TS_NODE_CWD ?? 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, @@ -479,25 +490,25 @@ export function register (opts: RegisterOptions = {}): Service { * Create TypeScript compiler instance. */ export function create (rawOptions: CreateOptions = {}): Service { - const cwd = resolve(rawOptions.cwd ?? DEFAULTS.cwd ?? process.cwd()) + const cwd = resolve(rawOptions.cwd ?? rawOptions.dir ?? DEFAULTS.cwd ?? process.cwd()) // tslint:disable-line:deprecation const compilerName = rawOptions.compiler ?? DEFAULTS.compiler - const projectSearchPath = resolve(cwd, rawOptions.projectSearchPath ?? 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. */ - function loadCompiler (name: string | undefined) { - const compiler = require.resolve(name || 'typescript', { paths: [cwd, __dirname] }) + function loadCompiler (name: string | undefined, relativeToPath: string) { + console.log(relativeToPath) + 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.projectSearchPath ?? 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 || [], @@ -506,7 +517,10 @@ export function create (rawOptions: CreateOptions = {}): Service { // If `compiler` option changed based on tsconfig, re-load the compiler. if (options.compiler !== compilerName) { - ({ compiler, ts } = loadCompiler(options.compiler)) + // TODO compiler name might not have changed, but compiler to load might be different based on where + // tsconfig is located + // Should re-attempt compiler resolution + ({ compiler, ts } = loadCompiler(options.compiler, configFilePath!)) } const readFile = options.readFile || ts.sys.readFile @@ -526,8 +540,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))) @@ -1030,8 +1047,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 } @@ -1042,8 +1058,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)) @@ -1076,7 +1093,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) @@ -1146,6 +1163,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`. @@ -1153,7 +1172,8 @@ function readConfig ( } { let config: any = { compilerOptions: {} } let basePath = cwd - let configFileName: string | undefined = undefined + let configFilePath: string | undefined = undefined + const projectSearchPath = resolve(cwd, rawOptions.projectSearchPath ?? cwd) const { fileExists = ts.sys.fileExists, @@ -1164,28 +1184,29 @@ function readConfig ( // Read project configuration when available. if (!skipProject) { - configFileName = project + configFilePath = project ? resolve(cwd, project) - : ts.findConfigFile(cwd, fileExists) + : ts.findConfigFile(projectSearchPath, 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) } } // Fix ts-node options that come from tsconfig.json - const tsconfigOptions: TsConfigOptions = Object.assign({}, config['ts-node']) + const tsconfigOptions: TsConfigOptions = Object.assign({}, filterRecognizedTsConfigTsNodeOptions(config['ts-node'])) // Remove resolution of "files". const files = rawOptions.files ?? tsconfigOptions.files ?? DEFAULTS.files @@ -1209,17 +1230,39 @@ 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 } +} + +/** + * Given the raw "ts-node" sub-object from a tsconfig, return an object with only the properties + * recognized by "ts-node" + */ +function filterRecognizedTsConfigTsNodeOptions (jsonObject: any): TsConfigOptions { + if (jsonObject == null) return jsonObject + const { + compiler, compilerHost, compilerOptions, emit, files, ignore, + ignoreDiagnostics, logError, preferTsExts, pretty, require, skipIgnore, + transpileOnly, typeCheck + } = jsonObject as TsConfigOptions + const filteredTsConfigOptions = { + compiler, compilerHost, compilerOptions, emit, files, ignore, + ignoreDiagnostics, logError, preferTsExts, pretty, require, skipIgnore, + transpileOnly, typeCheck + } + // Use the typechecker to make sure this implementation has the correct set of properties + const catchExtraneousProps: keyof TsConfigOptions = null as any as keyof typeof filteredTsConfigOptions + const catchMissingProps: keyof typeof filteredTsConfigOptions = null as any as keyof TsConfigOptions + return filteredTsConfigOptions } /** From 6a4a73b5589cdbbf6faae036fe22d304d7503321 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 12 Feb 2021 22:20:37 -0500 Subject: [PATCH 06/20] add ts-node-cwd bin, which is equivalent to ts-node --cwd-mode --- package.json | 3 +++ src/bin-cwd.ts | 5 +++++ 2 files changed, 8 insertions(+) create mode 100644 src/bin-cwd.ts diff --git a/package.json b/package.json index b2987cf12..c83011396 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 }) From 5639e25226abbb0fda0987acb251379964146d92 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 12 Feb 2021 22:22:14 -0500 Subject: [PATCH 07/20] rename projectSearchPath to projectSearchDir --- src/bin.ts | 4 ++-- src/index.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 113504960..8d0456781 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -162,7 +162,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re ignore, preferTsExts, logError, - projectSearchPath: getProjectSearchPath(cwd, scriptMode, cwdMode, scriptPath), + projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), project, skipProject, skipIgnore, @@ -217,7 +217,7 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re /** * Get project search path from args. */ -function getProjectSearchPath (cwd?: string, scriptMode?: boolean, cwdMode?: boolean, scriptPath?: string) { +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') diff --git a/src/index.ts b/src/index.ts index 3dfff19f2..740dd57cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -164,7 +164,7 @@ export interface CreateOptions { /** * Legacy alias for `cwd` * - * @deprecated use `projectSearchPath` or `cwd` + * @deprecated use `projectSearchDir` or `cwd` */ dir?: string /** @@ -247,7 +247,7 @@ export interface CreateOptions { /** * Search for TypeScript config file (`tsconfig.json`) in this or parent directories. */ - projectSearchPath?: string + projectSearchDir?: string /** * Skip project config resolution and loading. * @@ -315,7 +315,7 @@ export interface TsConfigOptions extends Omit Date: Fri, 12 Feb 2021 22:29:31 -0500 Subject: [PATCH 08/20] Revert undesirable changes from WIP commits --- package.json | 4 ++-- src/bin.ts | 1 - src/index.ts | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index c83011396..6da4a0f90 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,9 @@ "build-tsc": "tsc", "build-configSchema": "typescript-json-schema --topRef --refs --validationKeywords allOf --out tsconfig.schema.json tsconfig.json TsConfigSchema && node --require ./register ./scripts/create-merged-schema", "build-pack": "node ./scripts/build-pack.js", - "test-spec": "mocha dist/**/*.spec.js -R spec", + "test-spec": "mocha dist/**/*.spec.js -R spec --bail", "test-cov": "nyc mocha -- \"dist/**/*.spec.js\" -R spec --bail", - "test": "npm run build-tsc && npm run build-pack && npm run test-spec --", + "test": "npm run build && npm run lint && npm run test-cov --", "coverage-report": "nyc report --reporter=lcov", "prepare": "npm run build-nopack" }, diff --git a/src/bin.ts b/src/bin.ts index 8d0456781..10ddbbe11 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -149,7 +149,6 @@ export function main (argv: string[] = process.argv.slice(2), entrypointArgs: Re const replService = createRepl({ state }) const { evalAwarePartialHost } = replService - console.error([{__filename, cwd, scriptMode, cwdMode, scriptPath}, getProjectSearchPath(cwd, scriptMode, cwdMode, scriptPath)]) // Register the TypeScript compiler instance. const service = register({ cwd, diff --git a/src/index.ts b/src/index.ts index 740dd57cf..71d1f5340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -498,7 +498,6 @@ export function create (rawOptions: CreateOptions = {}): Service { * be changed by the tsconfig, so we sometimes have to do this twice. */ function loadCompiler (name: string | undefined, relativeToPath: string) { - console.log(relativeToPath) const compiler = require.resolve(name || 'typescript', { paths: [relativeToPath, __dirname] }) const ts: typeof _ts = require(compiler) return { compiler, ts } From 67ff9d5d443b1b67f8711dcaa118158a03f2a07a Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 12 Feb 2021 22:31:15 -0500 Subject: [PATCH 09/20] add --cwd-mode and --script-mode tests --- src/index.spec.ts | 45 ++++++++++++++++++++--- tests/cwd-and-script-mode/a/index.ts | 6 +++ tests/cwd-and-script-mode/a/tsconfig.json | 7 ++++ tests/cwd-and-script-mode/b/index.ts | 6 +++ tests/cwd-and-script-mode/b/tsconfig.json | 7 ++++ 5 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 tests/cwd-and-script-mode/a/index.ts create mode 100644 tests/cwd-and-script-mode/a/tsconfig.json create mode 100644 tests/cwd-and-script-mode/b/index.ts create mode 100644 tests/cwd-and-script-mode/b/tsconfig.json diff --git a/src/index.spec.ts b/src/index.spec.ts index 111b22570..13477e5e3 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -21,6 +21,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\+]+=*$/ @@ -66,6 +67,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') @@ -524,17 +527,49 @@ describe('ts-node', function () { }) if (semver.gte(ts.version, '2.7.0')) { - it('should support script mode', function (done) { - exec(`${BIN_SCRIPT_PATH} tests/scope/a/log`, function (err, stdout) { + it('should locate tsconfig relative to entry-point by default', 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 entry-point via ts-node-script', function (done) { + exec(`${BIN_SCRIPT_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 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 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.match(/plugin-b/) + + 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 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} tests/main-realpath/symlink/symlink.tsx`, function (err, stdout) { + exec(`${BIN_PATH} tests/main-realpath/symlink/symlink.tsx`, function (err, stdout) { expect(err).to.equal(null) expect(stdout).to.equal('') 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..db8db2640 --- /dev/null +++ b/tests/cwd-and-script-mode/a/index.ts @@ -0,0 +1,6 @@ +export {} +const register = process[Symbol.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..db8db2640 --- /dev/null +++ b/tests/cwd-and-script-mode/b/index.ts @@ -0,0 +1,6 @@ +export {} +const register = process[Symbol.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" + }] + } +} From ba5e5ec11ccc037940878bcc9f7c14bb3e3c6435 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 12 Feb 2021 22:31:30 -0500 Subject: [PATCH 10/20] revert undesirable logging from WIP commits --- src/index.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 13477e5e3..5929586c4 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -110,8 +110,7 @@ describe('ts-node', function () { }) }) it('shows version of compiler via -vv', function (done) { - exec(`${cmdNoProject} -vv`, function (err, stdout, stderr) { - console.log(stderr) + exec(`${cmdNoProject} -vv`, function (err, stdout) { expect(err).to.equal(null) expect(stdout.trim()).to.equal( `ts-node v${ testsDirRequire('ts-node/package').version }\n` + From 5338c9bc70d0047c491e9a898bc8a622e36b8490 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 12 Feb 2021 22:32:00 -0500 Subject: [PATCH 11/20] update tests which relied on --dir affecting to cwd to instead use projectSearchDir as needed --- src/index.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 5929586c4..485b1db5d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -759,8 +759,8 @@ describe('ts-node', function () { registered.enabled(false) const compilers = [ - register({ scopeDir: join(TEST_DIR, 'scope/a'), scope: true }), - register({ scopeDir: 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 => { From 060eb14b11c39960395c547ab3c8aa1ba5fee708 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 12 Feb 2021 22:32:21 -0500 Subject: [PATCH 12/20] remove --script-mode from test invocations that don't need it anymore --- src/index.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 485b1db5d..1ea30255f 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -661,7 +661,7 @@ describe('ts-node', function () { }) it('should transpile files inside a node_modules directory when not ignored', function (done) { - exec(`${cmdNoProject} --script-mode tests/from-node-modules/from-node-modules`, function (err, stdout, stderr) { + exec(`${cmdNoProject} tests/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: { @@ -681,11 +681,11 @@ describe('ts-node', function () { describe('should respect maxNodeModulesJsDepth', function () { it('for unscoped modules', function (done) { - exec(`${cmdNoProject} --script-mode tests/maxnodemodulesjsdepth`, function (err, stdout, stderr) { + exec(`${cmdNoProject} tests/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" + + "tests/maxnodemodulesjsdepth/other.ts(4,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' ) done() @@ -693,11 +693,11 @@ describe('ts-node', function () { }) it('for @scoped modules', function (done) { - exec(`${cmdNoProject} --script-mode tests/maxnodemodulesjsdepth-scoped`, function (err, stdout, stderr) { + exec(`${cmdNoProject} tests/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" + + "tests/maxnodemodulesjsdepth-scoped/other.ts(7,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' ) done() From 5901b89f179649a3366b55089cb337b5b756c14d Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 14 Feb 2021 21:35:20 -0500 Subject: [PATCH 13/20] fix lint failures --- src/index.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 1ea30255f..f74c36db1 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -527,7 +527,7 @@ describe('ts-node', function () { if (semver.gte(ts.version, '2.7.0')) { it('should locate tsconfig relative to entry-point by default', function (done) { - exec(`${BIN_PATH} --cwd-mode ../a/index`, {cwd: join(TEST_DIR, 'cwd-and-script-mode/b')}, function (err, stdout) { + 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.match(/plugin-b/) @@ -535,7 +535,7 @@ describe('ts-node', function () { }) }) it('should locate tsconfig relative to entry-point via ts-node-script', function (done) { - exec(`${BIN_SCRIPT_PATH} --script-mode ../a/index`, {cwd: join(TEST_DIR, 'cwd-and-script-mode/b')}, function (err, stdout) { + exec(`${BIN_SCRIPT_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/) @@ -543,7 +543,7 @@ describe('ts-node', function () { }) }) 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) { + 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/) @@ -551,7 +551,7 @@ describe('ts-node', function () { }) }) 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) { + 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.match(/plugin-b/) @@ -559,7 +559,7 @@ describe('ts-node', function () { }) }) 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) { + 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/) From 326e32eec20bb20c91e0d3b225e877d99a36a8ec Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 15 Feb 2021 03:06:09 -0500 Subject: [PATCH 14/20] fix tests --- src/index.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 7818fb265..8996d3318 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -537,15 +537,15 @@ describe('ts-node', function () { if (semver.gte(ts.version, '2.7.0')) { it('should locate tsconfig relative to entry-point by default', function (done) { - exec(`${BIN_PATH} --cwd-mode ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { + 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-b/) + 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} --script-mode ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { + 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/) @@ -560,16 +560,16 @@ describe('ts-node', function () { 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) { + 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 via ts-node-cwd', function (done) { - exec(`${BIN_CWD_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b') }, function (err, stdout) { + 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.match(/plugin-b/) @@ -695,7 +695,7 @@ describe('ts-node', function () { expect(err).to.not.equal(null) expect(stderr.replace(/\r\n/g, '\n')).to.contain( 'TSError: ⨯ Unable to compile TypeScript:\n' + - "tests/maxnodemodulesjsdepth/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() @@ -707,7 +707,7 @@ describe('ts-node', function () { expect(err).to.not.equal(null) expect(stderr.replace(/\r\n/g, '\n')).to.contain( 'TSError: ⨯ Unable to compile TypeScript:\n' + - "tests/maxnodemodulesjsdepth-scoped/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() From c01585e61c3b0fc1131df7b8461a24e7386a66cd Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 15 Feb 2021 04:59:47 -0500 Subject: [PATCH 15/20] fix requireResolveNonCached to avoid hack on node 10 & 11 --- src/bin.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/bin.ts b/src/bin.ts index 10ddbbe11..55cc28f3d 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -267,8 +267,15 @@ let guaranteedNonexistentDirectorySuffix = 0 * 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 = `.${pathSep}${base}` + const relativeModuleSpecifier = `./${base}` const req = createRequire(join(dir, 'imaginaryUncacheableRequireResolveScript')) return req.resolve(relativeModuleSpecifier, { paths: [`${ guaranteedNonexistentDirectoryPrefix }${ guaranteedNonexistentDirectorySuffix++ }`, ...req.resolve.paths(relativeModuleSpecifier) || []] }) From 9734642849a4329798677808201d350d3bb70c73 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 15 Feb 2021 05:00:04 -0500 Subject: [PATCH 16/20] fix tests to avoid type error on ts2.7 --- tests/cwd-and-script-mode/a/index.ts | 3 ++- tests/cwd-and-script-mode/b/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/cwd-and-script-mode/a/index.ts b/tests/cwd-and-script-mode/a/index.ts index db8db2640..f5fbb60fd 100644 --- a/tests/cwd-and-script-mode/a/index.ts +++ b/tests/cwd-and-script-mode/a/index.ts @@ -1,5 +1,6 @@ export {} -const register = process[Symbol.for('ts-node.register.instance')] +// 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/index.ts b/tests/cwd-and-script-mode/b/index.ts index db8db2640..f5fbb60fd 100644 --- a/tests/cwd-and-script-mode/b/index.ts +++ b/tests/cwd-and-script-mode/b/index.ts @@ -1,5 +1,6 @@ export {} -const register = process[Symbol.for('ts-node.register.instance')] +// 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 From 7d7f76b36babf88670f6145c9a2ad08dbdd19407 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 15 Feb 2021 05:28:40 -0500 Subject: [PATCH 17/20] fix tests on node 10 --- src/index.spec.ts | 6 ++++-- tests/import-order/require-compiled.js | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 tests/import-order/require-compiled.js diff --git a/src/index.spec.ts b/src/index.spec.ts index 8996d3318..37f9be53d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -470,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') @@ -480,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') 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') From 94f173734447f6b804016c1b69581cf7d1bbf5e4 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 15 Feb 2021 17:39:44 -0500 Subject: [PATCH 18/20] update README and final cleanup --- README.md | 34 +++++++++++++++++++++------------- src/bin.ts | 2 +- src/index.ts | 27 +-------------------------- 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index cb9994de8..1cacb46f2 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,12 +177,17 @@ 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 _The name of the environment variable and the option's default value are denoted in parentheses._ +| CLI | Environment | API | Description | +|---|---|---|---| +| `-T, --transpile-only` | `TS_NODE_TRANSPILE_ONLY` | `transpileOnly` | Use TypeScript's faster `transpileModule` (default: `false`) | + * `-T, --transpile-only` Use TypeScript's faster `transpileModule` (`TS_NODE_TRANSPILE_ONLY`, default: `false`) * `-H, --compiler-host` Use TypeScript's compiler host API (`TS_NODE_COMPILER_HOST`, default: `false`) * `-I, --ignore [pattern]` Override the path patterns to skip compilation (`TS_NODE_IGNORE`, default: `/node_modules/`) @@ -189,8 +195,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 +206,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 +227,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/src/bin.ts b/src/bin.ts index 55cc28f3d..d07a124e8 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { join, resolve, dirname, parse as parsePath, sep as pathSep } from 'path' +import { join, resolve, dirname, parse as parsePath } from 'path' import { inspect } from 'util' import Module = require('module') import arg = require('arg') diff --git a/src/index.ts b/src/index.ts index 78c61e963..dbaedb1a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -519,9 +519,6 @@ export function create (rawOptions: CreateOptions = {}): Service { // If `compiler` option changed based on tsconfig, re-load the compiler. if (options.compiler !== compilerName) { - // TODO compiler name might not have changed, but compiler to load might be different based on where - // tsconfig is located - // Should re-attempt compiler resolution ({ compiler, ts } = loadCompiler(options.compiler, configFilePath!)) } @@ -1208,7 +1205,7 @@ function readConfig ( } // Fix ts-node options that come from tsconfig.json - const tsconfigOptions: TsConfigOptions = Object.assign({}, filterRecognizedTsConfigTsNodeOptions(config['ts-node'])) + const tsconfigOptions: TsConfigOptions = Object.assign({}, config['ts-node']) // Remove resolution of "files". const files = rawOptions.files ?? tsconfigOptions.files ?? DEFAULTS.files @@ -1245,28 +1242,6 @@ function readConfig ( return { configFilePath, config: fixedConfig, options: tsconfigOptions } } -/** - * Given the raw "ts-node" sub-object from a tsconfig, return an object with only the properties - * recognized by "ts-node" - */ -function filterRecognizedTsConfigTsNodeOptions (jsonObject: any): TsConfigOptions { - if (jsonObject == null) return jsonObject - const { - compiler, compilerHost, compilerOptions, emit, files, ignore, - ignoreDiagnostics, logError, preferTsExts, pretty, require, skipIgnore, - transpileOnly, typeCheck - } = jsonObject as TsConfigOptions - const filteredTsConfigOptions = { - compiler, compilerHost, compilerOptions, emit, files, ignore, - ignoreDiagnostics, logError, preferTsExts, pretty, require, skipIgnore, - transpileOnly, typeCheck - } - // Use the typechecker to make sure this implementation has the correct set of properties - const catchExtraneousProps: keyof TsConfigOptions = null as any as keyof typeof filteredTsConfigOptions - const catchMissingProps: keyof typeof filteredTsConfigOptions = null as any as keyof TsConfigOptions - return filteredTsConfigOptions -} - /** * Internal source output. */ From 430173969fe7be6b0f42446b81ec1e6255e7670a Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 15 Feb 2021 17:41:48 -0500 Subject: [PATCH 19/20] more cleanup --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 1cacb46f2..ec75beb53 100644 --- a/README.md +++ b/README.md @@ -184,10 +184,6 @@ ts-node --compiler ntypescript --project src/tsconfig.json hello-world.ts _The name of the environment variable and the option's default value are denoted in parentheses._ -| CLI | Environment | API | Description | -|---|---|---|---| -| `-T, --transpile-only` | `TS_NODE_TRANSPILE_ONLY` | `transpileOnly` | Use TypeScript's faster `transpileModule` (default: `false`) | - * `-T, --transpile-only` Use TypeScript's faster `transpileModule` (`TS_NODE_TRANSPILE_ONLY`, default: `false`) * `-H, --compiler-host` Use TypeScript's compiler host API (`TS_NODE_COMPILER_HOST`, default: `false`) * `-I, --ignore [pattern]` Override the path patterns to skip compilation (`TS_NODE_IGNORE`, default: `/node_modules/`) From 647a9f1e738d94783e2a1eaf3b7ac3a41d28ea4e Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Mon, 15 Feb 2021 18:38:05 -0500 Subject: [PATCH 20/20] Load typescript compiler relative to tsconfig.json --- src/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index dbaedb1a4..b716611c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -498,7 +498,7 @@ export function create (rawOptions: CreateOptions = {}): Service { /** * 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, relativeToPath: string) { const compiler = require.resolve(name || 'typescript', { paths: [relativeToPath, __dirname] }) @@ -517,9 +517,11 @@ export function create (rawOptions: CreateOptions = {}): Service { ...rawOptions.require || [] ] - // If `compiler` option changed based on tsconfig, re-load the compiler. - if (options.compiler !== compilerName) { - ({ compiler, ts } = loadCompiler(options.compiler, configFilePath!)) + // 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