Skip to content

Commit 7fa4e9c

Browse files
authored
Fix resolution of lib/register.js when using multiple loaders (#76)
This PR fixes the issue that's been described at length in the following issues: - getsentry/sentry-javascript#12011 - nodejs/node#52987 To summarize, `import-in-the-middle` can cause problems in the following situation: - A Node core module (`node:*`) is imported - Another loader is added before `import-in-the-middle` - That loader tries to resolve files that don't exist on disk In practice, this occurs when using `import-in-the-middle` with code that's being executed with [`tsx`](https://github.com/privatenumber/tsx). `tsx` will try to resolve `.js` files by also looking for the same file path but with a `.ts` extension. In many cases, including the synthetic code that `import-in-the-middle` generates to import `lib/register.js`, such a corresponding `.ts` file does not exist. The actual error arises from Node, which assumes that `parentURL` will always be a `file://` URL when constructing an `ERR_MODULE_NOT_FOUND` error; see the linked issue above. In the above scenario, the `.ts` file that is being resolved does not exist, so such an error is created, and `parentURL === 'node:*'`, so the failing case is triggered. It seems like Node is receptive to changing that behavior, but in the meantime, I was hoping to land this patch so that one doesn't have to wait for and upgrade to a new version of Node. The fix works by short-circuiting the resolution of `lib/register.js` so that the other loader (that tries to resolve non-existent paths) is never tried.
1 parent e7f05ca commit 7fa4e9c

File tree

4 files changed

+44
-2
lines changed

4 files changed

+44
-2
lines changed

hook.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,19 @@ function addIitm (url) {
265265

266266
function createHook (meta) {
267267
let cachedResolve
268+
const iitmURL = new URL('lib/register.js', meta.url).toString()
269+
268270
async function resolve (specifier, context, parentResolve) {
269271
cachedResolve = parentResolve
272+
273+
// See github.com/DataDog/import-in-the-middle/pull/76.
274+
if (specifier === iitmURL) {
275+
return {
276+
url: specifier,
277+
shortCircuit: true
278+
}
279+
}
280+
270281
const { parentURL = '' } = context
271282
const newSpecifier = deleteIitm(specifier)
272283
if (isWin && parentURL.indexOf('file:node') === 0) {
@@ -308,7 +319,6 @@ function createHook (meta) {
308319
}
309320
}
310321

311-
const iitmURL = new URL('lib/register.js', meta.url).toString()
312322
async function getSource (url, context, parentGetSource) {
313323
if (hasIitm(url)) {
314324
const realUrl = deleteIitm(url)

test/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ options (assuming they're run from the project root):
1111

1212
```
1313
--require ./test/version-check.js
14-
--experimental-loader ./test/generic-loader.mmjs
14+
--experimental-loader ./test/generic-loader.mjs
1515
```
1616

1717
The entire test suite can be run with `npm test`.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { register } from 'node:module'
2+
3+
register('./typescript-loader.mjs', import.meta.url)
4+
register('../../hook.mjs', import.meta.url)
5+
6+
await import('node:util')
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* This simulates what something like `tsx` (https://github.com/privatenumber/tsx)
3+
* will do: it will try to resolve a URL with a `.js` extension to a `.ts` extension.
4+
*
5+
* Combined with the test case in the adjacent `multiple-loaders.test.mjs` file,
6+
* this forces `import-in-the-middle` into what used to be a failure state: where
7+
* `context.parentURL` is a `node:*` specifier and the `specifier` refers to a file
8+
* that does not exist.
9+
*
10+
* See https://github.com/nodejs/node/issues/52987 for more details.
11+
*/
12+
export async function resolve (specifier, context, defaultResolve) {
13+
if (!specifier.endsWith('.js') && !specifier.endsWith('.mjs')) {
14+
return await defaultResolve(specifier, context)
15+
}
16+
17+
try {
18+
return await defaultResolve(specifier.replace(/\.m?js/, '.ts'), context)
19+
} catch (err) {
20+
if (err.code !== 'ERR_MODULE_NOT_FOUND') {
21+
throw err
22+
}
23+
24+
return await defaultResolve(specifier, context)
25+
}
26+
}

0 commit comments

Comments
 (0)