55const path = require ( 'path' )
66const parse = require ( 'module-details-from-path' )
77const { fileURLToPath } = require ( 'url' )
8+ const { MessageChannel } = require ( 'worker_threads' )
89
910const {
1011 importHooks,
@@ -31,6 +32,75 @@ function callHookFn (hookFn, namespace, name, baseDir) {
3132 }
3233}
3334
35+ let sendModulesToLoader
36+
37+ /**
38+ * EXPERIMENTAL
39+ * This feature is experimental and may change in minor versions.
40+ * **NOTE** This feature is incompatible with the {internals: true} Hook option.
41+ *
42+ * Creates a message channel with a port that can be used to add hooks to the
43+ * list of exclusively included modules.
44+ *
45+ * This can be used to only wrap modules that are Hook'ed, however modules need
46+ * to be hooked before they are imported.
47+ *
48+ * ```ts
49+ * import { register } from 'module'
50+ * import { Hook, createAddHookMessageChannel } from 'import-in-the-middle'
51+ *
52+ * const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel()
53+ *
54+ * register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions)
55+ *
56+ * Hook(['fs'], (exported, name, baseDir) => {
57+ * // Instrument the fs module
58+ * })
59+ *
60+ * // Ensure that the loader has acknowledged all the modules
61+ * // before we allow execution to continue
62+ * await waitForAllMessagesAcknowledged()
63+ * ```
64+ */
65+ function createAddHookMessageChannel ( ) {
66+ const { port1, port2 } = new MessageChannel ( )
67+ let pendingAckCount = 0
68+ let resolveFn
69+
70+ sendModulesToLoader = ( modules ) => {
71+ pendingAckCount ++
72+ port1 . postMessage ( modules )
73+ }
74+
75+ port1 . on ( 'message' , ( ) => {
76+ pendingAckCount --
77+
78+ if ( resolveFn && pendingAckCount <= 0 ) {
79+ resolveFn ( )
80+ }
81+ } ) . unref ( )
82+
83+ function waitForAllMessagesAcknowledged ( ) {
84+ // This timer is to prevent the process from exiting with code 13:
85+ // 13: Unsettled Top-Level Await.
86+ const timer = setInterval ( ( ) => { } , 1000 )
87+ const promise = new Promise ( ( resolve ) => {
88+ resolveFn = resolve
89+ } ) . then ( ( ) => { clearInterval ( timer ) } )
90+
91+ if ( pendingAckCount === 0 ) {
92+ resolveFn ( )
93+ }
94+
95+ return promise
96+ }
97+
98+ const addHookMessagePort = port2
99+ const registerOptions = { data : { addHookMessagePort, include : [ ] } , transferList : [ addHookMessagePort ] }
100+
101+ return { registerOptions, addHookMessagePort, waitForAllMessagesAcknowledged }
102+ }
103+
34104function Hook ( modules , options , hookFn ) {
35105 if ( ( this instanceof Hook ) === false ) return new Hook ( modules , options , hookFn )
36106 if ( typeof modules === 'function' ) {
@@ -43,6 +113,10 @@ function Hook (modules, options, hookFn) {
43113 }
44114 const internals = options ? options . internals === true : false
45115
116+ if ( sendModulesToLoader && Array . isArray ( modules ) ) {
117+ sendModulesToLoader ( modules )
118+ }
119+
46120 this . _iitmHook = ( name , namespace ) => {
47121 const filename = name
48122 const isBuiltin = name . startsWith ( 'node:' )
@@ -92,3 +166,4 @@ module.exports = Hook
92166module . exports . Hook = Hook
93167module . exports . addHook = addHook
94168module . exports . removeHook = removeHook
169+ module . exports . createAddHookMessageChannel = createAddHookMessageChannel
0 commit comments