From 9cae1cb6e9e76a8f63112ca8534cc265eb02c60d Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 24 Aug 2022 18:12:21 -0700 Subject: [PATCH 01/12] mock modules that aren't found, test failing --- src/esmockModule.js | 8 +++++--- tests/local/notinstalledVueComponent.js | 3 +++ tests/tests-node/esmock.node.test.js | 13 +++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/local/notinstalledVueComponent.js diff --git a/src/esmockModule.js b/src/esmockModule.js index 543df576..9ddc8684 100644 --- a/src/esmockModule.js +++ b/src/esmockModule.js @@ -12,7 +12,7 @@ import { const isObj = o => typeof o === 'object' && o const isDefaultDefined = o => isObj(o) && 'default' in o - +const isDirPathRe = /^\.?\.?([a-zA-Z]:)?(\/|\\)/; const FILE_PROTOCOL = 'file:///' // https://url.spec.whatwg.org/, eg, file:///C:/demo file:///root/linux/path @@ -21,9 +21,9 @@ const pathAddProtocol = (pathFull, protocol) => { protocol = /^node:/.test(pathFull) ? '' : !resolvewith.iscoremodule(pathFull) ? FILE_PROTOCOL : 'node:' - if (protocol.includes(FILE_PROTOCOL)) + if (protocol.includes(FILE_PROTOCOL) && isDirPathRe.test(pathFull)) pathFull = fs.realpathSync.native(pathFull) - if (process.platform === 'win32') + if (process.platform === 'win32' && isDirPathRe.test(pathFull)) pathFull = pathFull.split(path.sep).join(path.posix.sep) return `${protocol}${pathFull.replace(/^\//, '')}` } @@ -77,6 +77,7 @@ const esmockModuleIsESM = (mockPathFull, isesm) => { return isesm isesm = !resolvewith.iscoremodule(mockPathFull) + && isDirPathRe.test(mockPathFull) && esmockModuleESMRe.test(fs.readFileSync(mockPathFull, 'utf-8')) esmockCacheResolvedPathIsESMSet(mockPathFull, isesm) @@ -139,6 +140,7 @@ const esmockModulesCreate = async (pathCallee, pathModule, esmockKey, defs, keys return mocks let mockedPathFull = resolvewith(keys[0], pathCallee) + || (opt.isErrorPackageNotFound === false && keys[0]) if (!mockedPathFull) { pathCallee = pathCallee .replace(/^\/\//, '') diff --git a/tests/local/notinstalledVueComponent.js b/tests/local/notinstalledVueComponent.js new file mode 100644 index 00000000..2aa8bc12 --- /dev/null +++ b/tests/local/notinstalledVueComponent.js @@ -0,0 +1,3 @@ +import {h} from 'vue' + +export default () => h('svg', { /* some properties */ }) diff --git a/tests/tests-node/esmock.node.test.js b/tests/tests-node/esmock.node.test.js index a945d625..c3077bc7 100644 --- a/tests/tests-node/esmock.node.test.js +++ b/tests/tests-node/esmock.node.test.js @@ -4,6 +4,18 @@ import assert from 'node:assert/strict' import esmock from '../../src/esmock.js' import sinon from 'sinon' +test('should mock package, even when package is not installed', async () => { + const component = await esmock(`../local/notinstalledVueComponent.js`, {}, { + vue: { + h: (...args) => args + } + }, { + isErrorPackageNotFound: false + }) + + assert.strictEqual(component()[0], 'svg') +}) +/* test('should mock a subpath', async () => { const localpackagepath = path.resolve('../local/') const { subpathfunctionWrap } = await esmock( @@ -409,3 +421,4 @@ test('should strict mock by default, partial mock optional', async () => { assert.deepEqual(pathWrapPartial.basename('/dog.png'), 'dog.png') }) +*/ From 4542452880fd1e8ef7a8d272012e7e3c27ffddf7 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 24 Aug 2022 18:41:58 -0700 Subject: [PATCH 02/12] added special conditions to loader in order to see passing test --- src/esmockLoader.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/esmockLoader.js b/src/esmockLoader.js index 9e343564..f0acb950 100644 --- a/src/esmockLoader.js +++ b/src/esmockLoader.js @@ -36,6 +36,12 @@ const resolve = async (specifier, context, nextResolve) => { const [ esmockKeyParam ] = (esmockKeyLong && esmockKeyLong.match(esmockKeyRe) || []) + if (esmockKeyLong && esmockKeyLong.includes('file:///vue')) { + return { + shortCircuit: true, + url: urlDummy + '#-#' + 'vue' + } + } // new versions of node: when multiple loaders are used and context // is passed to nextResolve, the process crashes in a recursive call // see: /esmock/issues/#48 @@ -87,6 +93,11 @@ const load = async (url, context, nextLoad) => { url = url.replace(withHashRe, '') } + if (url === 'vue') { + url = 'file:///vue?esmockKey=1&esmockModuleKey=vue' + + '&isesm=false&exportNames=default,h' + } + const exportedNames = exportNamesRe.test(url) && url.replace(exportNamesRe, '$1').split(',') if (exportedNames.length) { From 000cc5a282fc4206690dca84bdd4b456b76d720f Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 24 Aug 2022 18:43:38 -0700 Subject: [PATCH 03/12] remove semi --- src/esmockModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esmockModule.js b/src/esmockModule.js index 9ddc8684..62d3f4e2 100644 --- a/src/esmockModule.js +++ b/src/esmockModule.js @@ -12,7 +12,7 @@ import { const isObj = o => typeof o === 'object' && o const isDefaultDefined = o => isObj(o) && 'default' in o -const isDirPathRe = /^\.?\.?([a-zA-Z]:)?(\/|\\)/; +const isDirPathRe = /^\.?\.?([a-zA-Z]:)?(\/|\\)/ const FILE_PROTOCOL = 'file:///' // https://url.spec.whatwg.org/, eg, file:///C:/demo file:///root/linux/path From 845b8ebe03dfd84e37cacdf5f4ea584faf3e3ad3 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 24 Aug 2022 18:46:35 -0700 Subject: [PATCH 04/12] un-comment tests --- tests/tests-node/esmock.node.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/tests-node/esmock.node.test.js b/tests/tests-node/esmock.node.test.js index c3077bc7..189aa82d 100644 --- a/tests/tests-node/esmock.node.test.js +++ b/tests/tests-node/esmock.node.test.js @@ -15,7 +15,7 @@ test('should mock package, even when package is not installed', async () => { assert.strictEqual(component()[0], 'svg') }) -/* + test('should mock a subpath', async () => { const localpackagepath = path.resolve('../local/') const { subpathfunctionWrap } = await esmock( @@ -421,4 +421,3 @@ test('should strict mock by default, partial mock optional', async () => { assert.deepEqual(pathWrapPartial.basename('/dog.png'), 'dog.png') }) -*/ From bb87a77fc9d8cd160c6a65a52209be59a57abb79 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 24 Aug 2022 21:25:16 -0700 Subject: [PATCH 05/12] import dummy url: smaller and reusable import w/ other files --- src/esmockDummy.js | 3 +++ src/esmockLoader.js | 10 +--------- 2 files changed, 4 insertions(+), 9 deletions(-) create mode 100644 src/esmockDummy.js diff --git a/src/esmockDummy.js b/src/esmockDummy.js new file mode 100644 index 00000000..7b2d7c82 --- /dev/null +++ b/src/esmockDummy.js @@ -0,0 +1,3 @@ +// ex, file:///path/to/esmockDummy.js, +// file:///c:/path/to/esmockDummy.js +export default import.meta.url diff --git a/src/esmockLoader.js b/src/esmockLoader.js index f0acb950..9b57db53 100644 --- a/src/esmockLoader.js +++ b/src/esmockLoader.js @@ -1,20 +1,12 @@ import process from 'process' -import path from 'path' -import url from 'url' - import esmock from './esmock.js' import esmockIsLoader from './esmockIsLoader.js' +import urlDummy from './esmockDummy.js' global.esmockloader = esmockIsLoader export default esmock -// ex, file:///path/to/esmock, -// file:///c:/path/to/esmock -const urlDummy = 'file:///' + path - .join(path.dirname(url.fileURLToPath(import.meta.url)), 'esmock.js') - .replace(/^\//, '') - const [ major, minor ] = process.versions.node.split('.').map(it => +it) const isLT1612 = major < 16 || (major === 16 && minor < 12) From 578063e177289fb339bcc8fe6b0802e17de893a4 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 24 Aug 2022 23:39:14 -0700 Subject: [PATCH 06/12] do not call nextResolve for notfound specifiers --- src/esmockLoader.js | 66 +++++++++++++++------------- src/esmockModule.js | 8 ++-- tests/tests-node/esmock.node.test.js | 15 ++++++- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/esmockLoader.js b/src/esmockLoader.js index 9b57db53..cfae32fa 100644 --- a/src/esmockLoader.js +++ b/src/esmockLoader.js @@ -17,6 +17,22 @@ const exportNamesRe = /.*exportNames=(.*)/ const esmockKeyRe = /esmockKey=\d*/ const withHashRe = /.*#-#/ const isesmRe = /isesm=true/ +const notfoundRe = /notfound=([^&]*)/ + +// new versions of node: when multiple loaders are used and context +// is passed to nextResolve, the process crashes in a recursive call +// see: /esmock/issues/#48 +// +// old versions of node: if context.parentURL is defined, and context +// is not passed to nextResolve, the tests fail +// +// later versions of node v16 include 'node-addons' +const nextResolveCall = async (nextResolve, specifier, context) => ( + context.parentURL && + (context.conditions.slice(-1)[0] === 'node-addons' + || context.importAssertions || isLT1612) + ? await nextResolve(specifier, context) + : await nextResolve(specifier)) const resolve = async (specifier, context, nextResolve) => { const { parentURL } = context @@ -25,38 +41,31 @@ const resolve = async (specifier, context, nextResolve) => { const esmockKeyLong = esmockKeyParamSmall ? global.esmockKeyGet(esmockKeyParamSmall.split('=')[1]) : parentURL - const [ esmockKeyParam ] = - (esmockKeyLong && esmockKeyLong.match(esmockKeyRe) || []) - if (esmockKeyLong && esmockKeyLong.includes('file:///vue')) { - return { - shortCircuit: true, - url: urlDummy + '#-#' + 'vue' + if (!esmockKeyRe.test(esmockKeyLong)) + return nextResolveCall(nextResolve, specifier, context) + + const [ esmockKeyParam ] = esmockKeyLong.match(esmockKeyRe) + const [ keyUrl, keys ] = esmockKeyLong.split(esmockModuleKeysRe) + const moduleGlobals = keyUrl && keyUrl.replace(esmockGlobalsAndBeforeRe, '') + // do not call 'nextResolve' for notfound modules + if (esmockKeyLong.includes(`notfound=${specifier}`)) { + const moduleKeyRe = new RegExp( // eslint-disable-line prefer-destructuring + '.*file:///' + specifier + '(\\?' + esmockKeyParam + '(?:(?!#-#).)*).*') + const moduleKey = ( // eslint-disable-line prefer-destructuring + moduleGlobals.match(moduleKeyRe) || keys.match(moduleKeyRe) || [])[1] + if (moduleKey) { + return { + shortCircuit: true, + url: urlDummy + moduleKey + } } } - // new versions of node: when multiple loaders are used and context - // is passed to nextResolve, the process crashes in a recursive call - // see: /esmock/issues/#48 - // - // old versions of node: if context.parentURL is defined, and context - // is not passed to nextResolve, the tests fail - // - // later versions of node v16 include 'node-addons' - const resolved = context.parentURL && ( - context.conditions.slice(-1)[0] === 'node-addons' - || context.importAssertions || isLT1612) - ? await nextResolve(specifier, context) - : await nextResolve(specifier) - - if (!esmockKeyParam) - return resolved + const resolved = await nextResolveCall(nextResolve, specifier, context) const resolvedurl = decodeURI(resolved.url) const moduleKeyRe = new RegExp( '.*(' + resolvedurl + '\\?' + esmockKeyParam + '(?:(?!#-#).)*).*') - - const [ keyUrl, keys ] = esmockKeyLong.split(esmockModuleKeysRe) - const moduleGlobals = keyUrl.replace(esmockGlobalsAndBeforeRe, '') const moduleKeyChild = moduleKeyRe.test(keys) && keys.replace(moduleKeyRe, '$1') const moduleKeyGlobal = moduleKeyRe.test(moduleGlobals) @@ -83,11 +92,8 @@ const load = async (url, context, nextLoad) => { url = url.replace(esmockGlobalsAndAfterRe, '') if (url.startsWith(urlDummy)) { url = url.replace(withHashRe, '') - } - - if (url === 'vue') { - url = 'file:///vue?esmockKey=1&esmockModuleKey=vue' - + '&isesm=false&exportNames=default,h' + if (notfoundRe.test(url)) + url = url.replace(urlDummy, `file:///${(url.match(notfoundRe) || [])[1]}`) } const exportedNames = exportNamesRe.test(url) && diff --git a/src/esmockModule.js b/src/esmockModule.js index 62d3f4e2..641f080e 100644 --- a/src/esmockModule.js +++ b/src/esmockModule.js @@ -123,6 +123,7 @@ const esmockModuleCreate = async (esmockKey, key, mockPathFull, mockDef, opt) => 'esmockKey=' + esmockKey, 'esmockModuleKey=' + key, 'isesm=' + isesm, + opt.isfound ? 'found' : 'notfound=' + key, mockExportNames ? 'exportNames=' + mockExportNames : 'exportNone' ].join('&') @@ -140,8 +141,7 @@ const esmockModulesCreate = async (pathCallee, pathModule, esmockKey, defs, keys return mocks let mockedPathFull = resolvewith(keys[0], pathCallee) - || (opt.isErrorPackageNotFound === false && keys[0]) - if (!mockedPathFull) { + if (!mockedPathFull && opt.isPackageNotFoundError !== false) { pathCallee = pathCallee .replace(/^\/\//, '') .replace(process.cwd(), '.') @@ -155,9 +155,9 @@ const esmockModulesCreate = async (pathCallee, pathModule, esmockKey, defs, keys mocks.push(await esmockModuleCreate( esmockKey, keys[0], - mockedPathFull, + mockedPathFull || keys[0], defs[keys[0]], - opt + Object.assign({ isfound: Boolean(mockedPathFull) }, opt) )) return esmockModulesCreate( diff --git a/tests/tests-node/esmock.node.test.js b/tests/tests-node/esmock.node.test.js index 189aa82d..fb93075a 100644 --- a/tests/tests-node/esmock.node.test.js +++ b/tests/tests-node/esmock.node.test.js @@ -4,13 +4,25 @@ import assert from 'node:assert/strict' import esmock from '../../src/esmock.js' import sinon from 'sinon' +test('should mock package, even when package is not installed', async () => { + const component = await esmock(`../local/notinstalledVueComponent.js`, { + vue: { + h: (...args) => args + } + }, {}, { + isPackageNotFoundError: false + }) + + assert.strictEqual(component()[0], 'svg') +}) + test('should mock package, even when package is not installed', async () => { const component = await esmock(`../local/notinstalledVueComponent.js`, {}, { vue: { h: (...args) => args } }, { - isErrorPackageNotFound: false + isPackageNotFoundError: false }) assert.strictEqual(component()[0], 'svg') @@ -421,3 +433,4 @@ test('should strict mock by default, partial mock optional', async () => { assert.deepEqual(pathWrapPartial.basename('/dog.png'), 'dog.png') }) + From 4d257f571bdbd69319a97e77b46a3ee6d59ee88f Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 24 Aug 2022 23:52:31 -0700 Subject: [PATCH 07/12] try updating some lines to pass windows ci --- src/esmockLoader.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/esmockLoader.js b/src/esmockLoader.js index cfae32fa..8f7db722 100644 --- a/src/esmockLoader.js +++ b/src/esmockLoader.js @@ -42,10 +42,13 @@ const resolve = async (specifier, context, nextResolve) => { ? global.esmockKeyGet(esmockKeyParamSmall.split('=')[1]) : parentURL - if (!esmockKeyRe.test(esmockKeyLong)) + const [ esmockKeyParam ] = String(esmockKeyLong).match(esmockKeyRe) || [] + if (!esmockKeyParam) return nextResolveCall(nextResolve, specifier, context) + // if (!esmockKeyRe.test(esmockKeyLong)) + // return nextResolveCall(nextResolve, specifier, context) - const [ esmockKeyParam ] = esmockKeyLong.match(esmockKeyRe) + // const [ esmockKeyParam ] = esmockKeyLong.match(esmockKeyRe) const [ keyUrl, keys ] = esmockKeyLong.split(esmockModuleKeysRe) const moduleGlobals = keyUrl && keyUrl.replace(esmockGlobalsAndBeforeRe, '') // do not call 'nextResolve' for notfound modules From 618d244ee18dc22593a1c94ed77f5e4e2976976a Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 25 Aug 2022 00:45:08 -0700 Subject: [PATCH 08/12] check for mockedPathFull before updating windows path --- src/esmockModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esmockModule.js b/src/esmockModule.js index 641f080e..75f16aeb 100644 --- a/src/esmockModule.js +++ b/src/esmockModule.js @@ -149,7 +149,7 @@ const esmockModulesCreate = async (pathCallee, pathModule, esmockKey, defs, keys throw new Error(`not a valid path: "${keys[0]}" (used by ${pathCallee})`) } - if (process.platform === 'win32') + if (mockedPathFull && process.platform === 'win32') mockedPathFull = mockedPathFull.split(path.sep).join(path.posix.sep) mocks.push(await esmockModuleCreate( From fc00066038df880d980732dafbd55ce74fa6febf Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 25 Aug 2022 00:56:28 -0700 Subject: [PATCH 09/12] touch --- tests/tests-ava/spec/esmock.ava.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests-ava/spec/esmock.ava.spec.js b/tests/tests-ava/spec/esmock.ava.spec.js index 56cb2b9f..6ad7948a 100644 --- a/tests/tests-ava/spec/esmock.ava.spec.js +++ b/tests/tests-ava/spec/esmock.ava.spec.js @@ -2,7 +2,7 @@ import test from 'ava' import esmock from 'esmock' import sinon from 'sinon' -test('should not error when handling non-extnsible object', async t => { +test('should not error when handling non-extensible object', async t => { // if esmock tries to simulate babel and define default.default // runtime error may occur if non-extensible is defined there await esmock.px('../../local/importsNonDefaultClass.js', { From cced1e9bd32095e078fe75cfd8748aa196671668 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 25 Aug 2022 01:17:45 -0700 Subject: [PATCH 10/12] specify earlier version of node 18 --- .github/workflows/coverage.yml | 2 +- .github/workflows/node.js.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b8b843ad..d1ec6c99 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 5 strategy: matrix: - node-version: [18.x] + node-version: [18.6] os: [ubuntu-latest] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 33a55c9e..f742e5e8 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -15,7 +15,7 @@ jobs: timeout-minutes: 6 strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [14.x, 16.x, 18.6] os: [ubuntu-latest, windows-latest] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ From fffebc2997de12668ce992716a71f750d9fd5899 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 25 Aug 2022 01:24:10 -0700 Subject: [PATCH 11/12] remove commented out lines --- src/esmockLoader.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/esmockLoader.js b/src/esmockLoader.js index 8f7db722..64fa845c 100644 --- a/src/esmockLoader.js +++ b/src/esmockLoader.js @@ -42,13 +42,10 @@ const resolve = async (specifier, context, nextResolve) => { ? global.esmockKeyGet(esmockKeyParamSmall.split('=')[1]) : parentURL - const [ esmockKeyParam ] = String(esmockKeyLong).match(esmockKeyRe) || [] - if (!esmockKeyParam) + if (!esmockKeyRe.test(esmockKeyLong)) return nextResolveCall(nextResolve, specifier, context) - // if (!esmockKeyRe.test(esmockKeyLong)) - // return nextResolveCall(nextResolve, specifier, context) - // const [ esmockKeyParam ] = esmockKeyLong.match(esmockKeyRe) + const [ esmockKeyParam ] = String(esmockKeyLong).match(esmockKeyRe) const [ keyUrl, keys ] = esmockKeyLong.split(esmockModuleKeysRe) const moduleGlobals = keyUrl && keyUrl.replace(esmockGlobalsAndBeforeRe, '') // do not call 'nextResolve' for notfound modules From 9d9383d9d1be44e1a45c058c78e2cbc60f092f5b Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 25 Aug 2022 01:31:17 -0700 Subject: [PATCH 12/12] update changelog and increment package.json version --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d08af4e..86bb42c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # changelog + * 1.9.7 _Aug.25.2022_ + * support mocking specifiers that [aren't found in filesystem](https://github.com/iambumblehead/esmock/issues/126) * 1.9.6 _Aug.24.2022_ * support parent url to facilitate sourcemap usage, [113](https://github.com/iambumblehead/esmock/issues/113) * support import subpaths, eg `import: { '#sub': './path.js' }` diff --git a/package.json b/package.json index e8e6d3a2..8dc3ab99 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "esmock", "type": "module", - "version": "1.9.6", + "version": "1.9.7", "license": "ISC", "readmeFilename": "README.md", "description": "provides native ESM import mocking for unit tests",