Skip to content
Open
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
48 changes: 35 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,49 @@ When called a `hook` object is returned.

Arguments:

- `modules` <string[]> An optional array of module names to limit which modules
trigger a call of the `onrequire` callback. If specified, this must be the
first argument. Both regular modules (e.g. `react-dom`) and
sub-modules (e.g. `react-dom/server`) can be specified in the array.
- `options` <Object> An optional object containing fields that change when the
`onrequire` callback is called. If specified, this must be the second
argument.
- `options.internals` <boolean> Specifies whether `onrequire` should be called
when module-internal files are loaded; defaults to `false`.
- `onrequire` <Function> The function to call when a module is required.
- `modules` {string[]} An optional array of module names or normalized module
sub-paths to limit which `require(...)` calls will trigger a call of the
`onrequire` callback. If specified, this must be the first argument. There
are a number of forms these entries can take:

- A package name, e.g., `express` or `@fastify/busboy`.
- A package [entry-point](https://nodejs.org/api/packages.html#package-entry-points),
as listed in the "exports" entry in a package's "package.json" file, e.g.
`some-package/entry-point`.
- A package sub-module.
E.g., `express/lib/request` will hook
`.../node_modules/express/lib/request.js` and `express/lib/router` will hook
`.../node_modules/express/lib/router/index.js`. (Note: To hook an internal
package file using the `.cjs` extension you must specify the extension in
the `modules` entry. E.g. `@langchain/core/dist/callbacks/manager.cjs` is
required to hook
`.../node_modules/@langchain/core/dist/callbacks/manager.cjs`.
This is because []`.cjs` is not handled specially by `require()` the way
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This is because []`.cjs` is not handled specially by `require()` the way
This is because [`.cjs` is not handled specially by `require()` the way

`.js` is](https://nodejs.org/api/modules.html#file-modules).)
- A package sub-path, *if `options.internals === true`*. Using the `internals`
option allows hooking raw paths inside a package. The hook arguments for
these paths **include the file extension**. E.g.,
`new Hook(['@redis/client/dist/lib/client/index.js'], {internals: true}, ...`
will hook `.../node_modules/@redis/client/dist/lib/client/index.js`.

- `options` {Object} An optional object to configure Hook behaviour. If
specified, this must be the second argument.

- `options.internals` {boolean} Specifies whether `onrequire` should be called
when any module-internal files are loaded; defaults to `false`.

- `onrequire` {Function} The function to call when a module is required.

The `onrequire` callback will be called the first time a module is
required. The function is called with three arguments:

- `exports` <Object> The value of the `module.exports` property that would
- `exports` {Object} The value of the `module.exports` property that would
normally be exposed by the required module.
- `name` <string> The name of the module being required. If `options.internals`
- `name` {string} The name of the module being required. If `options.internals`
was set to `true`, the path of module-internal files that are loaded
(relative to `basedir`) will be appended to the module name, separated by
`path.sep`.
- `basedir` <string> The directory where the module is located, or `undefined`
- `basedir` {string} The directory where the module is located, or `undefined`
for core modules.

Return the value you want the module to expose (normally the `exports`
Expand Down
85 changes: 85 additions & 0 deletions test/cjs-sub-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict'

// This tests that sub-module files using the `.cjs` extension are *not*
// hookable via a normalized module path. Instead one must use the .cjs
// extension on the hook arg.
//
// E.g., a Hook arg of `cjs-sub-module/foo` will **not** hook
// `./node_modules/cjs-sub-module/foo.cjs`. This is different compared to `.js`
// file extension usage. The difference is that Node.js's `require()` treats
// `.js` and `.cjs` differently.
// See https://nodejs.org/api/modules.html#file-modules
Comment on lines +7 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the reasoning, but it would make instrumenting modules very complicated. The end user API of require-in-the-middle is that the export of whatever targeted module is provided to the hook function. The (package.json).exports map complicates this as it can resolve the same export name to multiple extensions. In my view, the hook function shouldn't need to know about that detail. It should just receive the right thing.


const test = require('tape')

const { Hook } = require('../')

test('require("cjs-sub-module/foo") does NOT hook cjs-sub-module/foo.cjs', function (t) {
const hook = new Hook(['cjs-sub-module/foo'], function (exports) {
t.fail('should not get here')
return exports
})

t.equal(require('./node_modules/cjs-sub-module'), 'cjs-sub-module/index.js')
t.equal(require('./node_modules/cjs-sub-module/foo.cjs'), 'cjs-sub-module/foo.cjs')

try {
require('./node_modules/cjs-sub-module/foo')
t.fail('the previous require should throw')
} catch (err) {
t.ok(/Cannot find module/.test(err.message), 'got expected exception')
}

hook.unhook()
t.end()
})

test('require("cjs-sub-module/bar") does NOT hook cjs-sub-module/bar/index.cjs', function (t) {
const hook = new Hook(['cjs-sub-module/bar'], function (exports) {
t.fail('should not get here')
return exports
})

t.equal(require('./node_modules/cjs-sub-module'), 'cjs-sub-module/index.js')
t.equal(require('./node_modules/cjs-sub-module/bar/index.cjs'), 'cjs-sub-module/bar/index.cjs')

try {
require('./node_modules/cjs-sub-module/bar')
t.fail('the previous require should throw')
} catch (err) {
t.ok(/Cannot find module/.test(err.message), 'got expected exception')
}

hook.unhook()
t.end()
})

test('require("cjs-sub-module/foo.cjs") DOES hook cjs-sub-module/foo.cjs', function (t) {
const hookedNames = []
const hook = new Hook(['cjs-sub-module/foo.cjs'], function (exports, name) {
hookedNames.push(name)
return exports
})

t.equal(require('./node_modules/cjs-sub-module'), 'cjs-sub-module/index.js')
t.equal(require('./node_modules/cjs-sub-module/foo.cjs'), 'cjs-sub-module/foo.cjs')
t.deepEqual(hookedNames, ['cjs-sub-module/foo.cjs'])

hook.unhook()
t.end()
})

test('require("cjs-sub-module/bar/index.cjs") DOES hook cjs-sub-module/bar/index.cjs', function (t) {
const hookedNames = []
const hook = new Hook(['cjs-sub-module/bar/index.cjs'], function (exports, name) {
hookedNames.push(name)
return exports
})

t.equal(require('./node_modules/cjs-sub-module'), 'cjs-sub-module/index.js')
t.equal(require('./node_modules/cjs-sub-module/bar/index.cjs'), 'cjs-sub-module/bar/index.cjs')
t.deepEqual(hookedNames, ['cjs-sub-module/bar/index.cjs'])

hook.unhook()
t.end()
})
1 change: 1 addition & 0 deletions test/node_modules/cjs-sub-module/bar/index.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/node_modules/cjs-sub-module/foo.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/node_modules/cjs-sub-module/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.