Skip to content

Commit

Permalink
Merge pull request #240 from iambumblehead/add-support-for-initialize…
Browse files Browse the repository at this point in the history
…-hook

add support for initialize hook
  • Loading branch information
iambumblehead authored Sep 11, 2023
2 parents b38e18c + fccb5a7 commit 8cb00f1
Show file tree
Hide file tree
Showing 19 changed files with 137 additions and 68 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.4.2 _tbd_
* 2.5.0 _Sep.09.2023_
* [remove duplicate nextLoad call](https://github.com/iambumblehead/esmock/pull/239)
* [add support for initialize hook](https://github.com/iambumblehead/esmock/pull/240)
* 2.4.1 _Sep.07.2023_
* [detect null AND undefined](https://github.com/iambumblehead/esmock/pull/238) loader-resolved source defintions
* restore commented-out test affected by un-caught `undefined` source definitions
Expand Down
23 changes: 1 addition & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,7 @@

**esmock provides native ESM import and globals mocking for unit tests.** Use examples below as a quick-start guide, see the [descriptive and friendly esmock guide here,][4] or browse [esmock's test runner examples.][3]


`esmock` is used with node's --loader
``` json
{
"name": "give-esmock-a-star",
"type": "module",
"scripts": {
"test": "node --loader=esmock --test",
"test-mocha": "mocha --loader=esmock",
"test-tap": "NODE_OPTIONS=--loader=esmock tap",
"test-ava": "NODE_OPTIONS=--loader=esmock ava",
"test-uvu": "NODE_OPTIONS=--loader=esmock uvu spec",
"test-tsm": "node --loader=tsm --loader=esmock --test *ts",
"test-ts": "node --loader=ts-node/esm --loader=esmock --test *ts",
"test-jest": "NODE_OPTIONS=--loader=esmock jest",
"test-tsx": "⚠ https://github.com/esbuild-kit/tsx/issues/264"
},
"jest": {
"runner": "jest-light-runner"
}
}
```
_**Note: For versions of node prior to v20.6.0,** "--loader" command line arguments must be used with `esmock` as demonstrated [in the wiki.][4] Current versions of node do not require "--loader"._

`esmock` has the below signature
``` javascript
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "esmock",
"type": "module",
"version": "2.4.1",
"version": "2.5.0",
"license": "ISC",
"readmeFilename": "README.md",
"description": "provides native ESM import and globals mocking for unit tests",
Expand Down Expand Up @@ -79,7 +79,7 @@
"lint": "eslint --ext=.js,.mjs .",
"lint-fix": "eslint --ext=.js,.mjs --fix .",
"mini:pkg": "npm pkg delete scripts devDependencies dependencies",
"mini:src": "cd src && npx rimraf \"!(esmock).js\"",
"mini:src": "cd src && npx rimraf \"!(esmock|esmockLoader).js\"",
"mini": "npm run mini:src && npm run mini:pkg",
"prepublishOnly": "npm run lint && npm run test-ci && npm run mini"
}
Expand Down
14 changes: 12 additions & 2 deletions src/esmock.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import module from 'node:module'
import * as hooks from './esmockLoader.js'
import esmockLoader from './esmockLoader.js'
import esmockModule from './esmockModule.js'
import esmockArgs from './esmockArgs.js'
import esmockErr from './esmockErr.js'

const esmockGo = opts => async (...args) => {
const [moduleId, parent, defs, gdefs, opt] = esmockArgs(args, opts)
if (!await esmockLoader())
if (!module.register && !await esmockLoader())
throw esmockErr.errMissingLoader()

const fileURLKey = await esmockModule(moduleId, parent, defs, gdefs, opt)
Expand All @@ -31,4 +33,12 @@ const esmock = Object.assign(esmockGo(), {
purge, p: esmockGo({ purge: false }), strict, strictest })

export {esmock as default, strict, strictest}
export * from './esmockLoader.js'

// for older node versions, to support "--loader=esmock" rather than
// "--loader=esmock/loader", esmock.js exported loader hook definitions here
//
// for newer node versions 20.6+, exporting hook definitions here causes
// problems when --loader is used w/ module.register
const hooksFinal = module.register ? {} : hooks
const { load, resolve, getSource, initialize, globalPreload } = hooksFinal
export { load, resolve, getSource, initialize, globalPreload }
7 changes: 4 additions & 3 deletions src/esmockCache.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import esmockPostMessage from './esmockRegister.js'

const esmockCache = {
isESM: {},

Expand All @@ -7,8 +9,7 @@ const esmockCache = {
}

const esmockTreeIdSet = (key, keylong) => (
typeof global.postMessageEsmk === 'function'
&& global.postMessageEsmk({ key, keylong }),
esmockPostMessage({ key, keylong }),
global.mockKeys[String(key)] = keylong)

const esmockTreeIdGet = key => (
Expand All @@ -30,7 +31,7 @@ Object.assign(global, {
esmockCache,
esmockCacheGet,
esmockTreeIdGet,
mockKeys: {}
mockKeys: global.mockKeys || {}
})

export {
Expand Down
3 changes: 2 additions & 1 deletion src/esmockErr.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const errModuleIdNotMocked = (moduleId, parent) =>
new Error(`un-mocked moduleId: "${moduleId}" (used by ${parent})`)

const errMissingLoader = () =>
new Error('the loader chain process must include esmock. '
new Error('For versions of node prior to v20.6.0, '
+ 'the loader chain process must include esmock. '
+ 'start the process using --loader=esmock.')

const errModuleIdNoDefs = (moduleId, parent) =>
Expand Down
29 changes: 25 additions & 4 deletions src/esmockLoader.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'node:fs/promises'
import module from 'node:module'
import process from 'process'
import esmockErr from './esmockErr.js'

Expand Down Expand Up @@ -28,13 +29,26 @@ const hashbangRe = /^(#![^\n]*\n)/
const moduleIdReCreate = (moduleid, treeid) => new RegExp(
`.*(${moduleid}(\\?${treeid}(?:(?!#-#).)*)).*`)

const globalPreload = (({ port }) => (
// node v12.0-v18.x, global
const mockKeys = global.mockKeys = (global.mockKeys || {})

// node v20.0-v20.6
const globalPreload = !module.register && (({ port }) => (
port.addEventListener('message', ev => (
global.mockKeys[ev.data.key] = ev.data.keylong)),
mockKeys[ev.data.key] = ev.data.keylong)),
port.unref(),
'global.postMessageEsmk = d => port.postMessage(d)'
))

// node v20.6-current
const initialize = module.register && (data => {
if (data && data.port) {
data.port.on('message', msg => {
mockKeys[msg.key] = msg.keylong
})
}
})

const parseImports = defstr => {
const [specifier, imports] = (defstr.match(esmkImportRe) || [])

Expand All @@ -57,7 +71,7 @@ const parseImportsTree = treeidspec => {
}

const treeidspecFromUrl = url => esmkIdRe.test(url)
&& global.esmockTreeIdGet(url.match(esmkIdRe)[0].split('=')[1])
&& mockKeys[url.match(esmkIdRe)[0].split('=')[1]]

// new versions of node: when multiple loaders are used and context
// is passed to nextResolve, the process crashes in a recursive call
Expand Down Expand Up @@ -201,4 +215,11 @@ const load = async (url, context, nextLoad) => {
// node lt 16.12 require getSource, node gte 16.12 warn remove getSource
const getSource = isLT1612 && load

export {load, resolve, getSource, globalPreload, loaderIsVerified as default}
export {
load,
resolve,
getSource,
initialize,
globalPreload,
loaderIsVerified as default
}
28 changes: 28 additions & 0 deletions src/esmockRegister.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import module from 'node:module'
import threads from 'node:worker_threads'

const channel = threads.MessageChannel
&& new threads.MessageChannel()

const register = (res => () => {
if (typeof res === 'boolean')
return res

if ((res = Boolean(module.register))) {
module.register('./esmockLoader.js', {
parentURL: import.meta.url,
data: { port: channel.port2 },
transferList: [channel.port2]
})
}

return res
})()

export default Object.assign(msg => {
if (register()) {
channel.port1.postMessage(msg)
} else if (typeof global.postMessageEsmk === 'function') {
global.postMessageEsmk(msg)
}
}, { register })
4 changes: 3 additions & 1 deletion tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"sinon": "^12.0.1"
},
"scripts": {
"mini": "cd .. && cd src && npx esbuild esmock.js --minify --bundle --allow-overwrite --platform=node --format=esm --outfile=esmock.js",
"mini:esmock": "cd .. && cd src && npx esbuild esmock.js --minify --bundle --allow-overwrite --platform=node --format=esm --outfile=esmock.js",
"mini:esmockLoader": "cd .. && cd src && npx esbuild esmockLoader.js --minify --bundle --allow-overwrite --platform=node --format=esm --outfile=esmockLoader.js",
"mini": "npm run mini:esmock && npm run mini:esmockLoader",
"isnodelt18": "node -e \"+process.versions.node.split('.')[0] < 18 || process.exit(1)\"",
"isnodegt19": "node -e \"+process.versions.node.split('.')[0] > 19 || process.exit(1)\"",
"isnodenight": "node -e \"process.versions.node.includes('night') || process.exit(1)\"",
Expand Down
4 changes: 3 additions & 1 deletion tests/tests-ava/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"babelGeneratedDoubleDefault": "file:../local/babelGeneratedDoubleDefault"
},
"scripts": {
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:default": "NODE_OPTIONS=--loader=esmock ava",
"test:win32": "set \"NODE_OPTIONS=--loader=esmock\" && ava",
"test": "run-script-os"
"test:current": "ava",
"test": "run-script-os && npm run isnodelt20_6 || npm run test:current"
}
}
13 changes: 7 additions & 6 deletions tests/tests-jest-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
"runner": "jest-light-runner"
},
"scripts": {
"test:default-metaresolve": "NODE_OPTIONS=\"--experimental-import-meta-resolve --loader=ts-node/esm --loader=esmock\" jest esmock.node-jest.test.ts",
"test:default-nometaresolve": "NODE_OPTIONS=\"--loader=ts-node/esm --loader=esmock\" jest esmock.node-jest.test.ts",
"test:default": "npm run test:default-metaresolve && npm run test:default-nometaresolve",
"test:win32-metaresolve": "set \"NODE_OPTIONS=--experimental-import-meta-resolve --loader=ts-node/esm --loader=esmock\" && jest esmock.node-jest.test.ts",
"test:win32-nometaresolve": "set \"NODE_OPTIONS=--loader=ts-node/esm --loader=esmock\" && jest esmock.node-jest.test.ts",
"test:win32": "npm run test:win32-metaresolve && npm run test:win32-nometaresolve",
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:defaultcurrent": "NODE_OPTIONS=\"--loader=ts-node/esm\" jest esmock.node-jest.test.ts",
"test:defaultloader": "NODE_OPTIONS=\"--loader=ts-node/esm --loader=esmock\" jest esmock.node-jest.test.ts",
"test:default": "npm run test:defaultloader && npm run isnodelt20_6 || npm run test:defaultcurrent",
"test:win32current": "set \"NODE_OPTIONS=--loader=ts-node/esm\" && jest esmock.node-jest.test.ts",
"test:win32loader": "set \"NODE_OPTIONS=--loader=ts-node/esm --loader=esmock\" && jest esmock.node-jest.test.ts",
"test:win32": "npm run test:win32loader && npm run isnodelt20_6 || npm run test:win32current",
"test": "run-script-os"
}
}
12 changes: 5 additions & 7 deletions tests/tests-jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@
"runner": "jest-light-runner"
},
"scripts": {
"test:default-metaresolve": "NODE_OPTIONS=\"--experimental-import-meta-resolve --loader=esmock\" jest",
"test:default-nometaresolve": "NODE_OPTIONS=--loader=esmock jest",
"test:default": "npm run test:default-metaresolve && npm run test:default-nometaresolve",
"test:win32-metaresolve": "set \"NODE_OPTIONS=--experimental-import-meta-resolve --loader=esmock\" && jest",
"test:win32-nometaresolve": "set NODE_OPTIONS=--loader=esmock && jest",
"test:win32": "npm run test:win32-metaresolve && npm run test:win32-metaresolve",
"test": "run-script-os"
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:default": "NODE_OPTIONS=--loader=esmock jest",
"test:win32": "set NODE_OPTIONS=--loader=esmock && jest",
"test:current": "jest",
"test": "run-script-os && npm run isnodelt20_6 || npm run test:current"
}
}
5 changes: 4 additions & 1 deletion tests/tests-mocha/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"passport": "^0.6.0"
},
"scripts": {
"test": "mocha --loader=esmock"
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:loader": "mocha --loader=esmock",
"test:current": "mocha",
"test": "npm run test:loader && npm run isnodelt20_6 || npm run test:current"
}
}
23 changes: 19 additions & 4 deletions tests/tests-no-loader/esmock.noloader.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import module from 'node:module'
import esmock from 'esmock'
import esmockErr from '../../src/esmockErr.js'

test('should throw error if !esmockloader', async () => {
await assert.rejects(() => esmock('./to/module.js'), {
message: esmockErr.errMissingLoader().message
if (!module.register) {
test('should throw error if !esmockloader', async () => {
await assert.rejects(() => esmock('./to/module.js'), {
message: esmockErr.errMissingLoader().message
})
})
})
}

// node version 20.6+ do not need --loader
if (module.register) {
test('should mock a module', async () => {
const main = await esmock('../local/mainUtil.js', {
'form-urlencoded': () => 'mock encode'
})

assert.strictEqual(typeof main, 'function')
assert.strictEqual(main.createString(), 'mock encode')
})
}
7 changes: 4 additions & 3 deletions tests/tests-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
"babelGeneratedDoubleDefault": "file:../local/babelGeneratedDoubleDefault"
},
"scripts": {
"test:metaresolve": "node --experimental-import-meta-resolve --loader=esmock --test",
"test": "node --loader=esmock --test && npm run test:metaresolve",
"test-one": "node --loader=esmock --test"
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:loader": "node --loader=esmock --test",
"test:current": "node --test",
"test": "npm run test:loader && npm run isnodelt20_6 || npm run test:current"
}
}
7 changes: 4 additions & 3 deletions tests/tests-nodets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
"babelGeneratedDoubleDefault": "file:../local/babelGeneratedDoubleDefault"
},
"scripts": {
"test-metaresolve": "node --experimental-import-meta-resolve --loader=ts-node/esm --loader=esmock --test esmock.node-ts.test.ts esmock.node-ts.importing.test.ts",
"test-nometaresolve": "node --loader=ts-node/esm --loader=esmock --test esmock.node-ts.test.ts esmock.node-ts.importing.test.ts",
"test": "npm run test-metaresolve && npm run test-nometaresolve"
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:current": "node --loader=ts-node/esm --test esmock.node-ts.test.ts esmock.node-ts.importing.test.ts",
"test:loader": "node --loader=ts-node/esm --loader=esmock --test esmock.node-ts.test.ts esmock.node-ts.importing.test.ts",
"test": "npm run test:loader && npm run isnodelt20_6 || npm run test:current"
}
}
7 changes: 4 additions & 3 deletions tests/tests-source-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
"typescript": "^5.1.6"
},
"scripts": {
"test-metaresolve": "rimraf dist && tsc && cross-env \"NODE_OPTIONS=--experimental-import-meta-resolve --loader=esmock\" NODE_NO_WARNINGS=1 ava",
"test-nometaresolve": "rimraf dist && tsc && cross-env NODE_OPTIONS=--loader=esmock NODE_NO_WARNINGS=1 ava",
"test": "npm run test-metaresolve && npm run test-nometaresolve",
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:loader": "rimraf dist && tsc && cross-env NODE_OPTIONS=--loader=esmock NODE_NO_WARNINGS=1 ava",
"test:current": "rimraf dist && tsc && cross-env NODE_NO_WARNINGS=1 ava",
"test": "npm run test:loader && npm run isnodelt20_6 || npm run test:current",
"test-no-maps": "rimraf dist && tsc --sourceMap false && cross-env NODE_OPTIONS=--loader=esmock NODE_NO_WARNINGS=1 ava"
},
"ava": {
Expand Down
7 changes: 4 additions & 3 deletions tests/tests-tsm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
"babelGeneratedDoubleDefault": "file:../local/babelGeneratedDoubleDefault"
},
"scripts": {
"test:metaresolve": "node --experimental-import-meta-resolve --loader=tsm --loader=esmock --test esmock.node-tsm.test.ts",
"test:nometaresolve": "node --loader=tsm --loader=esmock --test esmock.node-tsm.test.ts",
"test": "npm run test:metaresolve && npm run test:nometaresolve"
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:loader": "node --loader=tsm --loader=esmock --test esmock.node-tsm.test.ts",
"test:current": "node --loader=tsm --test esmock.node-tsm.test.ts",
"test": "npm run test:loader && npm run isnodelt20_6 || npm run test:current"
}
}
5 changes: 4 additions & 1 deletion tests/tests-uvu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"babelGeneratedDoubleDefault": "file:../local/babelGeneratedDoubleDefault"
},
"scripts": {
"test": "node --loader=esmock ./node_modules/uvu/bin.js"
"isnodelt20_6": "node -e \"(([mj, mn]) => (+mj < 20 || (+mj === 20 && +mn < 6)))(process.versions.node.split('.')) || process.exit(1)\"",
"test:loader": "node --loader=esmock ./node_modules/uvu/bin.js",
"test:current": "uvu",
"test": "npm run test:loader && npm run isnodelt20_6 || npm run test:current"
}
}

0 comments on commit 8cb00f1

Please sign in to comment.