Skip to content

Commit 1d7e528

Browse files
fix: support Node.js 26.1.0 and 25.9.0
## Problem - The compatibility matrix covered latest LTS `24.15.0` and older supported runtime boundaries, but missed newer non-EOL Node lines `26.1.0` and `25.9.0`. - Node 26 runtime-deprecates `module.register()` as DEP0205; suppressing that warning would hide the real migration target. - `module.registerHooks()` is only safe for tsx's CJS interop path once Node can re-enter `Module._load` from sync hooks (`24.11.1`, `25.1.0`, `26.0.0+`). Earlier `registerHooks()` versions keep the async `module.register()` path. ## Changes - Add an exact feature gate for the CJS-reload-safe `module.registerHooks()` boundary, with pinned Node source/PR citations. - Use sync `registerHooks()` resolve/load hooks on Node versions with CJS reload support, while preserving async `module.register()` for older supported Node versions. - Keep CJS `require()` on tsx's CJS loader path when the global CJS hook is active, and buffer early IPC dependency messages so watch mode sees sync-hook loads. - Update smoke/API/version-sensitive coverage for DEP0205, CJS namespace shape, `require.cache`, watch reruns, and Node `26.1.0`/`25.9.0` compatibility.
1 parent c1d2d45 commit 1d7e528

17 files changed

Lines changed: 1165 additions & 279 deletions

File tree

src/cjs/api/module-resolve-filename/resolve-implicit-extensions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const createImplicitResolver = (
2626
*/
2727
if (isDirectoryPattern.test(request)) {
2828
// If directory, can be index.js, index.ts, etc.
29-
let joinedPath = path.join(request, 'index.js');
29+
let joinedPath = path.join(request, 'index');
3030

3131
/**
3232
* path.join will remove the './' prefix if it exists
@@ -49,7 +49,7 @@ export const createImplicitResolver = (
4949

5050
if (nodeError.code === 'MODULE_NOT_FOUND') {
5151
try {
52-
return nextResolve(`${request}${path.sep}index.js`);
52+
return nextResolve(`${request}${path.sep}index`);
5353
} catch {}
5454
}
5555

src/cjs/api/register.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { loadTsconfig } from '../../utils/tsconfig.js';
55
import type { RequiredProperty } from '../../types.js';
66
import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js';
77
import { fileUrlPrefix } from '../../utils/path-utils.js';
8+
import { activateGlobalCjsLoader } from '../../utils/cjs-loader-state.js';
89
import type { LoaderState } from './types.js';
910
import { createExtensions } from './module-extensions.js';
1011
import { createResolveFilename } from './module-resolve-filename/index.js';
@@ -88,6 +89,11 @@ export const register: Register = (
8889
tsconfig,
8990
options?.namespace,
9091
);
92+
const unregisterGlobalCjsLoader = (
93+
options?.namespace
94+
? undefined
95+
: activateGlobalCjsLoader()
96+
);
9197

9298
const unregister = () => {
9399
if (sourceMapsEnabled === false) {
@@ -103,6 +109,7 @@ export const register: Register = (
103109
Module._resolveFilename = originalResolveFilename;
104110
}
105111
unregisterExtensions();
112+
unregisterGlobalCjsLoader?.();
106113
};
107114

108115
if (options?.namespace) {

src/esm/api/register.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import module from 'node:module';
22
import { MessageChannel, type MessagePort } from 'node:worker_threads';
33
import type { Message } from '../types.js';
44
import type { RequiredProperty } from '../../types.js';
5+
import { isFeatureSupported, moduleRegisterHooksCjsReload } from '../../utils/node-features.js';
56
import { interopCjsExports } from '../../cjs/api/module-resolve-filename/interop-cjs-exports.js';
7+
import { createData } from '../hook/initialize.js';
8+
import { createLoadSync } from '../hook/load.js';
9+
import { createResolveSync } from '../hook/resolve.js';
610
import { createScopedImport, type ScopedImport } from './scoped-import.js';
711

812
export type TsconfigOptions = false | string;
@@ -36,7 +40,12 @@ let cjsInteropApplied = false;
3640
export const register: Register = (
3741
options,
3842
) => {
39-
if (!module.register) {
43+
const supportsRegisterHooks = (
44+
typeof module.registerHooks === 'function'
45+
&& isFeatureSupported(moduleRegisterHooksCjsReload)
46+
);
47+
48+
if (!module.register && !supportsRegisterHooks) {
4049
throw new Error(`This version of Node.js (${process.version}) does not support module.register(). Please upgrade to Node v18.19 or v20.6 and above.`);
4150
}
4251

@@ -55,6 +64,34 @@ export const register: Register = (
5564
const { sourceMapsEnabled } = process;
5665
process.setSourceMapsEnabled(true);
5766

67+
if (supportsRegisterHooks) {
68+
const hookData = createData({
69+
namespace: options?.namespace,
70+
onImport: options?.onImport,
71+
tsconfig: options?.tsconfig,
72+
});
73+
const registeredHooks = module.registerHooks({
74+
load: createLoadSync(hookData),
75+
resolve: createResolveSync(hookData),
76+
});
77+
78+
const unregister = (async () => {
79+
hookData.active = false;
80+
registeredHooks.deregister();
81+
82+
if (sourceMapsEnabled === false) {
83+
process.setSourceMapsEnabled(false);
84+
}
85+
}) as NamespacedUnregister;
86+
87+
if (options?.namespace) {
88+
unregister.import = createScopedImport(options.namespace);
89+
unregister.unregister = unregister;
90+
}
91+
92+
return unregister;
93+
}
94+
5895
const { port1, port2 } = new MessageChannel();
5996
module.register(
6097
// Load new copy of loader so it can be registered multiple times

src/esm/hook/initialize.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { loadTsconfig } from '../../utils/tsconfig.js';
66

77
type Data = InitializationOptions & {
88
active: boolean;
9+
onImport?: (url: string) => void;
910
parsedTsconfig: TsconfigResult | undefined;
1011
};
1112

@@ -14,22 +15,39 @@ export const data: Data = {
1415
parsedTsconfig: undefined,
1516
};
1617

18+
export type { Data };
19+
20+
export const createData = (
21+
options?: InitializationOptions & {
22+
onImport?: (url: string) => void;
23+
},
24+
): Data => {
25+
const hookData: Data = {
26+
active: true,
27+
namespace: options?.namespace,
28+
onImport: options?.onImport,
29+
parsedTsconfig: undefined,
30+
port: options?.port,
31+
tsconfig: options?.tsconfig,
32+
};
33+
34+
if (options?.tsconfig !== false) {
35+
hookData.parsedTsconfig = loadTsconfig(options?.tsconfig ?? process.env.TSX_TSCONFIG_PATH);
36+
}
37+
38+
return hookData;
39+
};
40+
1741
export const initialize: InitializeHook = async (
1842
options?: InitializationOptions,
1943
) => {
2044
if (!options) {
2145
throw new Error('tsx must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0 and v18.19.0');
2246
}
2347

24-
data.namespace = options.namespace;
25-
26-
if (options.tsconfig !== false) {
27-
data.parsedTsconfig = loadTsconfig(options.tsconfig ?? process.env.TSX_TSCONFIG_PATH);
28-
}
48+
Object.assign(data, createData(options));
2949

3050
if (options.port) {
31-
data.port = options.port;
32-
3351
// Unregister
3452
options.port.on('message', (message: string) => {
3553
if (message === 'deactivate') {

0 commit comments

Comments
 (0)