From bf3a4fbb0b0a4fd806aefb8d2a598894303a0ebf Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 May 2024 19:58:41 +0200 Subject: [PATCH] fix: Resolve re-exports of external modules (#78) Fixes (potentially) #59, #62, #63 `parentResolve` is cached and later used when attempting to load bare specifiers. --- hook.js | 42 ++++++++++++++++++- .../some-external-module/index.mjs | 1 + .../some-external-module/package.json | 8 ++++ .../node_modules/some-external-module/sub.mjs | 1 + test/fixtures/re-export-star-external.mjs | 1 + .../sub-directory/re-export-star-external.mjs | 1 + test/hook/re-export-star-module.mjs | 17 ++++++++ 7 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/node_modules/some-external-module/index.mjs create mode 100644 test/fixtures/node_modules/some-external-module/package.json create mode 100644 test/fixtures/node_modules/some-external-module/sub.mjs create mode 100644 test/fixtures/re-export-star-external.mjs create mode 100644 test/fixtures/sub-directory/re-export-star-external.mjs create mode 100644 test/hook/re-export-star-module.mjs diff --git a/hook.js b/hook.js index 5bea2b9..9da900a 100644 --- a/hook.js +++ b/hook.js @@ -3,6 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. const { randomBytes } = require('crypto') +const { URL } = require('url') const specifiers = new Map() const isWin = process.platform === 'win32' @@ -91,6 +92,30 @@ function isStarExportLine (line) { return /^\* from /.test(line) } +function isBareSpecifier (specifier) { + // Relative and absolute paths are not bare specifiers. + if ( + specifier.startsWith('.') || + specifier.startsWith('/')) { + return false + } + + // Valid URLs are not bare specifiers. (file:, http:, node:, etc.) + + // eslint-disable-next-line no-prototype-builtins + if (URL.hasOwnProperty('canParse')) { + return !URL.canParse(specifier) + } + + try { + // eslint-disable-next-line no-new + new URL(specifier) + return false + } catch (err) { + return true + } +} + /** * @typedef {object} ProcessedModule * @property {string[]} imports A set of ESM import lines to be added to the @@ -128,6 +153,7 @@ async function processModule ({ srcUrl, context, parentGetSource, + parentResolve, ns = 'namespace', defaultAs = 'default' }) { @@ -154,13 +180,22 @@ async function processModule ({ if (isStarExportLine(n) === true) { const [, modFile] = n.split('* from ') const normalizedModName = normalizeModName(modFile) - const modUrl = new URL(modFile, srcUrl).toString() const modName = Buffer.from(modFile, 'hex') + Date.now() + randomBytes(4).toString('hex') + let modUrl + if (isBareSpecifier(modFile)) { + // Bare specifiers need to be resolved relative to the parent module. + const result = await parentResolve(modFile, { parentURL: srcUrl }) + modUrl = result.url + } else { + modUrl = new URL(modFile, srcUrl).href + } + const data = await processModule({ srcUrl: modUrl, context, parentGetSource, + parentResolve, ns: `$${modName}`, defaultAs: normalizedModName }) @@ -229,7 +264,9 @@ function addIitm (url) { } function createHook (meta) { + let cachedResolve async function resolve (specifier, context, parentResolve) { + cachedResolve = parentResolve const { parentURL = '' } = context const newSpecifier = deleteIitm(specifier) if (isWin && parentURL.indexOf('file:node') === 0) { @@ -278,7 +315,8 @@ function createHook (meta) { const { imports, namespaces, setters: mapSetters } = await processModule({ srcUrl: realUrl, context, - parentGetSource + parentGetSource, + parentResolve: cachedResolve }) const setters = Array.from(mapSetters.values()) diff --git a/test/fixtures/node_modules/some-external-module/index.mjs b/test/fixtures/node_modules/some-external-module/index.mjs new file mode 100644 index 0000000..21ec276 --- /dev/null +++ b/test/fixtures/node_modules/some-external-module/index.mjs @@ -0,0 +1 @@ +export const foo = 'bar' diff --git a/test/fixtures/node_modules/some-external-module/package.json b/test/fixtures/node_modules/some-external-module/package.json new file mode 100644 index 0000000..2f24602 --- /dev/null +++ b/test/fixtures/node_modules/some-external-module/package.json @@ -0,0 +1,8 @@ +{ + "name": "some-external-module", + "type": "module", + "exports": { + ".": "./index.mjs", + "./sub": "./sub.mjs" + } +} diff --git a/test/fixtures/node_modules/some-external-module/sub.mjs b/test/fixtures/node_modules/some-external-module/sub.mjs new file mode 100644 index 0000000..7e9b246 --- /dev/null +++ b/test/fixtures/node_modules/some-external-module/sub.mjs @@ -0,0 +1 @@ +export const bar = 'baz' diff --git a/test/fixtures/re-export-star-external.mjs b/test/fixtures/re-export-star-external.mjs new file mode 100644 index 0000000..5d7c2ea --- /dev/null +++ b/test/fixtures/re-export-star-external.mjs @@ -0,0 +1 @@ +export * from 'some-external-module' diff --git a/test/fixtures/sub-directory/re-export-star-external.mjs b/test/fixtures/sub-directory/re-export-star-external.mjs new file mode 100644 index 0000000..7c7d747 --- /dev/null +++ b/test/fixtures/sub-directory/re-export-star-external.mjs @@ -0,0 +1 @@ +export * from 'some-external-module/sub' diff --git a/test/hook/re-export-star-module.mjs b/test/hook/re-export-star-module.mjs new file mode 100644 index 0000000..b796e28 --- /dev/null +++ b/test/hook/re-export-star-module.mjs @@ -0,0 +1,17 @@ +import Hook from '../../index.js' +import { foo } from '../fixtures/re-export-star-external.mjs' +import { bar } from '../fixtures/sub-directory/re-export-star-external.mjs' +import { strictEqual } from 'assert' + +Hook((exports, name) => { + if (name.endsWith('fixtures/re-export-star-external.mjs')) { + exports.foo = '1' + } + + if (name.endsWith('fixtures/sub-directory/re-export-star-external.mjs')) { + exports.bar = '2' + } +}) + +strictEqual(foo, '1') +strictEqual(bar, '2')