-
-
Notifications
You must be signed in to change notification settings - Fork 543
Description
Desired behavior
I would like to be able to have the following script at ~/projects/ts-hello/src/hello.ts:
#!/usr/bin/env ts-node
import os from 'os';
console.log(`Hello, ${os.userInfo().username}!`);
console.log('Your arguments:', process.argv.slice(2));… and have npm link the executable with the following configuration at ~/projects/ts-hello/package.json:
{
"name": "ts-hello",
"version": "0.0.0",
"private": true,
"bin": {
"hello": "src/hello.ts"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^13.9.8"
}
}For this to work, both typescript and ts-node have to be installed globally (see more on this below):
$ npm install --global typescript ts-nodeAdditionally, we need to enable ES module interoperability with the following argument in ~/projects/ts-hello/tsconfig.json:
{
"compilerOptions": {
"esModuleInterop": true
}
}We can now change to the project directory, install the dependency and link the package:
$ cd ~/projects/ts-hello
$ npm install
$ npm linkActual behavior
Since the motivation for this issue is in part to document the best approach for others (as far as I can tell a lot of people already struggled with this problem before me), I will elaborate step by step how to arrive at an acceptable solution so that search engines can index all the error messages as well.
First, let's test that the script works without relying on the shebang:
$ cd ~/projects/ts-hello
$ ts-node src/hello.ts first second
Hello, <user>!
Your arguments: [ 'first', 'second' ]The problems start when you move a directory up:
$ cd ..
$ pwd
~/projects
$ ts-node ts-hello/src/hello.ts first second
/usr/local/lib/node_modules/ts-node/src/index.ts:421
return new TSError(diagnosticText, diagnosticCodes)
^
TSError: ⨯ Unable to compile TypeScript:
ts-hello/src/hello.ts:7:16 - error TS2307: Cannot find module 'os'.
7 import os from 'os';
~~~~
ts-hello/src/hello.ts:10:32 - error TS2580: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i @types/node`.
10 console.log('Your arguments:', process.argv.slice(2));
~~~~~~~
at createTSError (/usr/local/lib/node_modules/ts-node/src/index.ts:421:12)
at reportTSError (/usr/local/lib/node_modules/ts-node/src/index.ts:425:19)
at getOutput (/usr/local/lib/node_modules/ts-node/src/index.ts:553:36)
at Object.compile (/usr/local/lib/node_modules/ts-node/src/index.ts:758:32)
at Module.m._compile (/usr/local/lib/node_modules/ts-node/src/index.ts:837:43)
at Module._extensions..js (internal/modules/cjs/loader.js:1167:10)
at Object.require.extensions.<computed> [as .ts] (/usr/local/lib/node_modules/ts-node/src/index.ts:840:12)
at Module.load (internal/modules/cjs/loader.js:996:32)
at Function.Module._load (internal/modules/cjs/loader.js:896:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)Since node ts-hello/src/hello.js first second works without problems given the following script at ~/projects/ts-hello/src/hello.js:
#!/usr/bin/env node
const os = require('os');
console.log(`Hello, ${os.userInfo().username}!`);
console.log('Your arguments:', process.argv.slice(2));… this is disappointing but can easily be fixed by providing the TypeScript JSON project file explicitly as documented:
$ ts-node --project ts-hello/tsconfig.json ts-hello/src/hello.ts first second(And no, this is not just due to os being a native Node.js module. node also picks up dependencies specified in ~/projects/ts-hello/package.json while ts-node does not. It would be great if ts-node could work as similar to node as possible, just on .ts instead of .js files, of course.)
You can simplify the above with the undocumented --script-mode option that I discovered:
$ ts-node --script-mode ts-hello/src/hello.ts first secondThere is also an undocumented command that does the same:
$ ts-node-script ts-hello/src/hello.ts first second(Given the usefulness of these options, why are they not mentioned in the README?)
Once we give our script the permission to execute it directly (based on the shebang in the first line) with chmod u+x ts-hello/src/hello.ts, ts-hello/src/hello.ts first second fails with the same errors as above (whereas ts-hello/src/hello.js first second runs just fine after giving it the necessary permission as well).
By now, we know how to fix this. Just replace the first line of ~/projects/ts-hello/src/hello.ts with:
#!/usr/bin/env ts-node-script
(It seems to me that ts-node-script exists for exactly this purpose. Why was it not recommended in #73, #298, and #639? And if we're already at it: #116 would also benefit from a reference to this issue.)
This solves the errors and at this point you could just add an alias to this script to your shell startup script:
$ echo "alias hello='~/projects/ts-hello/src/hello.ts'" >> ~/.bashrcHowever, I would rather like to use the bin functionality of npm for this so that others could install my script easily if I decided to publish my package on www.npmjs.com.
As the output of the npm link command above told you, it created the following symlinks for your script and package:
/usr/local/bin/hello -> /usr/local/lib/node_modules/ts-hello/src/hello.ts
/usr/local/lib/node_modules/ts-hello -> ~/projects/ts-hello
So what happens if we use this linked command now?
$ hello
/usr/local/lib/node_modules/ts-node/src/index.ts:421
return new TSError(diagnosticText, diagnosticCodes)
^
TSError: ⨯ Unable to compile TypeScript:
~/projects/ts-hello/src/hello.ts:7:16 - error TS2307: Cannot find module 'os'.
7 import os from 'os';
~~~~
~/projects/ts-hello/src/hello.ts:10:32 - error TS2580: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i @types/node`.
10 console.log('Your arguments:', process.argv.slice(2));
~~~~~~~
at createTSError (/usr/local/lib/node_modules/ts-node/src/index.ts:421:12)
at reportTSError (/usr/local/lib/node_modules/ts-node/src/index.ts:425:19)
at getOutput (/usr/local/lib/node_modules/ts-node/src/index.ts:553:36)
at Object.compile (/usr/local/lib/node_modules/ts-node/src/index.ts:758:32)
at Module.m._compile (/usr/local/lib/node_modules/ts-node/src/index.ts:837:43)
at Module._extensions..js (internal/modules/cjs/loader.js:1167:10)
at Object.require.extensions.<computed> [as .ts] (/usr/local/lib/node_modules/ts-node/src/index.ts:840:12)
at Module.load (internal/modules/cjs/loader.js:996:32)
at Function.Module._load (internal/modules/cjs/loader.js:896:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)Back to field one. Since --script-mode just determines the directory name from the script path (see the code) and the script is located in /usr/local/bin whereas the package exists (resp. is linked) at /usr/local/lib/node_modules/ts-hello, this couldn't work.
So how do we solve this? The best I could come up with is:
#!/usr/bin/env -S ts-node --project /usr/local/lib/node_modules/ts-hello/tsconfig.json
A few explanations:
-Sallows you to pass arguments to the specified interpreter.- If this doesn't work on your platform, you can try to hack around this limitation.
- Ideally, the path to the global
node_modulesdirectory would be determined dynamically to be more platform-independent but I couldn't get#!/usr/bin/env -S ts-node --project $(npm get prefix)/lib/node_modules/ts-hello/tsconfig.jsonto work. - While it might be tempting to use
#!/usr/bin/env -S npx ts-node --project /usr/local/lib/node_modules/ts-hello/tsconfig.jsonin order not to require a global installation ofts-node, the user needs to installtypescriptmanually globally anyway astypescriptis only a peer dependency ofts-node. (Is there a way around this?)
Coming back to the original question: Is this currently really the best way to make a command line script with ts-node? (My solution is both complicated and platform-dependent.)
If yes, it would be so much easier (and it would have saved me hours) if --script-mode could follow the symbolic link before determining the directory. (And the README should state prominently that the correct shebang to use is #!/usr/bin/env ts-node-script!)