Skip to content

Commit

Permalink
make --script-mode the default; add --cwd-mode to switch back to old …
Browse files Browse the repository at this point in the history
…behavior; other cwd, project, and dir fixes (#1155)

* Use `--script-mode` by default
* Add `--cwd-mode` flag to opt-out of `--script-mode`
* Add `ts-node-cwd` entry-point that uses `--cwd-mode` by default
* Rename `--dir` to `--cwd`; `TS_NODE_DIR` to `TS_NODE_CWD`; parse legacy names for backwards compatibility
* Rewrite `--cwd` docs to say it changes effective `cwd`
* Add `projectSearchDir` API option to set directory from which tsconfig search is performed
* Fix bug where resolving entry-point location could poison `require.resolve` cache and prevent correct extension from loading (#1220)
* `--cwd` no longer sets `--scope`
* Remove `--scope` from CLI flags and tsconfig-loaded options; it is an API-only option, to match the intended use-case: programmatically installing multiple ts-node instances
* Add `scopeDir` API option
* Deprecate `TS_NODE_SCOPE` env var
* `ignore` rules evaluated relative to `tsconfig.json`, otherwise `cwd`; no longer tied to `--dir`
* `compiler` is loaded relative to `tsconfig.json` instead of `cwd` or entrypoint script (#1225)

---

*Original GH-generated squash summary*

* make --script-mode the default; add --cwd-mode to switch back to old behavior

* Fix bug where --script-mode entrypoint require.resolve poisons the require.resolve cache; causes entrypoint to resolve incorrectly when --prefer-ts-exts is used

* WIP TODO amend / rewrite this commit

* wip

* WIP

* add ts-node-cwd bin, which is equivalent to ts-node --cwd-mode

* rename projectSearchPath to projectSearchDir

* Revert undesirable changes from WIP commits

* add --cwd-mode and --script-mode tests

* revert undesirable logging from WIP commits

* update tests which relied on --dir affecting to cwd to instead use projectSearchDir as needed

* remove --script-mode from test invocations that don't need it anymore

* fix lint failures

* fix tests

* fix requireResolveNonCached to avoid hack on node 10 & 11

* fix tests to avoid type error on ts2.7

* fix tests on node 10

* update README and final cleanup

* more cleanup

* Load typescript compiler relative to tsconfig.json
  • Loading branch information
cspotcode committed Feb 16, 2021
1 parent 080af32 commit bacbeaf
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 93 deletions.
30 changes: 17 additions & 13 deletions README.md
Expand Up @@ -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
Expand All @@ -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
```

Expand Down Expand Up @@ -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`.

Expand All @@ -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

Expand All @@ -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`)
Expand All @@ -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
Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -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",
Expand All @@ -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": [
Expand Down
5 changes: 5 additions & 0 deletions src/bin-cwd.ts
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { main } from './bin'

main(undefined, { '--cwd-mode': true })
80 changes: 55 additions & 25 deletions 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')
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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 <script.ts> instead of current directory
--cwd-mode Use current directory instead of <script.ts> for config resolution
-T, --transpile-only Use TypeScript's faster \`transpileModule\`
-H, --compiler-host Use TypeScript's compiler host API
Expand All @@ -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\`
Expand All @@ -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))
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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 <script.ts>`')
}

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 <script.ts>`')
}
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.
Expand All @@ -240,15 +245,40 @@ 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
}
}
}

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) || []] })
}

/**
Expand Down

0 comments on commit bacbeaf

Please sign in to comment.