From ea5f6fd1c0894bf5f81a7f23f1926f833e3ae596 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Mon, 6 May 2024 16:59:36 -0700 Subject: [PATCH] docs, test: clarify how to hook an module sub-path using .cjs extension Refs: https://github.com/elastic/require-in-the-middle/pull/88#issuecomment-2097114383 --- README.md | 48 ++++++++--- test/cjs-sub-module.js | 85 +++++++++++++++++++ .../node_modules/cjs-sub-module/bar/index.cjs | 1 + test/node_modules/cjs-sub-module/foo.cjs | 1 + test/node_modules/cjs-sub-module/index.js | 1 + 5 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 test/cjs-sub-module.js create mode 100644 test/node_modules/cjs-sub-module/bar/index.cjs create mode 100644 test/node_modules/cjs-sub-module/foo.cjs create mode 100644 test/node_modules/cjs-sub-module/index.js diff --git a/README.md b/README.md index 9aafd73..a690c50 100644 --- a/README.md +++ b/README.md @@ -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 + `.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` diff --git a/test/cjs-sub-module.js b/test/cjs-sub-module.js new file mode 100644 index 0000000..feed10e --- /dev/null +++ b/test/cjs-sub-module.js @@ -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 + +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() +}) diff --git a/test/node_modules/cjs-sub-module/bar/index.cjs b/test/node_modules/cjs-sub-module/bar/index.cjs new file mode 100644 index 0000000..b8f4a5d --- /dev/null +++ b/test/node_modules/cjs-sub-module/bar/index.cjs @@ -0,0 +1 @@ +module.exports = 'cjs-sub-module/bar/index.cjs' diff --git a/test/node_modules/cjs-sub-module/foo.cjs b/test/node_modules/cjs-sub-module/foo.cjs new file mode 100644 index 0000000..bd66518 --- /dev/null +++ b/test/node_modules/cjs-sub-module/foo.cjs @@ -0,0 +1 @@ +module.exports = 'cjs-sub-module/foo.cjs' diff --git a/test/node_modules/cjs-sub-module/index.js b/test/node_modules/cjs-sub-module/index.js new file mode 100644 index 0000000..4877b02 --- /dev/null +++ b/test/node_modules/cjs-sub-module/index.js @@ -0,0 +1 @@ +module.exports = 'cjs-sub-module/index.js'