diff --git a/src/esmock.js b/src/esmock.js index 3c359dc6..bba7405d 100644 --- a/src/esmock.js +++ b/src/esmock.js @@ -17,8 +17,8 @@ const esmockGo = opts => async (...args) => { } const purge = mockModule => mockModule - && /object|function/.test(typeof mockModule) && 'esmockKey' in mockModule - && esmockModule.purge(mockModule.esmockKey) + && /object|function/.test(typeof mockModule) && 'esmkTreeId' in mockModule + && esmockModule.purge(mockModule.esmkTreeId) const strict = Object.assign(esmockGo({ strict: true }), { purge, p: esmockGo({ strict: true, purge: false }) }) diff --git a/src/esmockCache.js b/src/esmockCache.js index 386702b0..05342334 100644 --- a/src/esmockCache.js +++ b/src/esmockCache.js @@ -6,10 +6,10 @@ const esmockCache = { mockDefs: {} } -const esmockKeySet = (key, keylong) => ( +const esmockTreeIdSet = (key, keylong) => ( global.mockKeys[String(key)] = keylong) -const esmockKeyGet = key => ( +const esmockTreeIdGet = key => ( global.mockKeys[String(key)]) const esmockCacheSet = (key, mockDef) => ( @@ -26,7 +26,7 @@ const esmockCacheResolvedPathIsESMSet = (mockPathFull, isesm) => ( Object.assign(global, { esmockCacheGet, - esmockKeyGet, + esmockTreeIdGet, mockKeys: {} }) @@ -34,8 +34,8 @@ export { esmockCache as default, esmockCacheSet, esmockCacheGet, - esmockKeySet, - esmockKeyGet, + esmockTreeIdSet, + esmockTreeIdGet, esmockCacheResolvedPathIsESMGet, esmockCacheResolvedPathIsESMSet } diff --git a/src/esmockLoader.js b/src/esmockLoader.js index 15740a37..0b8518d2 100644 --- a/src/esmockLoader.js +++ b/src/esmockLoader.js @@ -4,14 +4,16 @@ import urlDummy from './esmockDummy.js' const [major, minor] = process.versions.node.split('.').map(it => +it) const isLT1612 = major < 16 || (major === 16 && minor < 12) -const esmockGlobalsAndAfterRe = /\?esmockGlobals=.*/ -const esmockGlobalsAndBeforeRe = /.*\?esmockGlobals=/ -const esmockModuleKeysRe = /#-#esmockModuleKeys/ +const esmkgdefsAndAfterRe = /\?esmkgdefs=.*/ +const esmkgdefsAndBeforeRe = /.*\?esmkgdefs=/ +const esmkdefsRe = /#-#esmkdefs/ +const esmkTreeIdRe = /esmkTreeId=\d*/ +const esmkModuleIdRe = /esmkModuleId=([^&]*)/ +const esmkIdRe = /\?esmk=\d*/ const exportNamesRe = /.*exportNames=(.*)/ -const esmockKeyRe = /esmockKey=\d*/ const withHashRe = /.*#-#/ const isesmRe = /isesm=true/ -const notfoundRe = /notfound=([^&]*)/ +const isnotfoundRe = /isfound=false/ // new versions of node: when multiple loaders are used and context // is passed to nextResolve, the process crashes in a recursive call @@ -30,49 +32,44 @@ const nextResolveCall = async (nextResolve, specifier, context) => ( const resolve = async (specifier, context, nextResolve) => { const { parentURL } = context - const [esmockKeyParamSmall] = - (parentURL && parentURL.match(/\?esmk=\d*/)) || [] - const esmockKeyLong = esmockKeyParamSmall - ? global.esmockKeyGet(esmockKeyParamSmall.split('=')[1]) + const treeidspec = esmkIdRe.test(parentURL) + ? global.esmockTreeIdGet(parentURL.match(esmkIdRe)[0].split('=')[1]) : parentURL - if (!esmockKeyRe.test(esmockKeyLong)) + if (!esmkTreeIdRe.test(treeidspec)) return nextResolveCall(nextResolve, specifier, context) - const [esmockKeyParam] = String(esmockKeyLong).match(esmockKeyRe) - const [keyUrl, keys] = esmockKeyLong.split(esmockModuleKeysRe) - const moduleGlobals = keyUrl && keyUrl.replace(esmockGlobalsAndBeforeRe, '') + const [treeid] = String(treeidspec).match(esmkTreeIdRe) + const [url, defs] = treeidspec.split(esmkdefsRe) + const gdefs = url && url.replace(esmkgdefsAndBeforeRe, '') // do not call 'nextResolve' for notfound modules - if (esmockKeyLong.includes(`notfound=${specifier}`)) { - const moduleKeyRe = new RegExp( - '.*file:///' + specifier + '(\\?' + esmockKeyParam + '(?:(?!#-#).)*).*') - const moduleKey = ( - moduleGlobals.match(moduleKeyRe) || keys.match(moduleKeyRe) || [])[1] - if (moduleKey) { + if (treeidspec.includes(`esmkModuleId=${specifier}&isfound=false`)) { + const moduleIdRe = new RegExp( + '.*file:///' + specifier + '(\\?' + treeid + '(?:(?!#-#).)*).*') + const moduleId = ( + gdefs.match(moduleIdRe) || defs.match(moduleIdRe) || [])[1] + if (moduleId) { return { shortCircuit: true, - url: urlDummy + moduleKey + url: urlDummy + moduleId } } } const resolved = await nextResolveCall(nextResolve, specifier, context) const resolvedurl = decodeURI(resolved.url) - const moduleKeyRe = new RegExp( - '.*(' + resolvedurl + '\\?' + esmockKeyParam + '(?:(?!#-#).)*).*') - const moduleKeyChild = moduleKeyRe.test(keys) - && keys.replace(moduleKeyRe, '$1') - const moduleKeyGlobal = moduleKeyRe.test(moduleGlobals) - && moduleGlobals.replace(moduleKeyRe, '$1') - - const moduleKey = moduleKeyChild || moduleKeyGlobal - if (moduleKey) { - resolved.url = isesmRe.test(moduleKey) - ? moduleKey - : urlDummy + '#-#' + moduleKey - } else if (moduleGlobals && moduleGlobals !== '0') { + const moduleIdRe = new RegExp( + '.*(' + resolvedurl + '\\?' + treeid + '(?:(?!#-#).)*).*') + const moduleId = + moduleIdRe.test(defs) && defs.replace(moduleIdRe, '$1') || + moduleIdRe.test(gdefs) && gdefs.replace(moduleIdRe, '$1') + if (moduleId) { + resolved.url = isesmRe.test(moduleId) + ? moduleId + : urlDummy + '#-#' + moduleId + } else if (gdefs && gdefs !== '0') { if (!resolved.url.startsWith('node:')) { - resolved.url += '?esmockGlobals=' + moduleGlobals + resolved.url += '?esmkgdefs=' + gdefs } } @@ -80,14 +77,14 @@ const resolve = async (specifier, context, nextResolve) => { } const load = async (url, context, nextLoad) => { - if (esmockModuleKeysRe.test(url)) // parent of mocked modules + if (esmkdefsRe.test(url)) // parent of mocked modules return nextLoad(url, context) - url = url.replace(esmockGlobalsAndAfterRe, '') + url = url.replace(esmkgdefsAndAfterRe, '') if (url.startsWith(urlDummy)) { url = url.replace(withHashRe, '') - if (notfoundRe.test(url)) - url = url.replace(urlDummy, `file:///${url.match(notfoundRe)[1]}`) + if (isnotfoundRe.test(url)) + url = url.replace(urlDummy, `file:///${url.match(esmkModuleIdRe)[1]}`) } const exportedNames = exportNamesRe.test(url) && diff --git a/src/esmockModule.js b/src/esmockModule.js index 84d6a12a..bb74babd 100644 --- a/src/esmockModule.js +++ b/src/esmockModule.js @@ -2,8 +2,8 @@ import fs from 'fs' import resolvewith from 'resolvewithplus' import { - esmockKeySet, - esmockKeyGet, + esmockTreeIdSet, + esmockTreeIdGet, esmockCacheSet, esmockCacheResolvedPathIsESMGet, esmockCacheResolvedPathIsESMSet @@ -12,24 +12,22 @@ import { const isObj = o => typeof o === 'object' && o const isDefaultIn = o => isObj(o) && 'default' in o const isDirPathRe = /^\.?\.?([a-zA-Z]:)?(\/|\\)/ -const esmockNextKey = ((key = 0) => () => ++key)() +const esmockNextId = ((id = 0) => () => ++id)() const esmockModuleIdNotFoundError = (moduleId, parent) => new Error( `invalid moduleId: "${moduleId}" (used by ${parent})` .replace(process.cwd(), '.') .replace(process.env.HOME, '~')) -const esmockModuleMergeDefault = (defLive, defMock) => - (isObj(defLive) && isObj(defMock)) - ? Object.assign({}, defLive, defMock) - : defMock +const esmockModuleMergeDefault = (defLive, def) => + (isObj(defLive) && isObj(def)) ? Object.assign({}, defLive, def) : def -const esmockModuleApply = (defLive, defMock, fileURL) => { - const def = Object.assign({}, defLive || {}, { +const esmockModuleApply = (defLive, def, fileURL) => { + def = Object.assign({}, defLive || {}, { default: esmockModuleMergeDefault( isDefaultIn(defLive) && defLive.default, - isDefaultIn(defMock) ? defMock.default : defMock) - }, defMock) + isDefaultIn(def) ? def.default : def) + }, def) // if safe, an extra "default.default" is added for compatibility with // babel-generated dist cjs files which also define "default.default" @@ -60,7 +58,7 @@ const esmockModuleIsESM = (fileURL, isesm) => { // return the default value directly, so that the esmock caller // does not need to lookup default as in "esmockedValue.default" -const esmockModuleImportedSanitize = (imported, esmockKey) => { +const esmockModuleImportedSanitize = (imported, esmkTreeId) => { const importedDefault = isDefaultIn(imported) && imported.default if (/boolean|string|number/.test(typeof importedDefault)) @@ -68,30 +66,29 @@ const esmockModuleImportedSanitize = (imported, esmockKey) => { // ex, non-extensible "[object Module]": import * as fs from 'fs'; export fs; return Object.isExtensible(importedDefault) - ? Object.assign(importedDefault, imported, { esmockKey }) - : Object.assign({}, importedDefault, imported, { esmockKey }) + ? Object.assign(importedDefault, imported, { esmkTreeId }) + : Object.assign({}, importedDefault, imported, { esmkTreeId }) } -const esmockModuleImportedPurge = modulePathKey => { +const esmockModuleImportedPurge = treeid => { const purgeKey = key => key === 'null' || esmockCacheSet(key, null) - const longKey = esmockKeyGet(modulePathKey.split('esmk=')[1]) - const [url, keys] = longKey.split('#-#esmockModuleKeys=') + const longKey = esmockTreeIdGet(treeid.split('esmk=')[1]) + const [url, keys] = longKey.split('#-#esmkdefs=') String(keys).split('#-#').forEach(purgeKey) - String(url.split('esmockGlobals=')[1]).split('#-#').forEach(purgeKey) + String(url.split('esmkgdefs=')[1]).split('#-#').forEach(purgeKey) } -const esmockModuleCreate = async (esmockKey, key, fileURL, defMock, opt) => { - const isesm = esmockModuleIsESM(fileURL) - const def = esmockModuleApply( - opt.strict || !fileURL || await import(fileURL), defMock, fileURL) - const mockExportNames = Object.keys(def).sort().join() - const mockModuleKey = (fileURL || 'file:///' + key) + '?' + [ - 'esmockKey=' + esmockKey, - 'esmockModuleKey=' + key, - 'isesm=' + isesm, - fileURL ? 'found' : 'notfound=' + key, - mockExportNames ? 'exportNames=' + mockExportNames : 'exportNone' +const esmockModuleCreate = async (treeid, def, id, fileURL, opt) => { + def = esmockModuleApply( + opt.strict || !fileURL || await import(fileURL), def, fileURL) + + const mockModuleKey = (fileURL || 'file:///' + id) + '?' + [ + 'esmkTreeId=' + treeid, + 'esmkModuleId=' + id, + 'isfound=' + Boolean(fileURL), + 'isesm=' + esmockModuleIsESM(fileURL), + 'exportNames=' + Object.keys(def).sort().join() ].join('&') esmockCacheSet(mockModuleKey, def) @@ -99,20 +96,20 @@ const esmockModuleCreate = async (esmockKey, key, fileURL, defMock, opt) => { return mockModuleKey } -const esmockModuleId = async (parent, key, defs, ids, opt, mocks, id) => { +const esmockModuleId = async (parent, treeid, defs, ids, opt, mocks, id) => { ids = ids || Object.keys(defs) id = ids[0] mocks = mocks || [] if (!id) return mocks - const mockedPathFull = resolvewith(id, parent) - if (!mockedPathFull && opt.isModuleNotFoundError !== false) + const fileURL = resolvewith(id, parent) + if (!fileURL && opt.isModuleNotFoundError !== false) throw esmockModuleIdNotFoundError(id, parent) - mocks.push(await esmockModuleCreate(key, id, mockedPathFull, defs[id], opt)) + mocks.push(await esmockModuleCreate(treeid, defs[id], id, fileURL, opt)) - return esmockModuleId(parent, key, defs, ids.slice(1), opt, mocks) + return esmockModuleId(parent, treeid, defs, ids.slice(1), opt, mocks) } const esmockModule = async (moduleId, parent, defs, gdefs, opt) => { @@ -120,18 +117,17 @@ const esmockModule = async (moduleId, parent, defs, gdefs, opt) => { if (!moduleFileURL) throw esmockModuleIdNotFoundError(moduleId, parent) - const esmockKey = typeof opt.key === 'number' ? opt.key : esmockNextKey() - const esmockKeyLong = moduleFileURL + '?' + - 'key=:esmockKey?esmockGlobals=:esmockGlobals#-#esmockModuleKeys=:moduleKeys' - .replace(/:esmockKey/, esmockKey) - .replace(/:esmockGlobals/, gdefs && (await esmockModuleId( - parent, esmockKey, gdefs, Object.keys(gdefs), opt)).join('#-#') || 0) - .replace(/:moduleKeys/, defs && (await esmockModuleId( - parent, esmockKey, defs, Object.keys(defs), opt)).join('#-#') || 0) + const treeid = typeof opt.id === 'number' ? opt.id : esmockNextId() + const treeidspec = `${moduleFileURL}?key=${treeid}?` + [ + 'esmkgdefs=' + (gdefs && (await esmockModuleId( + parent, treeid, gdefs, Object.keys(gdefs), opt)).join('#-#') || 0), + 'esmkdefs=', (defs && (await esmockModuleId( + parent, treeid, defs, Object.keys(defs), opt)).join('#-#') || 0) + ].join('#-#') - esmockKeySet(String(esmockKey), esmockKeyLong) + esmockTreeIdSet(String(treeid), treeidspec) - return moduleFileURL + `?esmk=${esmockKey}` + return moduleFileURL + `?esmk=${treeid}` } export default Object.assign(esmockModule, { diff --git a/tests/tests-ava/spec/esmock.ava.spec.js b/tests/tests-ava/spec/esmock.ava.spec.js index 8cfc5cd4..5ae1cf64 100644 --- a/tests/tests-ava/spec/esmock.ava.spec.js +++ b/tests/tests-ava/spec/esmock.ava.spec.js @@ -128,12 +128,12 @@ test('should purge local and global mocks', async t => { : filepath } }, { - key: 999 + id: 999 }) const keys = Object .keys(esmockCache.mockDefs) - .filter(key => /esmockKey=999/.test(key)) + .filter(key => /esmkTreeId=999/.test(key)) t.truthy(keys.length) t.true(keys.every(key => esmockCache.mockDefs[key] === null)) @@ -309,8 +309,8 @@ test('mocks inline `async import("name")`', async t => { filePath: 'filePath' })) - const [, key] = writeJSConfigFile.esmockKey.match(/esmk=(\d*)/) - const keyRe = new RegExp(`esmockKey=${key}[^d]`) + const [, key] = writeJSConfigFile.esmkTreeId.match(/esmk=(\d*)/) + const keyRe = new RegExp(`esmkTreeId=${key}[^d]`) const moduleKeys = Object.keys(esmockCache.mockDefs) .filter(moduleKey => keyRe.test(moduleKey)) @@ -401,3 +401,4 @@ test('should not error when mocked file has space in path', async t => { t.is(main.wild, 'tamed') }) + diff --git a/tests/tests-node/esmock.node.test.js b/tests/tests-node/esmock.node.test.js index 93553775..92179d88 100644 --- a/tests/tests-node/esmock.node.test.js +++ b/tests/tests-node/esmock.node.test.js @@ -137,12 +137,12 @@ test('should purge local and global mocks', async () => { : filepath } }, { - key: 999 + id: 999 }) const keys = Object .keys(esmockCache.mockDefs) - .filter(key => /esmockKey=999/.test(key)) + .filter(key => /esmkTreeId=999/.test(key)) assert.ok(keys.length) assert.ok(keys.every(key => esmockCache.mockDefs[key] === null)) @@ -323,8 +323,8 @@ test('mocks inline `async import("name")`', async () => { filePath: 'filePath' })) - const [, key] = writeJSConfigFile.esmockKey.match(/esmk=(\d*)/) - const keyRe = new RegExp(`esmockKey=${key}[^d]`) + const [, key] = writeJSConfigFile.esmkTreeId.match(/esmk=(\d*)/) + const keyRe = new RegExp(`esmkTreeId=${key}[^d]`) const moduleKeys = Object.keys(esmockCache.mockDefs) .filter(moduleKey => keyRe.test(moduleKey)) diff --git a/tests/tests-uvu/esmock.uvu.spec.js b/tests/tests-uvu/esmock.uvu.spec.js index 63e392e7..a0f15952 100644 --- a/tests/tests-uvu/esmock.uvu.spec.js +++ b/tests/tests-uvu/esmock.uvu.spec.js @@ -97,12 +97,12 @@ test('should purge local and global mocks', async () => { : filepath } }, { - key: 999 + id: 999 }) const keys = Object .keys(esmockCache.mockDefs) - .filter(key => /esmockKey=999/.test(key)) + .filter(key => /esmkTreeId=999/.test(key)) assert.ok(keys.length) assert.ok(keys.every(key => esmockCache.mockDefs[key] === null)) @@ -278,8 +278,8 @@ test('mocks inline `async import("name")`', async () => { filePath: 'filePath' })) - const [, key] = writeJSConfigFile.esmockKey.match(/esmk=(\d*)/) - const keyRe = new RegExp(`esmockKey=${key}[^d]`) + const [, key] = writeJSConfigFile.esmkTreeId.match(/esmk=(\d*)/) + const keyRe = new RegExp(`esmkTreeId=${key}[^d]`) const moduleKeys = Object.keys(esmockCache.mockDefs) .filter(moduleKey => keyRe.test(moduleKey))