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

feat: support Node v20.6.0 module.register() & --import flag #337

Merged
merged 7 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,15 @@ tsx --no-cache ./file.ts

`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.

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.
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.

```sh
node --loader tsx ./file.ts
node --import tsx ./file.ts
```

Or as an environment variable:
```sh
NODE_OPTIONS='--loader tsx' node ./file.ts
NODE_OPTIONS='--import tsx' node ./file.ts
```

> **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.
Expand All @@ -170,6 +170,13 @@ NODE_OPTIONS='--loader tsx' node ./file.ts

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

##### Node.js v20.6.0 and above
```sh
node --import tsx/esm ./file.ts
```

##### Node.js v20.5.1 and below

```sh
node --loader tsx/esm ./file.ts
```
Expand Down
18 changes: 1 addition & 17 deletions src/cjs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { TransformOptions } from 'esbuild';
import { installSourceMapSupport } from '../source-map';
import { transformSync, transformDynamicImport } from '../utils/transform';
import { resolveTsPath } from '../utils/resolve-ts-path';
import { compareNodeVersion } from '../utils/compare-node-version';
import { nodeSupportsImport, supportsNodePrefix } from '../utils/node-features';

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

const applySourceMap = installSourceMapSupport();

const nodeSupportsImport = (
// v13.2.0 and higher
compareNodeVersion([13, 2, 0]) >= 0

// 12.20.0 ~ 13.0.0
|| (
compareNodeVersion([12, 20, 0]) >= 0
&& compareNodeVersion([13, 0, 0]) < 0
)
);

const extensions = Module._extensions;
const defaultLoader = extensions['.js'];

Expand Down Expand Up @@ -137,11 +126,6 @@ Object.defineProperty(extensions, '.mjs', {
enumerable: false,
});

const supportsNodePrefix = (
compareNodeVersion([16, 0, 0]) >= 0
|| compareNodeVersion([14, 18, 0]) >= 0
);

// Add support for "node:" protocol
const defaultResolveFilename = Module._resolveFilename.bind(Module);
Module._resolveFilename = (request, parent, isMain, options) => {
Expand Down
12 changes: 12 additions & 0 deletions src/esm/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
import { isMainThread } from 'node:worker_threads';
import { supportsModuleRegister } from '../utils/node-features';
import { registerLoader } from './register';

// Loaded via --import flag
if (
supportsModuleRegister
&& isMainThread
) {
registerLoader();
}

export * from './loaders.js';
export * from './loaders-deprecated.js';
4 changes: 1 addition & 3 deletions src/esm/loaders-deprecated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import type { ModuleFormat } from 'module';
import type { TransformOptions } from 'esbuild';
import { transform, transformDynamicImport } from '../utils/transform';
import { compareNodeVersion } from '../utils/compare-node-version';
import { nodeSupportsDeprecatedLoaders } from '../utils/node-features';
import {
applySourceMap,
fileMatcher,
Expand All @@ -25,7 +25,7 @@
defaultGetFormat: getFormat,
) => MaybePromise<{ format: ModuleFormat }>;

const _getFormat: getFormat = async function (

Check warning on line 28 in src/esm/loaders-deprecated.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Unexpected unnamed async function
url,
context,
defaultGetFormat,
Expand Down Expand Up @@ -62,7 +62,7 @@
defaultTransformSource: transformSource,
) => MaybePromise<{ source: Source }>

const _transformSource: transformSource = async function (

Check warning on line 65 in src/esm/loaders-deprecated.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Unexpected unnamed async function
source,
context,
defaultTransformSource,
Expand Down Expand Up @@ -109,7 +109,5 @@
return result;
};

const nodeSupportsDeprecatedLoaders = compareNodeVersion([16, 12, 0]) < 0;

export const getFormat = nodeSupportsDeprecatedLoaders ? _getFormat : undefined;
export const transformSource = nodeSupportsDeprecatedLoaders ? _transformSource : undefined;
28 changes: 16 additions & 12 deletions src/esm/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import path from 'path';
import { pathToFileURL, fileURLToPath } from 'url';
import type {
ResolveFnOutput, ResolveHookContext, LoadHook, GlobalPreloadHook,
ResolveFnOutput, ResolveHookContext, LoadHook, GlobalPreloadHook, InitializeHook,
} from 'module';
import type { TransformOptions } from 'esbuild';
import { compareNodeVersion } from '../utils/compare-node-version';
import { transform, transformDynamicImport } from '../utils/transform';
import { resolveTsPath } from '../utils/resolve-ts-path';
import {
supportsNodePrefix,
} from '../utils/node-features';
import {
applySourceMap,
tsconfigPathsMatcher,
Expand All @@ -34,7 +36,7 @@
recursiveCall?: boolean,
) => MaybePromise<ResolveFnOutput>;

const isolatedLoader = compareNodeVersion([20, 0, 0]) >= 0;
let mainThreadPort: MessagePort | undefined;

type SendToParent = (data: {
type: 'dependency';
Expand All @@ -43,12 +45,21 @@

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

export const initialize: InitializeHook = async (data) => {
if (!data) {
throw new Error('tsx must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0');
}

const { port } = data;
mainThreadPort = port;
sendToParent = port.postMessage.bind(port);
};

/**
* Technically globalPreload is deprecated so it should be in loaders-deprecated
* but it shares a closure with the new load hook
*/
let mainThreadPort: MessagePort | undefined;
const _globalPreload: GlobalPreloadHook = ({ port }) => {
export const globalPreload: GlobalPreloadHook = ({ port }) => {
mainThreadPort = port;
sendToParent = port.postMessage.bind(port);

Expand All @@ -66,8 +77,6 @@
`;
};

export const globalPreload = isolatedLoader ? _globalPreload : undefined;

const resolveExplicitPath = async (
defaultResolve: NextResolve,
specifier: string,
Expand Down Expand Up @@ -149,12 +158,7 @@

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

const supportsNodePrefix = (
compareNodeVersion([14, 13, 1]) >= 0
|| compareNodeVersion([12, 20, 0]) >= 0
);

export const resolve: resolve = async function (

Check warning on line 161 in src/esm/loaders.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Unexpected unnamed async function

Check warning on line 161 in src/esm/loaders.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Async function has a complexity of 24. Maximum allowed is 10
specifier,
context,
defaultResolve,
Expand Down Expand Up @@ -248,7 +252,7 @@
}
};

export const load: LoadHook = async function (

Check warning on line 255 in src/esm/loaders.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

Unexpected unnamed async function
url,
context,
defaultLoad,
Expand Down
30 changes: 30 additions & 0 deletions src/esm/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import module from 'node:module';
import { MessageChannel } from 'node:worker_threads';
import { installSourceMapSupport } from '../source-map';

export const registerLoader = () => {
const { port1, port2 } = new MessageChannel();

installSourceMapSupport(port1);
if (process.send) {
port1.addListener('message', (message) => {
if (message.type === 'dependency') {
process.send!(message);
}
});
}

// Allows process to exit without waiting for port to close
port1.unref();

module.register(
'./index.mjs',
{
parentURL: import.meta.url,
data: {
port: port2,
},
transferList: [port2],
},
);
};
3 changes: 2 additions & 1 deletion src/run.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { StdioOptions } from 'child_process';
import { pathToFileURL } from 'url';
import spawn from 'cross-spawn';
import { supportsModuleRegister } from './utils/node-features';

export function run(
argv: string[],
Expand Down Expand Up @@ -34,7 +35,7 @@ export function run(
'--require',
require.resolve('./preflight.cjs'),

'--loader',
supportsModuleRegister ? '--import' : '--loader',
pathToFileURL(require.resolve('./loader.mjs')).toString(),

...argv,
Expand Down
9 changes: 1 addition & 8 deletions src/source-map.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import type { MessagePort } from 'node:worker_threads';
import sourceMapSupport, { type UrlAndMap } from 'source-map-support';
import type { Transformed } from './utils/transform/apply-transformers';
import { compareNodeVersion } from './utils/compare-node-version';

/**
* Node.js loaders are isolated from v20
* https://github.com/nodejs/node/issues/49455#issuecomment-1703812193
* https://github.com/nodejs/node/blob/33710e7e7d39d19449a75911537d630349110a0c/doc/api/module.md#L375-L376
*/
const isolatedLoader = compareNodeVersion([20, 0, 0]) >= 0;
import { isolatedLoader } from './utils/node-features';

export type RawSourceMap = UrlAndMap['map'];

Expand Down
9 changes: 0 additions & 9 deletions src/utils/compare-node-version.ts

This file was deleted.

36 changes: 36 additions & 0 deletions src/utils/node-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
type Version = [number, number, number];

const nodeVersion = process.versions.node.split('.').map(Number) as Version;

const compareNodeVersion = (version: Version) => (
nodeVersion[0] - version[0]
|| nodeVersion[1] - version[1]
|| nodeVersion[2] - version[2]
);

export const nodeSupportsImport = (
// v13.2.0 and higher
compareNodeVersion([13, 2, 0]) >= 0

// 12.20.0 ~ 13.0.0
|| (
compareNodeVersion([12, 20, 0]) >= 0
&& compareNodeVersion([13, 0, 0]) < 0
)
);

export const supportsNodePrefix = (
compareNodeVersion([16, 0, 0]) >= 0
|| compareNodeVersion([14, 18, 0]) >= 0
);

export const nodeSupportsDeprecatedLoaders = compareNodeVersion([16, 12, 0]) < 0;

/**
* Node.js loaders are isolated from v20
* https://github.com/nodejs/node/issues/49455#issuecomment-1703812193
* https://github.com/nodejs/node/blob/33710e7e7d39d19449a75911537d630349110a0c/doc/api/module.md#L375-L376
*/
export const isolatedLoader = compareNodeVersion([20, 0, 0]) >= 0;

export const supportsModuleRegister = compareNodeVersion([20, 6, 0]) >= 0;
Loading