|
| 1 | +'use strict' |
| 2 | +const { create } = require('@apm-js-collab/code-transformer') |
| 3 | +const Module = require('node:module') |
| 4 | +const parse = require('module-details-from-path') |
| 5 | +const getPackageVersion = require('./lib/get-package-version') |
| 6 | +const debug = require('debug')('@apm-js-collab/tracing-hooks:module-patch') |
| 7 | + |
| 8 | +class ModulePatch { |
| 9 | + constructor({ packages = new Set(), instrumentations = [] } = {}) { |
| 10 | + this.packages = packages |
| 11 | + this.instrumentator = create(instrumentations) |
| 12 | + this.transformers = new Map() |
| 13 | + this.resolve = Module._resolveFilename |
| 14 | + this.compile = Module.prototype._compile |
| 15 | + } |
| 16 | + |
| 17 | + /** |
| 18 | + * Patches the Node.js module class methods that are responsible for resolving filePaths and compiling code. |
| 19 | + * If a module is found that has an instrumentator, it will transform the code before compiling it |
| 20 | + * with tracing channel methods. |
| 21 | + */ |
| 22 | + patch() { |
| 23 | + const self = this |
| 24 | + Module._resolveFilename = function wrappedResolveFileName() { |
| 25 | + const resolvedName = self.resolve.apply(this, arguments) |
| 26 | + const resolvedModule = parse(resolvedName) |
| 27 | + if (resolvedModule && self.packages.has(resolvedModule.name)) { |
| 28 | + const version = getPackageVersion(resolvedModule.basedir, resolvedModule.name) |
| 29 | + const transformer = self.instrumentator.getTransformer(resolvedModule.name, version, resolvedModule.path) |
| 30 | + if (transformer) { |
| 31 | + self.transformers.set(resolvedName, transformer) |
| 32 | + } |
| 33 | + } |
| 34 | + return resolvedName |
| 35 | + } |
| 36 | + |
| 37 | + Module.prototype._compile = function wrappedCompile(...args) { |
| 38 | + const [content, filename] = args |
| 39 | + if (self.transformers.has(filename)) { |
| 40 | + const transformer = self.transformers.get(filename) |
| 41 | + try { |
| 42 | + const transformedCode = transformer.transform(content, 'unknown') |
| 43 | + args[0] = transformedCode |
| 44 | + } catch (error) { |
| 45 | + debug('Error transforming module %s: %o', filename, error) |
| 46 | + } finally { |
| 47 | + transformer.free() |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + return self.compile.apply(this, args) |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Clears all the transformers and restores the original Module methods that were wrapped. |
| 57 | + * **Note**: This is intended to be used in testing only. |
| 58 | + */ |
| 59 | + unpatch() { |
| 60 | + this.transformers.clear() |
| 61 | + Module._resolveFilename = this.resolve |
| 62 | + Module.prototype._compile = this.compile |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +module.exports = ModulePatch |
| 67 | + |
0 commit comments