Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BREAKING] make --script-mode the default; add --cwd-mode to switch back to old behavior #1155

Merged
merged 24 commits into from Feb 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
121f654
make --script-mode the default; add --cwd-mode to switch back to old …
cspotcode Nov 19, 2020
691577d
Merge remote-tracking branch 'origin/master' into ab/script-mode-by-d…
cspotcode Jan 17, 2021
5ed1d4a
Fix bug where --script-mode entrypoint require.resolve poisons the re…
cspotcode Jan 17, 2021
deb0602
Merge remote-tracking branch 'origin/master' into ab/script-mode-by-d…
cspotcode Jan 17, 2021
f1b79c7
Merge remote-tracking branch 'origin/master' into ab/script-mode-by-d…
cspotcode Jan 17, 2021
ad9cf74
WIP TODO amend / rewrite this commit
cspotcode Feb 1, 2021
09aa6a3
wip
cspotcode Feb 10, 2021
8f846bd
WIP
cspotcode Feb 13, 2021
6a4a73b
add ts-node-cwd bin, which is equivalent to ts-node --cwd-mode
cspotcode Feb 13, 2021
5639e25
rename projectSearchPath to projectSearchDir
cspotcode Feb 13, 2021
eedb6ba
Revert undesirable changes from WIP commits
cspotcode Feb 13, 2021
67ff9d5
add --cwd-mode and --script-mode tests
cspotcode Feb 13, 2021
ba5e5ec
revert undesirable logging from WIP commits
cspotcode Feb 13, 2021
5338c9b
update tests which relied on --dir affecting to cwd to instead use pr…
cspotcode Feb 13, 2021
060eb14
remove --script-mode from test invocations that don't need it anymore
cspotcode Feb 13, 2021
5901b89
fix lint failures
cspotcode Feb 15, 2021
70ed9f7
Merge remote-tracking branch 'origin/master' into ab/script-mode-by-d…
cspotcode Feb 15, 2021
326e32e
fix tests
cspotcode Feb 15, 2021
c01585e
fix requireResolveNonCached to avoid hack on node 10 & 11
cspotcode Feb 15, 2021
9734642
fix tests to avoid type error on ts2.7
cspotcode Feb 15, 2021
7d7f76b
fix tests on node 10
cspotcode Feb 15, 2021
94f1737
update README and final cleanup
cspotcode Feb 15, 2021
4301739
more cleanup
cspotcode Feb 15, 2021
647a9f1
Load typescript compiler relative to tsconfig.json
cspotcode Feb 15, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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