Skip to content

Commit 23e4694

Browse files
authoredOct 17, 2023
feat: support Node v20.6.0 module.register() & --import flag (#337)
1 parent e46366d commit 23e4694

10 files changed

+109
-53
lines changed
 

‎README.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,15 @@ tsx --no-cache ./file.ts
153153

154154
`tsx` is a standalone binary designed to be used in place of `node`, but sometimes you'll want to use `node` directly. For example, when adding TypeScript & ESM support to npm-installed binaries.
155155

156-
To use `tsx` as a Node.js loader, pass it in to the [`--loader`](https://nodejs.org/api/esm.html#loaders) flag. This will add TypeScript & ESM support for both ESM and CommonJS contexts.
156+
To use `tsx` as a Node.js loader, pass it in to the [`--import`](https://nodejs.org/api/module.html#enabling) flag. This will add TypeScript & ESM support for both Module and CommonJS contexts.
157157

158158
```sh
159-
node --loader tsx ./file.ts
159+
node --import tsx ./file.ts
160160
```
161161

162162
Or as an environment variable:
163163
```sh
164-
NODE_OPTIONS='--loader tsx' node ./file.ts
164+
NODE_OPTIONS='--import tsx' node ./file.ts
165165
```
166166

167167
> **Note:** The loader is limited to adding support for loading TypeScript/ESM files. CLI features such as _watch mode_ or suppressing "experimental feature" warnings will not be available.
@@ -170,6 +170,13 @@ NODE_OPTIONS='--loader tsx' node ./file.ts
170170

171171
If you only need to add TypeScript support in a Module context, you can use the ESM loader:
172172

173+
##### Node.js v20.6.0 and above
174+
```sh
175+
node --import tsx/esm ./file.ts
176+
```
177+
178+
##### Node.js v20.5.1 and below
179+
173180
```sh
174181
node --loader tsx/esm ./file.ts
175182
```

‎src/cjs/index.ts

+1-17
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { TransformOptions } from 'esbuild';
1111
import { installSourceMapSupport } from '../source-map';
1212
import { transformSync, transformDynamicImport } from '../utils/transform';
1313
import { resolveTsPath } from '../utils/resolve-ts-path';
14-
import { compareNodeVersion } from '../utils/compare-node-version';
14+
import { nodeSupportsImport, supportsNodePrefix } from '../utils/node-features';
1515

1616
const isRelativePathPattern = /^\.{1,2}\//;
1717
const isTsFilePatten = /\.[cm]?tsx?$/;
@@ -31,17 +31,6 @@ const tsconfigPathsMatcher = tsconfig && createPathsMatcher(tsconfig);
3131

3232
const applySourceMap = installSourceMapSupport();
3333

34-
const nodeSupportsImport = (
35-
// v13.2.0 and higher
36-
compareNodeVersion([13, 2, 0]) >= 0
37-
38-
// 12.20.0 ~ 13.0.0
39-
|| (
40-
compareNodeVersion([12, 20, 0]) >= 0
41-
&& compareNodeVersion([13, 0, 0]) < 0
42-
)
43-
);
44-
4534
const extensions = Module._extensions;
4635
const defaultLoader = extensions['.js'];
4736

@@ -137,11 +126,6 @@ Object.defineProperty(extensions, '.mjs', {
137126
enumerable: false,
138127
});
139128

140-
const supportsNodePrefix = (
141-
compareNodeVersion([16, 0, 0]) >= 0
142-
|| compareNodeVersion([14, 18, 0]) >= 0
143-
);
144-
145129
// Add support for "node:" protocol
146130
const defaultResolveFilename = Module._resolveFilename.bind(Module);
147131
Module._resolveFilename = (request, parent, isMain, options) => {

‎src/esm/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
1+
import { isMainThread } from 'node:worker_threads';
2+
import { supportsModuleRegister } from '../utils/node-features';
3+
import { registerLoader } from './register';
4+
5+
// Loaded via --import flag
6+
if (
7+
supportsModuleRegister
8+
&& isMainThread
9+
) {
10+
registerLoader();
11+
}
12+
113
export * from './loaders.js';
214
export * from './loaders-deprecated.js';

‎src/esm/loaders-deprecated.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url';
77
import type { ModuleFormat } from 'module';
88
import type { TransformOptions } from 'esbuild';
99
import { transform, transformDynamicImport } from '../utils/transform';
10-
import { compareNodeVersion } from '../utils/compare-node-version';
10+
import { nodeSupportsDeprecatedLoaders } from '../utils/node-features';
1111
import {
1212
applySourceMap,
1313
fileMatcher,
@@ -109,7 +109,5 @@ const _transformSource: transformSource = async function (
109109
return result;
110110
};
111111

112-
const nodeSupportsDeprecatedLoaders = compareNodeVersion([16, 12, 0]) < 0;
113-
114112
export const getFormat = nodeSupportsDeprecatedLoaders ? _getFormat : undefined;
115113
export const transformSource = nodeSupportsDeprecatedLoaders ? _transformSource : undefined;

‎src/esm/loaders.ts

+16-12
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import type { MessagePort } from 'node:worker_threads';
22
import path from 'path';
33
import { pathToFileURL, fileURLToPath } from 'url';
44
import type {
5-
ResolveFnOutput, ResolveHookContext, LoadHook, GlobalPreloadHook,
5+
ResolveFnOutput, ResolveHookContext, LoadHook, GlobalPreloadHook, InitializeHook,
66
} from 'module';
77
import type { TransformOptions } from 'esbuild';
8-
import { compareNodeVersion } from '../utils/compare-node-version';
98
import { transform, transformDynamicImport } from '../utils/transform';
109
import { resolveTsPath } from '../utils/resolve-ts-path';
10+
import {
11+
supportsNodePrefix,
12+
} from '../utils/node-features';
1113
import {
1214
applySourceMap,
1315
tsconfigPathsMatcher,
@@ -34,7 +36,7 @@ type resolve = (
3436
recursiveCall?: boolean,
3537
) => MaybePromise<ResolveFnOutput>;
3638

37-
const isolatedLoader = compareNodeVersion([20, 0, 0]) >= 0;
39+
let mainThreadPort: MessagePort | undefined;
3840

3941
type SendToParent = (data: {
4042
type: 'dependency';
@@ -43,12 +45,21 @@ type SendToParent = (data: {
4345

4446
let sendToParent: SendToParent | undefined = process.send ? process.send.bind(process) : undefined;
4547

48+
export const initialize: InitializeHook = async (data) => {
49+
if (!data) {
50+
throw new Error('tsx must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0');
51+
}
52+
53+
const { port } = data;
54+
mainThreadPort = port;
55+
sendToParent = port.postMessage.bind(port);
56+
};
57+
4658
/**
4759
* Technically globalPreload is deprecated so it should be in loaders-deprecated
4860
* but it shares a closure with the new load hook
4961
*/
50-
let mainThreadPort: MessagePort | undefined;
51-
const _globalPreload: GlobalPreloadHook = ({ port }) => {
62+
export const globalPreload: GlobalPreloadHook = ({ port }) => {
5263
mainThreadPort = port;
5364
sendToParent = port.postMessage.bind(port);
5465

@@ -66,8 +77,6 @@ const _globalPreload: GlobalPreloadHook = ({ port }) => {
6677
`;
6778
};
6879

69-
export const globalPreload = isolatedLoader ? _globalPreload : undefined;
70-
7180
const resolveExplicitPath = async (
7281
defaultResolve: NextResolve,
7382
specifier: string,
@@ -149,11 +158,6 @@ async function tryDirectory(
149158

150159
const isRelativePathPattern = /^\.{1,2}\//;
151160

152-
const supportsNodePrefix = (
153-
compareNodeVersion([14, 13, 1]) >= 0
154-
|| compareNodeVersion([12, 20, 0]) >= 0
155-
);
156-
157161
export const resolve: resolve = async function (
158162
specifier,
159163
context,

‎src/esm/register.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import module from 'node:module';
2+
import { MessageChannel } from 'node:worker_threads';
3+
import { installSourceMapSupport } from '../source-map';
4+
5+
export const registerLoader = () => {
6+
const { port1, port2 } = new MessageChannel();
7+
8+
installSourceMapSupport(port1);
9+
if (process.send) {
10+
port1.addListener('message', (message) => {
11+
if (message.type === 'dependency') {
12+
process.send!(message);
13+
}
14+
});
15+
}
16+
17+
// Allows process to exit without waiting for port to close
18+
port1.unref();
19+
20+
module.register(
21+
'./index.mjs',
22+
{
23+
parentURL: import.meta.url,
24+
data: {
25+
port: port2,
26+
},
27+
transferList: [port2],
28+
},
29+
);
30+
};

‎src/run.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { StdioOptions } from 'child_process';
22
import { pathToFileURL } from 'url';
33
import spawn from 'cross-spawn';
4+
import { supportsModuleRegister } from './utils/node-features';
45

56
export function run(
67
argv: string[],
@@ -34,7 +35,7 @@ export function run(
3435
'--require',
3536
require.resolve('./preflight.cjs'),
3637

37-
'--loader',
38+
supportsModuleRegister ? '--import' : '--loader',
3839
pathToFileURL(require.resolve('./loader.mjs')).toString(),
3940

4041
...argv,

‎src/source-map.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import type { MessagePort } from 'node:worker_threads';
22
import sourceMapSupport, { type UrlAndMap } from 'source-map-support';
33
import type { Transformed } from './utils/transform/apply-transformers';
4-
import { compareNodeVersion } from './utils/compare-node-version';
5-
6-
/**
7-
* Node.js loaders are isolated from v20
8-
* https://github.com/nodejs/node/issues/49455#issuecomment-1703812193
9-
* https://github.com/nodejs/node/blob/33710e7e7d39d19449a75911537d630349110a0c/doc/api/module.md#L375-L376
10-
*/
11-
const isolatedLoader = compareNodeVersion([20, 0, 0]) >= 0;
4+
import { isolatedLoader } from './utils/node-features';
125

136
export type RawSourceMap = UrlAndMap['map'];
147

‎src/utils/compare-node-version.ts

-9
This file was deleted.

‎src/utils/node-features.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
type Version = [number, number, number];
2+
3+
const nodeVersion = process.versions.node.split('.').map(Number) as Version;
4+
5+
const compareNodeVersion = (version: Version) => (
6+
nodeVersion[0] - version[0]
7+
|| nodeVersion[1] - version[1]
8+
|| nodeVersion[2] - version[2]
9+
);
10+
11+
export const nodeSupportsImport = (
12+
// v13.2.0 and higher
13+
compareNodeVersion([13, 2, 0]) >= 0
14+
15+
// 12.20.0 ~ 13.0.0
16+
|| (
17+
compareNodeVersion([12, 20, 0]) >= 0
18+
&& compareNodeVersion([13, 0, 0]) < 0
19+
)
20+
);
21+
22+
export const supportsNodePrefix = (
23+
compareNodeVersion([16, 0, 0]) >= 0
24+
|| compareNodeVersion([14, 18, 0]) >= 0
25+
);
26+
27+
export const nodeSupportsDeprecatedLoaders = compareNodeVersion([16, 12, 0]) < 0;
28+
29+
/**
30+
* Node.js loaders are isolated from v20
31+
* https://github.com/nodejs/node/issues/49455#issuecomment-1703812193
32+
* https://github.com/nodejs/node/blob/33710e7e7d39d19449a75911537d630349110a0c/doc/api/module.md#L375-L376
33+
*/
34+
export const isolatedLoader = compareNodeVersion([20, 0, 0]) >= 0;
35+
36+
export const supportsModuleRegister = compareNodeVersion([20, 6, 0]) >= 0;

0 commit comments

Comments
 (0)
Please sign in to comment.