Skip to content

Commit

Permalink
Merge pull request #251 from iambumblehead/add-support-for-optional-r…
Browse files Browse the repository at this point in the history
…esolvers

add support for custom resolvers
  • Loading branch information
koshic authored Oct 13, 2023
2 parents dd97845 + 4dd23ae commit d3ca335
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 9 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# changelog

* 2.5.3 _Oct.12.2023_
* [update resolver](https://github.com/iambumblehead/esmock/pull/243) to latest version, slightly faster with fewer loops
* [update resolver](https://github.com/iambumblehead/esmock/pull/250) to latest version, slightly faster with fewer loops
* [add support for resolver](https://github.com/iambumblehead/esmock/pull/251) configuration option
* 2.5.2 _Oct.06.2023_
* [update resolver](https://github.com/iambumblehead/esmock/pull/243) to improve module resolution. See resolvewithplus tags [v2.0.6](https://github.com/iambumblehead/resolvewithplus/releases/tag/v2.0.6) and [v2.0.7.](https://github.com/iambumblehead/resolvewithplus/releases/tag/v2.0.7)
* **resolve "exports" before "main".** The [spec says:](https://nodejs.org/api/packages.html#package-entry-points) _the "exports" field takes precedence over "main" in supported versions of Node.js._ The updated resolver correctly returns "main" before "exports" (older resolver did not).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "esmock",
"type": "module",
"version": "2.5.2",
"version": "2.5.3",
"license": "ISC",
"readmeFilename": "README.md",
"description": "provides native ESM import and globals mocking for unit tests",
Expand Down
7 changes: 5 additions & 2 deletions src/esmock.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
type MockMap = { [specifier: string]: any }
type Resolver = (id: string, parent: string) => string | null

type Options = {
strict?: boolean | undefined,
purge?: boolean | undefined,
isModuleNotFoundError?: boolean | undefined
isModuleNotFoundError?: boolean | undefined,
resolver?: Resolver | undefined
}

type MockFunction = {
Expand Down Expand Up @@ -90,5 +92,6 @@ export {
strictest,
type MockFunction,
type MockMap,
type Options
type Options,
type Resolver
}
4 changes: 3 additions & 1 deletion src/esmockArgs.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import resolver from 'resolvewithplus'

// extracts path or fileurl from stack,
// ' at <anonymous> (/root/test.js:11:31)' -> /root/test.js
// ' at Object.handler (file:///D:/a/test.js:11:31)' -> file:///D:/a/test.js
Expand All @@ -17,7 +19,7 @@ export default (arg, optsextra) => {
(new Error).stack.split('\n')[3].replace(stackpathre, '$2'),
...arg.slice(1)
]
arg[4] = {...arg[4], ...optsextra}
arg[4] = { resolver, ...arg[4], ...optsextra}

return arg
}
14 changes: 10 additions & 4 deletions src/esmockModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const isDirPathRe = /^\.?\.?([a-zA-Z]:)?(\/|\\)/
const nextId = ((id = 0) => () => ++id)()
const objProto = Object.getPrototypeOf({})
const isPlainObj = o => Object.getPrototypeOf(o) === objProto
const iscoremodule = resolvewith.iscoremodule
const protocolNodeRe = /^node:/
const addprotocolnode = p => protocolNodeRe.test(p) ? p : `node:${p}`

// assigning the object to its own prototypal inheritor can error, eg
// 'Cannot assign to read only property \'F_OK\' of object \'#<Object>\''
Expand Down Expand Up @@ -46,7 +49,7 @@ const esmockModuleApply = (defLive, def, fileURL) => {

// if safe, an extra "default.default" is added for compatibility with
// babel-generated dist cjs files which also define "default.default"
if (!resolvewith.iscoremodule(fileURL) && Object.isExtensible(def.default))
if (!iscoremodule(fileURL) && Object.isExtensible(def.default))
def.default.default = def.default

return def
Expand All @@ -59,7 +62,7 @@ const esmockModuleIsESM = (fileURL, isesm) => {
if (typeof isesm === 'boolean')
return isesm

isesm = !resolvewith.iscoremodule(fileURL)
isesm = !iscoremodule(fileURL)
&& isDirPathRe.test(fileURL)
&& esmockIsESMRe.test(fs.readFileSync(fileURL, 'utf-8'))

Expand Down Expand Up @@ -108,14 +111,17 @@ const esmockModuleCreate = async (treeid, def, id, fileURL, opt) => {
return mockModuleKey
}

const esmockResolve = (id, parent, opt) => (
iscoremodule(id) ? addprotocolnode(id) : opt.resolver(id, parent))

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 fileURL = resolvewith(id, parent)
const fileURL = esmockResolve(id, parent, opt)
if (!fileURL && opt.isModuleNotFoundError !== false && id !== 'import')
throw esmockErr.errModuleIdNotFound(id, parent)

Expand All @@ -125,7 +131,7 @@ const esmockModuleId = async (parent, treeid, defs, ids, opt, mocks, id) => {
}

const esmockModule = async (moduleId, parent, defs, gdefs, opt) => {
const moduleFileURL = resolvewith(moduleId, parent)
const moduleFileURL = esmockResolve(moduleId, parent, opt)
if (!moduleFileURL)
throw esmockErr.errModuleIdNotFound(moduleId, parent)

Expand Down
7 changes: 7 additions & 0 deletions tests/local/customResolverChild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const isMocked = false
const isCustomResolverChild = true

export default {
isCustomResolverChild,
isMocked
}
9 changes: 9 additions & 0 deletions tests/local/customResolverParent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import child from 'RESOLVECUSTOM'
import path from 'node:path'

const pathbasenameresult = path.basename('/the/very/happy-dog.png')

export {
child,
pathbasenameresult
}
66 changes: 66 additions & 0 deletions tests/tests-node/esmock.node.resolver-custom.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import module from 'node:module'
import esmock from 'esmock'

function resolverCustom (moduleId, parent) {
parent = parent.replace(/\/tests\/.*$/, '/tests/tests-node/')

// This logic looks unusual because of constraints here. This function must:
// * must work at windows, where path.join and path.resolve cause issues
// * must be string-serializable, no external funcions
// * must resolve these moduleIds to corresponding, existing filepaths
// * '../local/customResolverParent.js',
// * 'RESOLVECUSTOM/
return (
/RESOLVECUSTOM$/.test(moduleId)
? parent + '../local/customResolverChild.js'
: parent + moduleId
).replace(/\/tests-node\/\.\./, '')
}

async function resolve (specifier, context, next) {
return next(
specifier === 'RESOLVECUSTOM'
? resolverCustom(specifier, context.parentURL)
: specifier, context)
}

module.register && module.register(`
data:text/javascript,
${encodeURIComponent(resolverCustom)}
export ${encodeURIComponent(resolve)}`.slice(1))

test('should use custom resolver', async () => {
if (!module.register)
return assert.ok('skip test')

const customResolverParent = await esmock(
'../local/customResolverParent.js', {}, {
RESOLVECUSTOM: ({ isMocked: true })
}, {
resolver: resolverCustom
})

assert.ok(customResolverParent.child.isCustomResolverChild)
assert.ok(customResolverParent.child.isMocked)
})

test('should not call custom resover with builtin moduleIds', async () => {
if (!module.register)
return assert.ok('skip test')

const customResolverParent = await esmock(
'../local/customResolverParent.js', {}, {
RESOLVECUSTOM: ({ isMocked: true }),
path: { basename: () => 'basenametest' }
}, {
resolver: resolverCustom
})

assert.ok(customResolverParent.child.isCustomResolverChild)
assert.ok(customResolverParent.child.isMocked)
assert.strictEqual(
customResolverParent.pathbasenameresult,
'basenametest')
})

0 comments on commit d3ca335

Please sign in to comment.