Skip to content

Commit

Permalink
feat: support hooking process.getBuiltinModule (#92)
Browse files Browse the repository at this point in the history
Also, run CI tests with Node 22, drop v19 testing
  • Loading branch information
watson authored Jul 25, 2024
1 parent c3e267e commit 714ce69
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
test-vers:
strategy:
matrix:
node: ['8.6', '8', '10.0', '10', '12.0', '12', '14.0', '14', '16.0', '16', '18.0', '18', '19', '20']
node: ['8.6', '8', '10.0', '10', '12.0', '12', '14.0', '14', '16.0', '16', '18.0', '18', '20', '22']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ modules on-the-fly as they are being required.
[![npm](https://img.shields.io/npm/v/require-in-the-middle.svg)](https://www.npmjs.com/package/require-in-the-middle)
[![Test status](https://github.com/elastic/require-in-the-middle/workflows/Test/badge.svg)](https://github.com/elastic/require-in-the-middle/actions)

Also supports hooking into calls to `process.getBuiltinModule()`, which was introduced in Node.js v22.3.0.

## Installation

Expand Down Expand Up @@ -72,7 +73,7 @@ argument).
### `hook.unhook()`

Removes the `onrequire` callback so that it will not be triggerd by
subsequent calls to `require()`.
subsequent calls to `require()` or `process.getBuiltinModule()`.

## License

Expand Down
47 changes: 43 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ function Hook (modules, options, onrequire) {
return self._origRequire.apply(this, arguments)
}

return patchedRequire.call(this, arguments, false)
}

if (typeof process.getBuiltinModule === 'function') {
this._origGetBuiltinModule = process.getBuiltinModule
this._getBuiltinModule = process.getBuiltinModule = function (id) {
if (self._unhooked === true) {
// if the patched process.getBuiltinModule function could not be removed because
// someone else patched it after it was patched here, we just abort and pass the
// request onwards to the original process.getBuiltinModule
debug('ignoring process.getBuiltinModule call - module is soft-unhooked')
return self._origGetBuiltinModule.apply(this, arguments)
}

return patchedRequire.call(this, arguments, true)
}
}

// Preserve the original require/process.getBuiltinModule arguments in `args`
function patchedRequire (args, coreOnly) {
const id = args[0]
const core = isCore(id)
let filename // the string used for caching
if (core) {
Expand All @@ -151,6 +172,12 @@ function Hook (modules, options, onrequire) {
filename = idWithoutPrefix
}
}
} else if (coreOnly) {
// `coreOnly` is `true` if this was a call to `process.getBuiltinModule`, in which case
// we don't want to return anything if the requested `id` isn't a core module. Falling
// back to default behaviour, which at the time of this wrting is simply returning `undefined`
debug('call to process.getBuiltinModule with unknown built-in id')
return self._origGetBuiltinModule.apply(this, args)
} else {
try {
filename = Module._resolveFilename(id, this)
Expand All @@ -164,7 +191,7 @@ function Hook (modules, options, onrequire) {
// where `@azure/functions-core` resolves to an internal object.
// https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.5.2/src/setupCoreModule.ts#L46-L54
debug('Module._resolveFilename("%s") threw %j, calling original Module.require', id, resolveErr.message)
return self._origRequire.apply(this, arguments)
return self._origRequire.apply(this, args)
}
}

Expand All @@ -185,7 +212,9 @@ function Hook (modules, options, onrequire) {
patching.add(filename)
}

const exports = self._origRequire.apply(this, arguments)
const exports = coreOnly
? self._origGetBuiltinModule.apply(this, args)
: self._origRequire.apply(this, args)

// If it's already patched, just return it as-is.
if (isPatching === true) {
Expand Down Expand Up @@ -288,11 +317,21 @@ function Hook (modules, options, onrequire) {

Hook.prototype.unhook = function () {
this._unhooked = true

if (this._require === Module.prototype.require) {
Module.prototype.require = this._origRequire
debug('unhook successful')
debug('require unhook successful')
} else {
debug('unhook unsuccessful')
debug('require unhook unsuccessful')
}

if (process.getBuiltinModule !== undefined) {
if (this._getBuiltinModule === process.getBuiltinModule) {
process.getBuiltinModule = this._origGetBuiltinModule
debug('process.getBuiltinModule unhook successful')
} else {
debug('process.getBuiltinModule unhook unsuccessful')
}
}
}

Expand Down
65 changes: 65 additions & 0 deletions test/process-getbuiltinmodule.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict'

const test = require('tape')

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

// The `process.getBuiltinModule(id) function was added in Node.js 22.3.0
const skip = !process.getBuiltinModule

test('process.getBuiltinModule should be patched', { skip }, function (t) {
let numOnRequireCalls = 0

const hook = new Hook(['http'], function (exports, name, basedir) {
numOnRequireCalls++
return exports
})

const a = process.getBuiltinModule('http')
t.equal(numOnRequireCalls, 1)

const b = require('http')
t.equal(numOnRequireCalls, 1)

t.strictEqual(a, b, 'modules are the same')

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

test('patched process.getBuiltinModule should work with node: prefix', { skip }, function (t) {
let numOnRequireCalls = 0

const hook = new Hook(['http'], function (exports, name, basedir) {
numOnRequireCalls++
return exports
})

process.getBuiltinModule('node:http')
t.equal(numOnRequireCalls, 1)
t.end()
hook.unhook()
})

test('patched process.getBuiltinModule should preserve default behavior for non-builtin modules', { skip }, function (t) {
const beforePatching = process.getBuiltinModule('ipp-printer')

const hook = new Hook(['ipp-printer'], function (exports, name, basedir) {
t.fail('should not call hook')
})

const afterPatching = process.getBuiltinModule('ipp-printer')

t.strictEqual(beforePatching, afterPatching, 'modules are the same')
t.end()
hook.unhook()
})

test('hook.unhook() works for process.getBuiltinModule', { skip }, function (t) {
const hook = new Hook(['http'], function (exports, name, basedir) {
t.fail('should not call onrequire')
})
hook.unhook()
process.getBuiltinModule('http')
t.end()
})

0 comments on commit 714ce69

Please sign in to comment.