diff --git a/.gitignore b/.gitignore index ba2a97b..496aa41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules coverage +test/trace diff --git a/.snapshots/6a43d2825780d3f626066653ae42ea9b/0.json b/.snapshots/6a43d2825780d3f626066653ae42ea9b/0.json new file mode 100644 index 0000000..5a10c93 --- /dev/null +++ b/.snapshots/6a43d2825780d3f626066653ae42ea9b/0.json @@ -0,0 +1 @@ +"class Foo {\n constructor() {\n this.name = 'foo'\n }\n\n doStuff() {\n return 'doing stuff'\n }\n}\n\nexport default Foo\n\n" \ No newline at end of file diff --git a/.snapshots/92c32a3caef2a8ae0f34a33c4d0c9c09/0.json b/.snapshots/92c32a3caef2a8ae0f34a33c4d0c9c09/0.json new file mode 100644 index 0000000..710b0ec --- /dev/null +++ b/.snapshots/92c32a3caef2a8ae0f34a33c4d0c9c09/0.json @@ -0,0 +1 @@ +"const tr_ch_apm_dc = require(\"diagnostics_channel\");\nconst {tracingChannel: tr_ch_apm_tracingChannel} = tr_ch_apm_dc;\nconst tr_ch_apm$unitTestCjs = tr_ch_apm_tracingChannel(\"orchestrion:pkg-1:unitTestCjs\");\nconst tr_ch_apm_hasSubscribers = ch => ch.start.hasSubscribers || ch.end.hasSubscribers || ch.asyncStart.hasSubscribers || ch.asyncEnd.hasSubscribers || ch.error.hasSubscribers;\nclass Foo {\n constructor() {\n this.name = 'foo';\n }\n doStuff(...__apm$args) {\n const __apm$arguments = [...__apm$args].slice(0, arguments.length);\n const __apm$ctx = {\n arguments: __apm$arguments,\n self: this,\n moduleVersion: \"1.0.0\"\n };\n const __apm$traced = () => {\n const __apm$wrapped = function () {\n return 'doing stuff';\n };\n return __apm$wrapped.apply(this, __apm$arguments);\n };\n if (!tr_ch_apm_hasSubscribers(tr_ch_apm$unitTestCjs)) return __apm$traced();\n return tr_ch_apm$unitTestCjs.start.runStores(__apm$ctx, () => {\n try {\n let promise = __apm$traced();\n if (typeof promise?.then !== \"function\") {\n __apm$ctx.result = promise;\n return promise;\n }\n if (promise instanceof Promise && promise.constructor === Promise) {\n return promise.then(result => {\n __apm$ctx.result = result;\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n return result;\n }, err => {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestCjs.error.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n throw err;\n });\n }\n promise.then(result => {\n __apm$ctx.result = result;\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n }, err => {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestCjs.error.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n });\n return promise;\n } catch (err) {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestCjs.error.publish(__apm$ctx);\n throw err;\n } finally {\n __apm$ctx.self ??= this;\n tr_ch_apm$unitTestCjs.end.publish(__apm$ctx);\n }\n });\n }\n}\nmodule.exports = Foo;\n" \ No newline at end of file diff --git a/.snapshots/bf201be9f3ad62844fbf7840451d8fb9/0.json b/.snapshots/bf201be9f3ad62844fbf7840451d8fb9/0.json new file mode 100644 index 0000000..5a10c93 --- /dev/null +++ b/.snapshots/bf201be9f3ad62844fbf7840451d8fb9/0.json @@ -0,0 +1 @@ +"class Foo {\n constructor() {\n this.name = 'foo'\n }\n\n doStuff() {\n return 'doing stuff'\n }\n}\n\nexport default Foo\n\n" \ No newline at end of file diff --git a/.snapshots/bfdef7279baf7efbbdbab219c5724025/0.json b/.snapshots/bfdef7279baf7efbbdbab219c5724025/0.json new file mode 100644 index 0000000..e7f5d56 --- /dev/null +++ b/.snapshots/bfdef7279baf7efbbdbab219c5724025/0.json @@ -0,0 +1 @@ +"class Test {\n constructor() {\n this.name = 'Test'\n }\n\n getName() {\n return this.name\n }\n}\n\nexport default Test\n\n" \ No newline at end of file diff --git a/.snapshots/e913999be3e178237d5749cd95c1f7c2/0.json b/.snapshots/e913999be3e178237d5749cd95c1f7c2/0.json new file mode 100644 index 0000000..3eb3330 --- /dev/null +++ b/.snapshots/e913999be3e178237d5749cd95c1f7c2/0.json @@ -0,0 +1 @@ +"class Test {\n constructor() {\n this.name = 'Test'\n }\n\n getName() {\n return this.name\n }\n}\n\nmodule.exports = Test\n" \ No newline at end of file diff --git a/.snapshots/f18d870d94ba54b2020bac0e9cbaafdf/0.json b/.snapshots/f18d870d94ba54b2020bac0e9cbaafdf/0.json new file mode 100644 index 0000000..710b0ec --- /dev/null +++ b/.snapshots/f18d870d94ba54b2020bac0e9cbaafdf/0.json @@ -0,0 +1 @@ +"const tr_ch_apm_dc = require(\"diagnostics_channel\");\nconst {tracingChannel: tr_ch_apm_tracingChannel} = tr_ch_apm_dc;\nconst tr_ch_apm$unitTestCjs = tr_ch_apm_tracingChannel(\"orchestrion:pkg-1:unitTestCjs\");\nconst tr_ch_apm_hasSubscribers = ch => ch.start.hasSubscribers || ch.end.hasSubscribers || ch.asyncStart.hasSubscribers || ch.asyncEnd.hasSubscribers || ch.error.hasSubscribers;\nclass Foo {\n constructor() {\n this.name = 'foo';\n }\n doStuff(...__apm$args) {\n const __apm$arguments = [...__apm$args].slice(0, arguments.length);\n const __apm$ctx = {\n arguments: __apm$arguments,\n self: this,\n moduleVersion: \"1.0.0\"\n };\n const __apm$traced = () => {\n const __apm$wrapped = function () {\n return 'doing stuff';\n };\n return __apm$wrapped.apply(this, __apm$arguments);\n };\n if (!tr_ch_apm_hasSubscribers(tr_ch_apm$unitTestCjs)) return __apm$traced();\n return tr_ch_apm$unitTestCjs.start.runStores(__apm$ctx, () => {\n try {\n let promise = __apm$traced();\n if (typeof promise?.then !== \"function\") {\n __apm$ctx.result = promise;\n return promise;\n }\n if (promise instanceof Promise && promise.constructor === Promise) {\n return promise.then(result => {\n __apm$ctx.result = result;\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n return result;\n }, err => {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestCjs.error.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n throw err;\n });\n }\n promise.then(result => {\n __apm$ctx.result = result;\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n }, err => {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestCjs.error.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestCjs.asyncEnd.publish(__apm$ctx);\n });\n return promise;\n } catch (err) {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestCjs.error.publish(__apm$ctx);\n throw err;\n } finally {\n __apm$ctx.self ??= this;\n tr_ch_apm$unitTestCjs.end.publish(__apm$ctx);\n }\n });\n }\n}\nmodule.exports = Foo;\n" \ No newline at end of file diff --git a/.snapshots/f6db2a11e195aabf3d4758b820f07147/0.json b/.snapshots/f6db2a11e195aabf3d4758b820f07147/0.json new file mode 100644 index 0000000..e5c16ee --- /dev/null +++ b/.snapshots/f6db2a11e195aabf3d4758b820f07147/0.json @@ -0,0 +1 @@ +"const tr_ch_apm_dc = require(\"diagnostics_channel\");\nconst {tracingChannel: tr_ch_apm_tracingChannel} = tr_ch_apm_dc;\nconst tr_ch_apm$unitTestEsm = tr_ch_apm_tracingChannel(\"orchestrion:esm-pkg:unitTestEsm\");\nconst tr_ch_apm_hasSubscribers = ch => ch.start.hasSubscribers || ch.end.hasSubscribers || ch.asyncStart.hasSubscribers || ch.asyncEnd.hasSubscribers || ch.error.hasSubscribers;\nclass Foo {\n constructor() {\n this.name = 'foo';\n }\n doStuff(...__apm$args) {\n const __apm$arguments = [...__apm$args].slice(0, arguments.length);\n const __apm$ctx = {\n arguments: __apm$arguments,\n self: this,\n moduleVersion: \"1.0.0\"\n };\n const __apm$traced = () => {\n const __apm$wrapped = function () {\n return 'doing stuff';\n };\n return __apm$wrapped.apply(this, __apm$arguments);\n };\n if (!tr_ch_apm_hasSubscribers(tr_ch_apm$unitTestEsm)) return __apm$traced();\n return tr_ch_apm$unitTestEsm.start.runStores(__apm$ctx, () => {\n try {\n let promise = __apm$traced();\n if (typeof promise?.then !== \"function\") {\n __apm$ctx.result = promise;\n return promise;\n }\n if (promise instanceof Promise && promise.constructor === Promise) {\n return promise.then(result => {\n __apm$ctx.result = result;\n tr_ch_apm$unitTestEsm.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestEsm.asyncEnd.publish(__apm$ctx);\n return result;\n }, err => {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestEsm.error.publish(__apm$ctx);\n tr_ch_apm$unitTestEsm.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestEsm.asyncEnd.publish(__apm$ctx);\n throw err;\n });\n }\n promise.then(result => {\n __apm$ctx.result = result;\n tr_ch_apm$unitTestEsm.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestEsm.asyncEnd.publish(__apm$ctx);\n }, err => {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestEsm.error.publish(__apm$ctx);\n tr_ch_apm$unitTestEsm.asyncStart.publish(__apm$ctx);\n tr_ch_apm$unitTestEsm.asyncEnd.publish(__apm$ctx);\n });\n return promise;\n } catch (err) {\n __apm$ctx.error = err;\n tr_ch_apm$unitTestEsm.error.publish(__apm$ctx);\n throw err;\n } finally {\n __apm$ctx.self ??= this;\n tr_ch_apm$unitTestEsm.end.publish(__apm$ctx);\n }\n });\n }\n}\nexport default Foo;\n" \ No newline at end of file diff --git a/README.md b/README.md index 9ec0cae..0163aa3 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,32 @@ # Tracing Hooks + This repository contains a ESM loader for injecting tracing channel hooks into Node.js modules. It also has a patch for Module to be used to patch CJS modules. ## Usage -To load esm loader: +Note: the module loading hooks API in Node.js has changed as of +v26. To support all active Node.js versions with +forward-compatibility, create a combined loader as an ESM module. + +This can be done for any CommonJS _or_ ES Module application, but +the loader itself must use ESM. ```js -// esm-loader.mjs -import { register } from 'node:module'; +// loader.mjs +import Module from 'node:module' + +// the synchronous hooks for newer node versions +import { initialize, resolve, load } from '@apm-js-collab/tracing-hooks/hook-sync.mjs' +import ModulePatch from '@apm-js-collab/tracing-hooks' + +// the instrumentations we want to apply const instrumentations = [ { channelName: 'channel1', module: { name: 'pkg1', verisonRange: '>=1.0.0', filePath: 'index.js' }, functionQuery: { className: 'Class1', - methodName: 'method1', + methodName: 'method1', kind: 'Async' } }, @@ -23,58 +35,49 @@ const instrumentations = [ module: { name: 'pkg2', verisonRange: '>=1.0.0', filePath: 'index.js' }, functionQuery: { className: 'Class2, - methodName: 'method2', + methodName: 'method2', kind: 'Sync' } } ] -register('@apm-js-collab/tracing-hooks/hook.mjs', import.meta.url, { - data: { instrumentations } -}); -``` +// detection to decide module loader hooks to use +// registerHooks was present but not stable until 24.13 and 25.1 +const version = (process.versions.node ?? '0.0.0') + .split('.') + .map(n => parseInt(n, 10)) +const stableSyncHooks = version[0] > 25 || + version[0] === 25 && version[1] >= 1 || + version[0] === 24 && version[1] >= 13 -To use the loader, you can run your Node.js application with the `--import` flag: +if (typeof Module.registerHooks === 'function' && stableSyncHooks) { + initialize({ instrumentations }) + Module.registerHooks({ resolve, load }) +} else if (typeof Module.register === 'function') { + Module.register('@apm-js-collab/tracing-hooks/hook.mjs', import.meta.url, { + data: { instrumentations } + }); -```bash -node --import esm-loader.mjs your-app.js + // ALSO patch `Module.prototype._compile` for the CJS side: when + // an ESM file `import`s a CJS package, Node loads the package's + // entry through the ESM bridge but resolves the package's + // INTERNAL `require()` calls through the CJS machinery. + // Those internal requires never reach the ESM resolve hook, so + // without this patch the file we actually want to instrument is + // loaded untransformed. + // This isn't necessary in the registerHooks case, because Node + // applies those hooks to all CJS and ESM modules. + new ModulePatch({ instrumentations }).patch(); +} else { + throw new Error('No available API to apply module load hooks') +} ``` -To load CJS patch: +To run your application with these instrumentations applied, pass +it to the `--import` argument: -```js -// cjs-patch.js -const ModulePatch = require('@apm-js-collab/tracing-hooks') -const instrumentations = [ - { - channelName: 'channel1', - module: { name: 'pkg1', verisonRange: '>=1.0.0', filePath: 'index.js' }, - functionQuery: { - className: 'Class1', - methodName: 'method1', - kind: 'Async' - } - }, - { - channelName: 'channel2', - module: { name: 'pkg2', verisonRange: '>=1.0.0', filePath: 'index.js' }, - functionQuery: { - className: 'Class2', - methodName: 'method2', - kind: 'Sync' - } - } -] - - -const modulePatch = new ModulePatch({ instrumentations }); -modulePatch.patch() ``` - -To use the CJS patch you can run your Node.js application with the `--require` flag: - -```bash -node --require cjs-patch.js your-app.js +node --import=loader.mjs ./my-app.js ``` ## Debugging diff --git a/hook-sync.mjs b/hook-sync.mjs new file mode 100644 index 0000000..6706979 --- /dev/null +++ b/hook-sync.mjs @@ -0,0 +1 @@ +export { initializeSync as initialize, loadSync as load, resolveSync as resolve } from './hook.mjs' diff --git a/hook.mjs b/hook.mjs index d10fa63..0b30ca0 100644 --- a/hook.mjs +++ b/hook.mjs @@ -5,12 +5,16 @@ import { create } from '@apm-js-collab/code-transformer' import parse from 'module-details-from-path' import { fileURLToPath } from 'node:url' import getPackageVersion from './lib/get-package-version.js' +import { readFileSync } from 'node:fs'; const debug = createDebug('@apm-js-collab/tracing-hooks:esm-hook') let transformers = null let packages = null let instrumentator = null export async function initialize(data = {}) { + return initializeSync(data) +} +export function initializeSync(data = {}) { const instrumentations = data?.instrumentations || [] instrumentator = create(instrumentations) packages = new Set(instrumentations.map(i => i.module.name)) @@ -18,7 +22,9 @@ export async function initialize(data = {}) { } export async function resolve(specifier, context, nextResolve) { - const url = await nextResolve(specifier, context) + return resolveFromURL(await nextResolve(specifier, context)) +} +function resolveFromURL(url) { const resolvedModule = parse(url.url) if (resolvedModule && packages.has(resolvedModule.name)) { const path = fileURLToPath(resolvedModule.basedir) @@ -30,9 +36,13 @@ export async function resolve(specifier, context, nextResolve) { } return url } +export function resolveSync(specifier, context, nextResolve) { + return resolveFromURL(nextResolve(specifier, context)) +} export async function load(url, context, nextLoad) { const result = await nextLoad(url, context) + if (transformers.has(url) === false) { return result } @@ -40,8 +50,28 @@ export async function load(url, context, nextLoad) { if (result.format === 'commonjs') { const parsedUrl = new URL(result.responseURL ?? url) result.source ??= await readFile(parsedUrl) + /* c8 ignore next - mysteriously uncovered closing brace? */ + } + + return loadResult(url, result) +} + +export function loadSync(url, context, nextLoad) { + const result = nextLoad(url, context) + + if (transformers.has(url) === false) { + return result } + if (result.format === 'commonjs') { + const parsedUrl = new URL(result.responseURL ?? url) + result.source ??= readFileSync(parsedUrl) + } + + return loadResult(url, result) +} + +export function loadResult(url, result) { const code = result.source if (code) { const transformer = transformers.get(url) @@ -58,4 +88,3 @@ export async function load(url, context, nextLoad) { return result } - diff --git a/index.js b/index.js index 2e994d3..2a83672 100644 --- a/index.js +++ b/index.js @@ -60,6 +60,7 @@ function dump(code, filename) { const path = require('node:path') const fs = require('node:fs') + /* c8 ignore next */ const base = process.env.TRACING_DUMP_DIR ?? os.tmpdir() const dirname = path.dirname(filename) const basename = path.basename(filename) diff --git a/package.json b/package.json index 79c41e1..f580ef7 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "files": [ "index.js", "hook.mjs", + "hook-sync.mjs", "lib" ], "dependencies": { diff --git a/test/hook-sync.test.mjs b/test/hook-sync.test.mjs new file mode 100644 index 0000000..c856cff --- /dev/null +++ b/test/hook-sync.test.mjs @@ -0,0 +1,203 @@ +'use strict' +import test from 'node:test' +import assert from 'node:assert' +import path from 'node:path' +import { readFileSync } from 'node:fs' +import Snap from '@matteo.collina/snap' + +test.beforeEach(async (t) => { + const syncLoaderRewriter = await import('../hook-sync.mjs') + syncLoaderRewriter.initialize({ + instrumentations: [ + { + channelName: 'unitTestEsm', + module: { name: 'esm-pkg', versionRange: '>=1', filePath: 'foo.js' }, + functionQuery: { + className: 'Foo', + methodName: 'doStuff', + kind: 'Async' + } + }, + { + channelName: 'unitTestCjs', + module: { name: 'pkg-1', versionRange: '>=1', filePath: 'foo.js' }, + functionQuery: { + className: 'Foo', + methodName: 'doStuff', + kind: 'Async' + } + } + ] + }) + + const snap = Snap(`${import.meta.url}/${t.name}`) + + t.ctx = { + syncLoaderRewriter, + snap + } +}) + + +test('should rewrite code if it matches a subscriber and esm module', async (t) => { + const { syncLoaderRewriter, snap } = t.ctx + const esmPath = path.join(import.meta.dirname, './example-deps/lib/node_modules/esm-pkg/foo.js') + function resolveFn() { + return { url: `file://${esmPath}` } + } + function nextLoad() { + const data = readFileSync(esmPath, 'utf8') + return { + format: 'module', + source: data + } + } + const url = syncLoaderRewriter.resolve('esm-pkg', {}, resolveFn) + const result = syncLoaderRewriter.load(url.url, {}, nextLoad) + assert.equal(result.format, 'module') + assert.equal(result.shortCircuit, true) + const snapshot = await snap(result.source) + assert.deepEqual(result.source, snapshot) +}) + +test('should not rewrite code if it does not match a subscriber and a esm module', async (t) => { + const { syncLoaderRewriter, snap } = t.ctx + const esmPath = path.join(import.meta.dirname, './example-deps/lib/node_modules/esm-pkg-2/index.js') + function resolveFn() { + return { url: `file://${esmPath}` } + } + function nextLoad() { + const data = readFileSync(esmPath, 'utf8') + return { + format: 'module', + source: data + } + } + const url = syncLoaderRewriter.resolve('esm-pkg-2', {}, resolveFn) + const result = syncLoaderRewriter.load(url.url, {}, nextLoad) + assert.equal(result.format, 'module') + assert.ok(!result.shortCircuit) + const snapshot = await snap(result.source) + assert.deepEqual(result.source, snapshot) +}) + +test('should rewrite code if it matches a subscriber and a cjs module', async (t) => { + const { syncLoaderRewriter, snap } = t.ctx + const cjsPath = path.join(import.meta.dirname, './example-deps/lib/node_modules/pkg-1/foo.js') + function resolveFn() { + return { url: `file://${cjsPath}` } + } + function nextLoad(url, context) { + const data = readFileSync(cjsPath, 'utf8') + return { + format: 'commonjs', + source: data + } + } + + const url = syncLoaderRewriter.resolve('pkg-1', {}, resolveFn) + const result = syncLoaderRewriter.load(url.url, {}, nextLoad) + assert.equal(result.format, 'commonjs') + assert.equal(result.shortCircuit, true) + const snapshot = await snap(result.source) + assert.deepEqual(result.source, snapshot) +}) + +test('should rewrite code if it matches a subscriber and a cjs module(responseUrl)', async (t) => { + const { syncLoaderRewriter, snap } = t.ctx + const cjsPath = path.join(import.meta.dirname, './example-deps/lib/node_modules/pkg-1/foo.js') + function resolveFn() { + return { url: `file://${cjsPath}` } + } + function nextLoad(url) { + const data = readFileSync(cjsPath, 'utf8') + return { + repsonseURL: url, + format: 'commonjs', + source: data + } + } + const url = syncLoaderRewriter.resolve('pkg-1', {}, resolveFn) + const result = syncLoaderRewriter.load(url.url, {}, nextLoad) + assert.equal(result.format, 'commonjs') + assert.equal(result.shortCircuit, true) + const snapshot = await snap(result.source) + assert.deepEqual(result.source, snapshot) +}) + +test('should not rewrite code if it does not match a subscriber and a cjs module', async (t) => { + const { syncLoaderRewriter, snap } = t.ctx + const cjsPath = path.join(import.meta.dirname, './example-deps/lib/node_modules/pkg-2/index.js') + function resolveFn() { + return { url: `file://${cjsPath}` } + } + + function nextLoad() { + const data = readFileSync(cjsPath, 'utf8') + return { + format: 'commonjs', + source: data + } + } + + const url = syncLoaderRewriter.resolve('pkg-2', {}, resolveFn) + const result = syncLoaderRewriter.load(url.url, {}, nextLoad) + assert.equal(result.format, 'commonjs') + assert.ok(!result.shortCircuit) + const snapshot = await snap(result.source) + assert.deepEqual(result.source, snapshot) +}) + +test('should not rewrite code if a function query does not exist in file', async (t) => { + const { syncLoaderRewriter, snap } = t.ctx + syncLoaderRewriter.initialize({ + instrumentations: [ + { + channelName: 'unitTestEsm', + module: { name: 'esm-pkg', versionRange: '>=1', filePath: 'foo.js' }, + functionQuery: { + className: 'Blah', + } + } + ] + }) + const esmPath = path.join(import.meta.dirname, './example-deps/lib/node_modules/esm-pkg/foo.js') + function resolveFn() { + return { url: `file://${esmPath}` } + } + function nextLoad() { + const data = readFileSync(esmPath, 'utf8') + return { + format: 'module', + source: data + } + } + const url = syncLoaderRewriter.resolve('esm-pkg', {}, resolveFn) + const result = syncLoaderRewriter.load(url.url, {}, nextLoad) + assert.equal(result.format, 'module') + assert.ok(!result.shortCircuit) + const snapshot = await snap(result.source) + assert.deepEqual(result.source, snapshot) +}) + +test('should default initialization to not crash if not defined', async (t) => { + const { syncLoaderRewriter, snap } = t.ctx + syncLoaderRewriter.initialize() + const esmPath = path.join(import.meta.dirname, './example-deps/lib/node_modules/esm-pkg/foo.js') + function resolveFn() { + return { url: `file://${esmPath}` } + } + function nextLoad() { + const data = readFileSync(esmPath, 'utf8') + return { + format: 'module', + source: data + } + } + const url = syncLoaderRewriter.resolve('esm-pkg', {}, resolveFn) + const result = syncLoaderRewriter.load(url.url, {}, nextLoad) + assert.equal(result.format, 'module') + assert.ok(!result.shortCircuit) + const snapshot = await snap(result.source) + assert.deepEqual(result.source, snapshot) +}) diff --git a/test/index.test.js b/test/index.test.js index 116ace7..68864c3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -6,7 +6,7 @@ const Module = require('node:module') const Snap = require('@matteo.collina/snap') const ModulePatch = require('../index.js') const path = require('node:path') -const { readFileSync } = require('node:fs') +const { readFileSync, mkdirSync, rmSync, statSync } = require('node:fs') test.beforeEach((t) => { const subscribers = { @@ -45,15 +45,26 @@ test('should init ModulePatch', (t) => { }) test('should rewrite code for a match transformer', async (t) => { - const { modulePath, modulePatch, snap } = t.ctx - modulePatch.patch() - const resolvedPath = Module._resolveFilename(modulePath, null, false) - const data = readFileSync(resolvedPath, 'utf8') - const testModule = new Module(resolvedPath) - testModule._compile(data, resolvedPath) - const rewrittenCode = testModule.exports.toString() - const snapshot = await snap(rewrittenCode) - assert.deepEqual(rewrittenCode, snapshot) + // cover the trace debugging dump code path + const tracingDir = __dirname + '/trace' + process.env.TRACING_DUMP = '1' + process.env.TRACING_DUMP_DIR = tracingDir + try { + mkdirSync(__dirname + '/trace', { recursive: true }) + const { modulePath, modulePatch, snap } = t.ctx + modulePatch.patch() + const resolvedPath = Module._resolveFilename(modulePath, null, false) + const data = readFileSync(resolvedPath, 'utf8') + const testModule = new Module(resolvedPath) + testModule._compile(data, resolvedPath) + const rewrittenCode = testModule.exports.toString() + const snapshot = await snap(rewrittenCode) + assert.deepEqual(rewrittenCode, snapshot) + const expectedDump = path.join(tracingDir, modulePath) + assert.equal(statSync(expectedDump).isFile(), true) + } finally { + rmSync(tracingDir, { recursive: true, force: true }) + } }) test('should not rewrite code for an unmatch patch', async (t) => {