Skip to content

with internals: true the name returned for the imported specifier is the implementation file, different from RITM #238

@trentm

Description

@trentm

Let's hook import('got') -- a type=module package -- using internals: true:

import { Hook } from 'import-in-the-middle'
register('import-in-the-middle/hook.mjs', import.meta.url) 
Hook(['got'], { internals: true }, (exports, name, baseDir) => {
  console.log('HIT: name=%s', name)
})
await import('got')

The result:

HIT: name=got/dist/source/core/errors.js
...
HIT: name=got/dist/source/core/utils/is-unix-socket-url.js
HIT: name=got/dist/source/core/diagnostics-channel.js
HIT: name=got/dist/source/core/index.js
HIT: name=got/dist/source/as-promise/types.js
HIT: name=got/dist/source/as-promise/index.js
HIT: name=got/dist/source/create.js
HIT: name=got/dist/source/types.js
HIT: name=got/dist/source/index.js   <--- This one differs from RITM

With RITM the callback name value for the loaded specifier is the specifier, rather than the internal path. E.g. with ioredis:

const { Hook } = require('require-in-the-middle')
Hook(['ioredis'], { internals: true }, (exports, name, baseDir) => {
  console.log('HIT: name=%s', name)
  return exports
})
require('ioredis')

The result:

HIT: name=ioredis/built/utils/lodash.js
HIT: name=ioredis/built/utils/debug.js
...
HIT: name=ioredis/built/DataHandler.js
HIT: name=ioredis/built/redis/event_handler.js
HIT: name=ioredis/built/redis/RedisOptions.js
HIT: name=ioredis/built/Redis.js
HIT: name=ioredis   <--- This one differs from IITM

RITM is preferring the specifier/id match over the internals: true-match.

My motivation

This currently breaks OTel's attempt to use internals: true with its IITM hook. I want to use internals: true as an alternative to @isaacs' open-telemetry/opentelemetry-js#6246 (I don't want to move to using Hook arguments that match entry-points because of some issues with that that I haven't yet written up on that issue.) The motivation for that PR to OTel is to support hooking @langchain/langgraph/prebuilt with OTel instrumentation (see https://github.com/getsentry/sentry-javascript/pull/18403/files#diff-e6fb561aa706da86d96d54111d486f4472ac8c4c069453a29d456da4c8f3ffcbR35).

I might try to change OTel's IITM usage to work around this, but I haven't done that yet.

Option 1: Change IITM to match RITM here

Assuming #236 and then #237 get merged, the change would be to swap the specifier/id-check and the internals-check:

diff --git a/index.js b/index.js
index 975deac..f206f25 100644
--- a/index.js
+++ b/index.js
@@ -151,9 +151,6 @@ function Hook (modules, options, hookFn) {
           if (!baseDir) {
             // built-in module (or unexpected non file:// name?)
             callHookFn(hookFn, namespace, name, baseDir)
-          } else if (internals) {
-            const internalPath = name + path.sep + path.relative(baseDir, fileURLToPath(filename))
-            callHookFn(hookFn, namespace, internalPath, baseDir)
           } else if (baseDir.endsWith(specifiers.get(filename))) {
             // An import of the top-level module (e.g. `import 'ioredis'`).
             // Note: Slight behaviour difference from RITM. RITM uses
@@ -161,6 +158,9 @@ function Hook (modules, options, hookFn) {
             // main file, which will catch `require('ioredis/built/index.js')`.
             // The check here will not catch `import 'ioredis/built/index.js'`.
             callHookFn(hookFn, namespace, name, baseDir)
+          } else if (internals) {
+            const internalPath = name + path.sep + path.relative(baseDir, fileURLToPath(filename))
+            callHookFn(hookFn, namespace, internalPath, baseDir)
           }
         } else if (matchArg === specifier) {
           callHookFn(hookFn, namespace, specifier, baseDir)

Option 2: do that, but also add a 4th subPath arg to the callback

With this option the callback would be:

function onImport(mod, name, baseDir, modPath)

where modPath is the ${moduleName}/${subPath}, e.g. ioredis/built/index.js.
I haven't prototyped this. That modPath could be added just for internals: true, or in all cases.

This would allow users of RITM and IITM to be more specific about which implementation files to monkey-patch:

  • not "when user code loads 'ioredis'",
  • but "whenever the file that implements that we want to monkey-patch is loaded"

This is the API that orchestrion-js has: module: { name: 'pkg1', verisonRange: '>=1.0.0', filePath: 'index.js' }, (excluding the versionRange, of course, though that could be interesting).

Thoughts?

Option 3: leave it alone

... and document the subtlety.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions