diff --git a/src/bin.ts b/src/bin.ts index c303129a3..9d923967d 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -7,7 +7,7 @@ import Module = require('module') import arg = require('arg') import { diffLines } from 'diff' import { Script } from 'vm' -import { readFileSync, statSync } from 'fs' +import { readFileSync, statSync, realpathSync } from 'fs' import { homedir } from 'os' import { VERSION, TSError, parse, Register, register } from './index' @@ -154,6 +154,7 @@ export function main (argv: string[]) { } const cwd = dir || 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)) @@ -251,7 +252,28 @@ function getCwd (dir?: string, scriptMode?: boolean, scriptPath?: string) { throw new TypeError('Script mode cannot be combined with `--dir`') } - return dirname(scriptPath) + // 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. + // In extremely rare cases, is is technically possible to resolve the wrong directory, + // because we do not yet know preferTsExts, jsx, nor allowJs. + // See also, justification why this will not happen in real-world situations: + // https://github.com/TypeStrong/ts-node/pull/1009#issuecomment-613017081 + const exts = ['.js', '.jsx', '.ts', '.tsx'] + const extsTemporarilyInstalled: string[] = [] + for (const ext of exts) { + if (!hasOwnProperty(require.extensions, ext)) { // tslint:disable-line + extsTemporarilyInstalled.push(ext) + require.extensions[ext] = function() {} // tslint:disable-line + } + } + try { + return dirname(require.resolve(scriptPath)) + } finally { + for (const ext of extsTemporarilyInstalled) { + delete require.extensions[ext] // tslint:disable-line + } + } } return dir @@ -481,6 +503,11 @@ function isRecoverable (error: TSError) { return error.diagnosticCodes.every(code => RECOVERY_CODES.has(code)) } +/** Safe `hasOwnProperty` */ +function hasOwnProperty (object: any, property: string): boolean { + return Object.prototype.hasOwnProperty.call(object, property) +} + if (require.main === module) { main(process.argv.slice(2)) } diff --git a/src/index.spec.ts b/src/index.spec.ts index 29b90495a..ce3ba1639 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -379,6 +379,14 @@ describe('ts-node', function () { expect(err).to.equal(null) expect(stdout).to.equal('.ts\n') + return done() + }) + }) + it('should read tsconfig relative to realpath, not symlink, in scriptMode', function (done) { + exec(`node ${BIN_SCRIPT_PATH} tests/main-realpath/symlink/symlink.tsx`, function (err, stdout) { + expect(err).to.equal(null) + expect(stdout).to.equal('') + return done() }) }) diff --git a/tests/main-realpath/symlink/symlink.tsx b/tests/main-realpath/symlink/symlink.tsx new file mode 120000 index 000000000..c2c3efb1f --- /dev/null +++ b/tests/main-realpath/symlink/symlink.tsx @@ -0,0 +1 @@ +../target/target.tsx \ No newline at end of file diff --git a/tests/main-realpath/symlink/tsconfig.json b/tests/main-realpath/symlink/tsconfig.json new file mode 100644 index 000000000..9f78b68d0 --- /dev/null +++ b/tests/main-realpath/symlink/tsconfig.json @@ -0,0 +1 @@ +this tsconfig is intentionally invalid, to confirm that ts-node does *not* attempt to parse it diff --git a/tests/main-realpath/target/target.tsx b/tests/main-realpath/target/target.tsx new file mode 100644 index 000000000..1a206f56d --- /dev/null +++ b/tests/main-realpath/target/target.tsx @@ -0,0 +1,4 @@ +// Will throw a compiler error unless ./tsconfig.json is parsed, which enables JSX +function foo() { +
+} diff --git a/tests/main-realpath/target/tsconfig.json b/tests/main-realpath/target/tsconfig.json new file mode 100644 index 000000000..986627de2 --- /dev/null +++ b/tests/main-realpath/target/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "jsx": "react" + } +}