Skip to content

Commit 3e1e546

Browse files
authoredJun 29, 2024
feat(cjs): improve compatibility with other loaders
1 parent f748e19 commit 3e1e546

File tree

7 files changed

+123
-32
lines changed

7 files changed

+123
-32
lines changed
 

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"node-pty": "^1.0.0",
109109
"outdent": "^0.8.0",
110110
"pkgroll": "^2.1.1",
111+
"proxyquire": "^2.1.3",
111112
"simple-git-hooks": "^2.11.1",
112113
"split2": "^4.2.0",
113114
"strip-ansi": "^7.1.0",

‎pnpm-lock.yaml

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/cjs/api/module-extensions.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { shouldApplySourceMap, inlineSourceMap } from '../../source-map.js';
88
import { parent } from '../../utils/ipc/client.js';
99
import { fileMatcher } from '../../utils/tsconfig.js';
1010
import { implicitlyResolvableExtensions } from './resolve-implicit-extensions.js';
11+
import type { LoaderState } from './types.js';
1112

1213
const typescriptExtensions = [
1314
'.cts',
@@ -23,22 +24,6 @@ const transformExtensions = [
2324
'.mjs',
2425
] as const;
2526

26-
const cloneExtensions = <ObjectType extends object>(
27-
extensions: ObjectType,
28-
) => {
29-
const cloneTo: ObjectType = Object.create(Object.getPrototypeOf(extensions));
30-
31-
// Preserves setters if they exist (e.g. nyc via append-transform)
32-
const descriptors = Object.getOwnPropertyDescriptors(extensions);
33-
for (const property in descriptors) {
34-
if (Object.hasOwn(descriptors, property)) {
35-
Object.defineProperty(cloneTo, property, descriptors[property]);
36-
}
37-
}
38-
39-
return cloneTo;
40-
};
41-
4227
const safeSet = <T extends Record<string, unknown>>(
4328
object: T,
4429
property: keyof T,
@@ -82,18 +67,20 @@ const safeSet = <T extends Record<string, unknown>>(
8267
};
8368

8469
export const createExtensions = (
85-
extendExtensions: NodeJS.RequireExtensions,
70+
state: LoaderState,
71+
extensions: NodeJS.RequireExtensions,
8672
namespace?: string,
8773
) => {
88-
// Clone Module._extensions with null prototype
89-
const extensions = cloneExtensions(extendExtensions);
90-
9174
const defaultLoader = extensions['.js'];
9275

9376
const transformer = (
9477
module: Module,
9578
filePath: string,
9679
) => {
80+
if (state.enabled === false) {
81+
return defaultLoader(module, filePath);
82+
}
83+
9784
// Make sure __filename doesnt contain query
9885
const [cleanFilePath, query] = filePath.split('?');
9986

@@ -198,5 +185,22 @@ export const createExtensions = (
198185
configurable: true,
199186
});
200187

201-
return extensions;
188+
// Unregister
189+
return () => {
190+
/**
191+
* The extensions are only reverted if they're still tsx's transformers
192+
*
193+
* Otherwise, it means they have been wrapped by another loader and should
194+
* be left untouched not to remove the other loader
195+
*/
196+
if (extensions['.js'] === transformer) {
197+
extensions['.js'] = defaultLoader;
198+
}
199+
200+
for (const extension of [...implicitlyResolvableExtensions, '.mjs']) {
201+
if (extensions[extension] === transformer) {
202+
delete extensions[extension];
203+
}
204+
}
205+
};
202206
};

‎src/cjs/api/module-resolve-filename.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { NodeError } from '../../types.js';
66
import { isRelativePath, fileUrlPrefix, tsExtensionsPattern } from '../../utils/path-utils.js';
77
import { tsconfigPathsMatcher, allowJs } from '../../utils/tsconfig.js';
88
import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js';
9-
import type { ResolveFilename, SimpleResolve } from './types.js';
9+
import type { ResolveFilename, SimpleResolve, LoaderState } from './types.js';
1010
import { createImplicitResolver } from './resolve-implicit-extensions.js';
1111

1212
const nodeModulesPath = `${path.sep}node_modules${path.sep}`;
@@ -154,6 +154,7 @@ const resolveRequest = (
154154
};
155155

156156
export const createResolveFilename = (
157+
state: LoaderState,
157158
nextResolve: ResolveFilename,
158159
namespace?: string,
159160
): ResolveFilename => (
@@ -162,6 +163,10 @@ export const createResolveFilename = (
162163
isMain,
163164
options,
164165
) => {
166+
if (state.enabled === false) {
167+
return nextResolve(request, parent, isMain, options);
168+
}
169+
165170
const resolve: SimpleResolve = request_ => nextResolve(
166171
request_,
167172
parent,

‎src/cjs/api/register.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
44
import { loadTsconfig } from '../../utils/tsconfig.js';
55
import type { RequiredProperty } from '../../types.js';
66
import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js';
7+
import type { LoaderState } from './types.js';
78
import { createExtensions } from './module-extensions.js';
89
import { createResolveFilename } from './module-resolve-filename.js';
910

@@ -62,27 +63,35 @@ export const register: Register = (
6263
options,
6364
) => {
6465
const { sourceMapsEnabled } = process;
65-
const { _extensions, _resolveFilename } = Module;
66+
const state: LoaderState = {
67+
enabled: true,
68+
};
6669

6770
loadTsconfig(process.env.TSX_TSCONFIG_PATH);
6871

6972
// register
7073
process.setSourceMapsEnabled(true);
71-
const resolveFilename = createResolveFilename(_resolveFilename, options?.namespace);
74+
75+
const originalResolveFilename = Module._resolveFilename;
76+
const resolveFilename = createResolveFilename(state, originalResolveFilename, options?.namespace);
7277
Module._resolveFilename = resolveFilename;
7378

74-
const extensions = createExtensions(Module._extensions, options?.namespace);
75-
// @ts-expect-error overwriting read-only property
76-
Module._extensions = extensions;
79+
const unregisterExtensions = createExtensions(state, Module._extensions, options?.namespace);
7780

7881
const unregister = () => {
7982
if (sourceMapsEnabled === false) {
8083
process.setSourceMapsEnabled(false);
8184
}
82-
83-
// @ts-expect-error overwriting read-only property
84-
Module._extensions = _extensions;
85-
Module._resolveFilename = _resolveFilename;
85+
state.enabled = false;
86+
87+
/**
88+
* Only revert the _resolveFilename & extensions if they're unwrapped
89+
* by another loader extension
90+
*/
91+
if (Module._resolveFilename === resolveFilename) {
92+
Module._resolveFilename = originalResolveFilename;
93+
}
94+
unregisterExtensions();
8695
};
8796

8897
if (options?.namespace) {

‎src/cjs/api/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type Module from 'module';
22

3+
export type LoaderState = {
4+
enabled: boolean;
5+
};
6+
37
export type ResolveFilename = typeof Module._resolveFilename;
48

59
export type SimpleResolve = (request: string) => string;

‎tests/specs/api.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default testSuite(({ describe }, node: NodeApis) => {
118118
return code;
119119
}, '.ts');
120120
`,
121-
node_modules: ({ symlink }) => symlink(path.resolve('node_modules'), 'junction'),
121+
'node_modules/append-transform': ({ symlink }) => symlink(path.resolve('node_modules/append-transform'), 'junction'),
122122
});
123123

124124
const { stdout } = await execaNode('./index.js', {
@@ -311,6 +311,38 @@ export default testSuite(({ describe }, node: NodeApis) => {
311311

312312
expect(stdout).toBe('foo bar json file.ts\nfoo bar json file.ts\nfoo bar json file.ts\nUnregistered');
313313
});
314+
315+
test('works with proxyquire (eslint tests)', async () => {
316+
await using fixture = await createFixture({
317+
'index.js': `
318+
const proxyquire = require('proxyquire');
319+
const tsx = require(${JSON.stringify(tsxCjsApiPath)});
320+
321+
tsx.register();
322+
323+
proxyquire('./test.js', {
324+
path: {
325+
sep: 'hello world',
326+
},
327+
});
328+
`,
329+
330+
'test.js': `
331+
const path = require('path');
332+
console.log(path.sep);
333+
`,
334+
335+
'node_modules/proxyquire': ({ symlink }) => symlink(path.resolve('node_modules/proxyquire'), 'junction'),
336+
});
337+
338+
const { stdout } = await execaNode('./index.js', {
339+
cwd: fixture.path,
340+
nodePath: node.path,
341+
nodeOptions: [],
342+
});
343+
344+
expect(stdout).toBe('hello world');
345+
});
314346
});
315347
});
316348

0 commit comments

Comments
 (0)
Please sign in to comment.