I work with appoptics-apm and have been watching progress on ECMAScript modules from a distance for a while. I'd like to thank everyone for wrestling with the complexities involved in integrating ES modules with CJS modules.
I started working with node 13 recently to test how patching will work with ES modules. At this point it's not clear to me. I know that patching is a bit of an edge case but APM products depend on it. I haven't followed every conversation on this in detail so it's quite possible that I'm missing some basic things that have been discussed and should be obvious.
For any not familiar with how we patch, here's a quick overview.
- our agent is required before any other modules.
- it replaces
module.constructor.prototype.require with our patchedRequire()
patchedRequire() uses realRequire() to load the requested module (mod).
patchedRequire() loads package.json using this.require.call(this, 'name/package.json')
patchedRequire() creates a relative require function for this module using this.require.bind(this)
patchedRequire() requires the instrumentation code and executes it with the argument mod
- the instrumentation code returns the patched
mod
patchedRequire() replaces the cached version of mod with the patched version of mod
Step 4 is used to make decisions about what must be patched for a given version of the package being loaded.
Step 5 is used when patching a package requires that files in its lib (or other directory) need to be patched as well.
There are other approaches and I'm open to changing how our patching works if need be. But given how this seems to be working now it's not clear to me that hooks are intended to address APM-style monkey-patching.
The following code is my attempt to get started with how this would work.
Example
simple.js:
import lib from './lib-func.mjs';
import {another} from './lib-func.mjs';
console.log(lib, another);
hooks/simple.js
import {pathToFileURL} from 'url';
const baseURL = pathToFileURL(process.cwd()).href;
const doImport = false;
const log = function (...args) {args.unshift('hooks:'); console.log(...args);}
const probes = new Map();
export async function resolve (specifier, parentURL, defaultResolver) {
const isMain = parentURL === undefined;
// resolve it using the default function.
const resolution = defaultResolver(specifier, isMain ? baseURL : parentURL);
log(`resolving ${specifier}`);
// how to know "main" modules (to set require context)?
if (specifier === './lib-func.mjs') {
log(`making ${specifier} dynamic`);
// is there any other way to get get control via the `dynamicInstantiate()` hook?
resolution.format = 'dynamic';
probes[resolution.url] = specifier;
}
return resolution;
}
export async function dynamicInstantiate (url) {
log(`dynamic ${url}`);
if (url in probes) {
log(`found probe`, probes[url]);
if (!doImport) {
return {
exports: ['default'],
execute: (exports) => {
exports.default.set(function hacking () {return 'hacking'});
}
}
} else {
return import(url).then(m => {
return {
exports: ['default'],
execute: (exports) => {
console.log('execute', exports);
exports.default.set(m);
}
}
})
}
} else {
return {
exports: ['default'],
execute: (exports) => {
exports.default.set(function fakeXyzzy () {return 'really don\'t want to be here'});
}
}
}
}
When execute it as is it works, more or less as I would expect it to.
bruce@uxpanapa:~/working/esm-modules$ node --experimental-loader ./hooks/simple.mjs ./simple.mjs
(node:1343) ExperimentalWarning: The ESM module loader is experimental.
(node:1343) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
hooks: resolving file:///home/bruce/working/esm-modules/simple.mjs
hooks: resolving ./lib-func.mjs
hooks: making ./lib-func.mjs dynamic
hooks: dynamic file:///home/bruce/working/esm-modules/lib-func.mjs
hooks: found probe ./lib-func.mjs
[Function: hacking]
hacking
But when I change doImport to true it fails to output anything.
bruce@uxpanapa:~/working/esm-modules$ node --experimental-loader ./hooks/simple.mjs ./simple.mjs
(node:1701) ExperimentalWarning: The ESM module loader is experimental.
(node:1701) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
hooks: resolving file:///home/bruce/working/esm-modules/simple.mjs
hooks: resolving ./lib-func.mjs
hooks: making ./lib-func.mjs dynamic
hooks: dynamic file:///home/bruce/working/esm-modules/lib-func.mjs
hooks: found probe ./lib-func.mjs
hooks: resolving file:///home/bruce/working/esm-modules/lib-func.mjs
What am I missing? Is it not possible to execute import() in the dynamic hook? I can see how a dynamic hook would allow arbitrary code to be created but I don't see how I would patch the existing module (and/or cache).
Any pointers would be appreciated.
I work with appoptics-apm and have been watching progress on ECMAScript modules from a distance for a while. I'd like to thank everyone for wrestling with the complexities involved in integrating ES modules with CJS modules.
I started working with node 13 recently to test how patching will work with ES modules. At this point it's not clear to me. I know that patching is a bit of an edge case but APM products depend on it. I haven't followed every conversation on this in detail so it's quite possible that I'm missing some basic things that have been discussed and should be obvious.
For any not familiar with how we patch, here's a quick overview.
module.constructor.prototype.requirewith ourpatchedRequire()patchedRequire()usesrealRequire()to load the requested module (mod).patchedRequire()loadspackage.jsonusingthis.require.call(this, 'name/package.json')patchedRequire()creates a relative require function for this module usingthis.require.bind(this)patchedRequire()requires the instrumentation code and executes it with the argumentmodmodpatchedRequire()replaces the cached version ofmodwith the patched version ofmodStep 4 is used to make decisions about what must be patched for a given version of the package being loaded.
Step 5 is used when patching a package requires that files in its
lib(or other directory) need to be patched as well.There are other approaches and I'm open to changing how our patching works if need be. But given how this seems to be working now it's not clear to me that hooks are intended to address APM-style monkey-patching.
The following code is my attempt to get started with how this would work.
Example
simple.js:
hooks/simple.js
When execute it as is it works, more or less as I would expect it to.
But when I change
doImportto true it fails to output anything.What am I missing? Is it not possible to execute
import()in the dynamic hook? I can see how a dynamic hook would allow arbitrary code to be created but I don't see how I would patch the existing module (and/or cache).Any pointers would be appreciated.