Skip to content

Commit 1dbdea6

Browse files
committed
start .ts app with node native type stripping
1 parent 31adee8 commit 1dbdea6

13 files changed

Lines changed: 485 additions & 26 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ You can start any application (Node.js, Bun, and also Python, Ruby, binaries in
8585

8686
```bash
8787
$ pm2 start app.js
88+
$ pm2 start app.ts
89+
$ pm2 start app.py
8890
```
8991

9092
Your app is now daemonized, monitored and kept alive forever.

lib/Common.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,25 @@ Common.sink.resolveInterpreter = function(app) {
423423
app.exec_interpreter = process.execPath;
424424
}
425425

426+
// TypeScript support: .ts maps to bun, but when bun is not installed
427+
// fall back to node type stripping when the node version supports it
428+
if (noInterpreter && extName == '.ts' && betterInterpreter == 'bun' &&
429+
cst.IS_BUN === false && which('bun') == null) {
430+
var typestrip = require('./tools/typestrip.js');
431+
var ts_support = typestrip.supportLevel(typestrip.resolvedNodeVersion());
432+
433+
if (ts_support === false)
434+
throw new Error('TypeScript apps require bun, Node.js >= 22.18 (native type stripping) or a custom interpreter (e.g. --interpreter ts-node)');
435+
436+
betterInterpreter = 'node';
437+
if (ts_support === 'flag') {
438+
if (!app.node_args)
439+
app.node_args = [];
440+
if (app.node_args.indexOf('--experimental-strip-types') === -1)
441+
app.node_args.push('--experimental-strip-types');
442+
}
443+
}
444+
426445
// No interpreter defined and correspondance in schema hashmap
427446
if (noInterpreter && betterInterpreter) {
428447
app.exec_interpreter = betterInterpreter;

lib/God/ClusterMode.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
* @project PM2
1212
*/
1313
var cluster = require('cluster');
14+
var path = require('path');
1415
var Utility = require('../Utility.js');
1516
var pkg = require('../../package.json');
17+
var typestrip = require('../tools/typestrip.js');
1618

1719
/**
1820
* Description
@@ -38,6 +40,15 @@ module.exports = function ClusterMode(God) {
3840
cluster.settings.execArgv = env_copy.node_args;
3941
}
4042

43+
// Cluster workers run on the daemon's node, whose version can differ
44+
// from the CLI that resolved the interpreter: ensure type stripping
45+
// is active for .ts apps when this node needs the flag
46+
if (path.extname(env_copy.pm_exec_path || '') === '.ts' &&
47+
typestrip.supportLevel(process.versions.node) === 'flag' &&
48+
cluster.settings.execArgv.indexOf('--experimental-strip-types') === -1) {
49+
cluster.settings.execArgv = cluster.settings.execArgv.concat('--experimental-strip-types');
50+
}
51+
4152
env_copy._pm2_version = pkg.version;
4253

4354
try {

lib/ProcessContainer.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,7 @@ delete process.env.pm2_env;
110110
* @return
111111
*/
112112
function exec(script, stds) {
113-
if (p.extname(script) == '.ts' || p.extname(script) == '.tsx') {
114-
try {
115-
require('ts-node/register');
116-
} catch (e) {
117-
console.error('Failed to load Typescript interpreter:', e.message || e);
118-
}
119-
}
113+
ProcessUtils.enableTypeScript(script, pm2_env.pm_cwd);
120114

121115
process.on('message', function (msg) {
122116
if (msg.type === 'log:reload') {

lib/ProcessContainerFork.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ if (process.connected &&
2525

2626
// Require the real application
2727
if (process.env.pm_exec_path) {
28+
ProcessUtils.enableTypeScript(process.env.pm_exec_path, process.env.pm_cwd);
2829
if (ProcessUtils.isESModule(process.env.pm_exec_path) === true) {
2930
import(url.pathToFileURL(process.env.pm_exec_path));
3031
}

lib/ProcessUtils.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@ module.exports = {
1919
}, conf))
2020
}
2121
},
22+
enableTypeScript(exec_path, cwd) {
23+
var path = require('path')
24+
var ext = path.extname(exec_path || '')
25+
26+
if (ext !== '.ts' && ext !== '.tsx')
27+
return
28+
29+
// .ts needs nothing when the runtime already strips types
30+
// (enabled by default on node >= 22.18 / >= 23.6, or via flag);
31+
// .tsx always needs a transformer
32+
var stripping = (process.features && process.features.typescript) ||
33+
process.execArgv.indexOf('--experimental-strip-types') !== -1 ||
34+
process.execArgv.indexOf('--experimental-transform-types') !== -1
35+
if (ext === '.ts' && stripping)
36+
return
37+
38+
// Resolve ts-node from the application dependencies, not from PM2's
39+
try {
40+
require(require.resolve('ts-node/register', { paths: [cwd || path.dirname(exec_path)] }))
41+
} catch (e) {
42+
console.error('TypeScript support unavailable: install ts-node in your project or use Node.js >= 22.18 (%s)', e.message || e)
43+
}
44+
},
2245
isESModule(exec_path) {
2346
var fs = require('fs')
2447
var path = require('path')

lib/tools/typestrip.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
var fs = require('fs');
3+
var semver = require('semver');
4+
var execSync = require('child_process').execSync;
5+
var which = require('./which.js');
6+
7+
// Node.js type stripping availability:
8+
// enabled by default since 22.18.0 (22.x line) and 23.6.0,
9+
// behind --experimental-strip-types since 22.6.0.
10+
var NATIVE_RANGE = '>=22.18.0 <23.0.0 || >=23.6.0';
11+
var FLAG_RANGE = '>=22.6.0';
12+
13+
var resolved_node_version = null;
14+
15+
/**
16+
* Type stripping support of a given node version.
17+
* Returns 'native' (runs .ts without flags), 'flag'
18+
* (needs --experimental-strip-types) or false.
19+
*/
20+
function supportLevel(version) {
21+
if (semver.satisfies(version, NATIVE_RANGE))
22+
return 'native';
23+
if (semver.satisfies(version, FLAG_RANGE))
24+
return 'flag';
25+
return false;
26+
}
27+
28+
/**
29+
* Version of the `node` binary fork mode will spawn (the one in PATH),
30+
* which can differ from the node running PM2.
31+
*/
32+
function resolvedNodeVersion() {
33+
if (resolved_node_version)
34+
return resolved_node_version;
35+
36+
var node_path = which('node');
37+
if (node_path == null)
38+
return resolved_node_version = process.versions.node;
39+
40+
try {
41+
if (fs.realpathSync(node_path) === fs.realpathSync(process.execPath))
42+
return resolved_node_version = process.versions.node;
43+
resolved_node_version = execSync(JSON.stringify(node_path) + ' --version', {
44+
encoding: 'utf8'
45+
}).trim().replace(/^v/, '');
46+
} catch(e) {
47+
resolved_node_version = process.versions.node;
48+
}
49+
return resolved_node_version;
50+
}
51+
52+
module.exports = {
53+
supportLevel: supportLevel,
54+
resolvedNodeVersion: resolvedNodeVersion
55+
};

test/e2e.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ runTest ./test/e2e/cli/reload.sh
1616
runTest ./test/e2e/cli/start-app.sh
1717
runTest ./test/e2e/cli/operate-regex.sh
1818
#runTest ./test/e2e/cli/bun.sh
19+
runTest ./test/e2e/cli/typescript.sh
1920
runTest ./test/e2e/cli/app-configuration.sh
2021
runTest ./test/e2e/cli/binary.sh
2122
runTest ./test/e2e/cli/startOrX.sh

test/e2e/cli/typescript.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
3+
SRC=$(cd $(dirname "$0"); pwd)
4+
source "${SRC}/../include.sh"
5+
6+
cd $file_path/interpreter
7+
8+
# Without bun, .ts falls back to node type stripping which requires
9+
# node >= 22.6; skip on older nodes where no .ts interpreter is available
10+
if [ "$IS_BUN" = false ] && ! command -v bun > /dev/null 2>&1; then
11+
TS_SUPPORT=$(node -p "require('${SRC}/../../../lib/tools/typestrip.js').supportLevel(process.versions.node) !== false")
12+
if [ "$TS_SUPPORT" != "true" ]; then
13+
echo "Skipping TypeScript test: node $(node -v) has no type stripping and bun is not installed"
14+
exit 0
15+
fi
16+
fi
17+
18+
########### typescript fork test
19+
20+
$pm2 delete all
21+
22+
>typescript-native.log
23+
24+
$pm2 start echo-strippable.ts -o typescript-native.log --merge-logs
25+
26+
sleep 1.5
27+
28+
grep "Hello Typescript!" typescript-native.log
29+
spec "Should run TypeScript file in fork mode"
30+
31+
should 'should have process online' 'online' 1
32+
33+
if [ "$IS_BUN" = false ] && ! command -v bun > /dev/null 2>&1; then
34+
should 'should run TypeScript with node interpreter' "exec_interpreter: 'node'" 1
35+
fi
36+
37+
########### typescript cluster test
38+
39+
$pm2 delete all
40+
41+
>typescript-native.log
42+
43+
$pm2 start echo-strippable.ts -i 2 -o typescript-native.log --merge-logs
44+
45+
sleep 2
46+
47+
should 'should have 2 processes online' 'online' 2
48+
49+
grep "Hello Typescript!" typescript-native.log
50+
spec "Should run TypeScript file in cluster mode"
51+
52+
$pm2 delete all
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Erasable-only TypeScript syntax: runnable by node type stripping,
2+
// unlike echo.ts whose constructor parameter property requires a transform
3+
interface Greeting {
4+
message: string;
5+
}
6+
7+
const greeting: Greeting = { message: "Hello Typescript!" };
8+
9+
console.log(greeting.message);
10+
11+
setInterval(function (): void {}, 2000);

0 commit comments

Comments
 (0)