diff --git a/README.md b/README.md index 47dc9913..5fabe641 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ _**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"._ -_**Note: Typescript loaders** `ts-node` 👍 and `tsm` 👍 are compatible with other loaders, [including esmock.][3] `swc` 👎 and `tsx` 👎 are demonstrated as **incompatible** with other loaders, including esmock._ +_**Note: TypeScript loaders** `ts-node` 👍, `tsm` 👍 and `tsx` 👍 are compatible with other loaders, [including esmock.][3] `swc-node` has, at time of writing, been demonstrated as **incompatible** with other loaders, including esmock._ `esmock` has the below signature ```js diff --git a/src/esmockLoader.js b/src/esmockLoader.js index 9770fc81..e4eca587 100644 --- a/src/esmockLoader.js +++ b/src/esmockLoader.js @@ -1,82 +1,90 @@ -import fs from 'node:fs/promises' -import module from 'node:module' -import process from 'process' -import esmockErr from './esmockErr.js' +import fs from "node:fs/promises"; +import module from "node:module"; +import process from "process"; +import esmockErr from "./esmockErr.js"; -const [major, minor] = process.versions.node.split('.').map(it => +it) -const isLT1612 = major < 16 || (major === 16 && minor < 12) +const [major, minor] = process.versions.node.split(".").map((it) => +it); +const isLT1612 = major < 16 || (major === 16 && minor < 12); // ex, file:///path/to/esmockLoader.js, // file:///c:/path/to/esmockLoader.js -const urlDummy = import.meta.url -const esmkgdefsAndAfterRe = /\?esmkgdefs=.*/ -const esmkgdefsAndBeforeRe = /.*\?esmkgdefs=/ -const esmkdefsRe = /#-#esmkdefs/ -const esmkImportStartRe = /^file:\/\/\/import\?/ -const esmkImportRe = /file:\/\/\/import\?([^#]*)/ -const esmkImportListItemRe = /\bimport,|,import\b|\bimport\b/g -const esmkTreeIdRe = /esmkTreeId=\d*/ -const esmkModuleIdRe = /esmkModuleId=([^&]*)/ -const esmkIdRe = /\?esmk=\d*/ -const exportNamesRe = /.*exportNames=(.*)/ -const withHashRe = /.*#-#/ -const isesmRe = /isesm=true/ -const isnotfoundRe = /isfound=false/ -const iscommonjsmoduleRe = /^(commonjs|module)$/ -const isstrict3 = /strict=3/ -const hashbangRe = /^(#![^\n]*\n)/ +const urlDummy = import.meta.url; +const esmkgdefsAndAfterRe = /\?esmkgdefs=.*/; +const esmkgdefsAndBeforeRe = /.*\?esmkgdefs=/; +const esmkdefsRe = /#-#esmkdefs/; +const esmkImportStartRe = /^file:\/\/\/import\?/; +const esmkImportRe = /file:\/\/\/import\?([^#]*)/; +const esmkImportListItemRe = /\bimport,|,import\b|\bimport\b/g; +const esmkTreeIdRe = /esmkTreeId=\d*/; +const esmkModuleIdRe = /esmkModuleId=([^&]*)/; +const esmkIdRe = /\?esmk=\d*/; +const exportNamesRe = /.*exportNames=(.*)/; +const withHashRe = /.*#-#/; +const isesmRe = /isesm=true/; +const isnotfoundRe = /isfound=false/; +const iscommonjsmoduleRe = /^(commonjs|module)$/; +const isstrict3 = /strict=3/; +const hashbangRe = /^(#![^\n]*\n)/; // returned regexp will match embedded moduleid w/ treeid -const moduleIdReCreate = (moduleid, treeid) => new RegExp( - `.*(${moduleid}(\\?${treeid}(?:(?!#-#).)*)).*`) +const moduleIdReCreate = (moduleid, treeid) => + new RegExp(`.*(${moduleid}(\\?${treeid}(?:(?!#-#).)*)).*`); // node v12.0-v18.x, global -const mockKeys = global.mockKeys = (global.mockKeys || {}) -const mockKeysSource = global.mockKeysSource = (global.mockKeysSource || {}) +const mockKeys = (global.mockKeys = global.mockKeys || {}); +const mockKeysSource = (global.mockKeysSource = global.mockKeysSource || {}); // node v20.0-v20.6 -const globalPreload = !module.register && (({ port }) => ( - port.addEventListener('message', ev => ( - ev.data.keysource - ? mockKeysSource[ev.data.keysource] = ev.data.source - : mockKeys[ev.data.key] = ev.data.keylong)), - port.unref(), - 'global.postMessageEsmk = d => port.postMessage(d)' -)) +const globalPreload = + !module.register && + (({ port }) => ( + port.addEventListener("message", (ev) => + ev.data.keysource + ? (mockKeysSource[ev.data.keysource] = ev.data.source) + : (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 => { - msg.keysource - ? mockKeysSource[msg.keysource] = msg.source - : mockKeys[msg.key] = msg.keylong - }) - } -}) +const initialize = + module.register && + ((data) => { + if (data && data.port) { + data.port.on("message", (msg) => { + msg.keysource + ? (mockKeysSource[msg.keysource] = msg.source) + : (mockKeys[msg.key] = msg.keylong); + }); + } + }); -const parseImports = defstr => { - const [specifier, imports] = (defstr.match(esmkImportRe) || []) +const parseImports = (defstr) => { + const [specifier, imports] = defstr.match(esmkImportRe) || []; - return [ // return [specifier, importNames] - specifier, exportNamesRe.test(imports) && - imports.replace(exportNamesRe, '$1').split(',')] -} + return [ + // return [specifier, importNames] + specifier, + exportNamesRe.test(imports) && + imports.replace(exportNamesRe, "$1").split(","), + ]; +}; // parses local and global mock imports from long-url treeidspec -const parseImportsTree = treeidspec => { - const defs = treeidspec.split(esmkdefsRe)[1] || '' - const defimports = parseImports(defs) - const gdefs = treeidspec.replace(esmkgdefsAndBeforeRe, '') - const gdefimports = parseImports(gdefs) +const parseImportsTree = (treeidspec) => { + const defs = treeidspec.split(esmkdefsRe)[1] || ""; + const defimports = parseImports(defs); + const gdefs = treeidspec.replace(esmkgdefsAndBeforeRe, ""); + const gdefimports = parseImports(gdefs); return [ defimports[0] || gdefimports[0], - [...new Set([defimports[1] || [], gdefimports[1] || []].flat())] - ] -} + [...new Set([defimports[1] || [], gdefimports[1] || []].flat())], + ]; +}; -const treeidspecFromUrl = url => esmkIdRe.test(url) - && mockKeys[url.match(esmkIdRe)[0].split('=')[1]] +const treeidspecFromUrl = (url) => + esmkIdRe.test(url) && 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 @@ -86,155 +94,167 @@ const treeidspecFromUrl = url => esmkIdRe.test(url) // is not passed to nextResolve, the tests fail // // later versions of node v16 include 'node-addons' -const nextResolveCall = async (nextResolve, specifier, context) => ( +const nextResolveCall = async (nextResolve, specifier, context) => context.parentURL && - (context.conditions.slice(-1)[0] === 'node-addons' - || context.importAssertions || isLT1612) + (context.conditions.slice(-1)[0] === "node-addons" || + context.importAssertions || + isLT1612) ? nextResolve(specifier, context) - : nextResolve(specifier)) + : nextResolve(specifier); const resolve = async (specifier, context, nextResolve) => { - const { parentURL } = context - const treeidspec = treeidspecFromUrl(parentURL) || parentURL + const { parentURL } = context; + const treeidspec = treeidspecFromUrl(parentURL) || parentURL; if (!esmkTreeIdRe.test(treeidspec)) - return nextResolveCall(nextResolve, specifier, context) + return nextResolveCall(nextResolve, specifier, context); - const [treeid] = String(treeidspec).match(esmkTreeIdRe) - const [url, defs] = treeidspec.split(esmkdefsRe) - const gdefs = url && url.replace(esmkgdefsAndBeforeRe, '') + 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 (treeidspec.includes(`esmkModuleId=${specifier}&isfound=false`)) { - const moduleIdRe = moduleIdReCreate(`file:///${specifier}`, treeid) - const moduleId = ( - gdefs.match(moduleIdRe) || defs.match(moduleIdRe) || [])[2] + const moduleIdRe = moduleIdReCreate(`file:///${specifier}`, treeid); + const moduleId = (gdefs.match(moduleIdRe) || + defs.match(moduleIdRe) || + [])[2]; if (moduleId) { return { shortCircuit: true, - url: urlDummy + moduleId - } + url: urlDummy + moduleId, + }; } } if (esmkImportStartRe.test(specifier)) { return { shortCircuit: true, - url: specifier.replace(esmkImportStartRe, urlDummy + '?') - } + url: specifier.replace(esmkImportStartRe, urlDummy + "?"), + }; } - const resolved = await nextResolveCall(nextResolve, specifier, context) - const moduleIdRe = moduleIdReCreate(resolved.url, treeid) + const resolved = await nextResolveCall(nextResolve, specifier, context); + const moduleIdRe = moduleIdReCreate(resolved.url, treeid); const moduleId = - moduleIdRe.test(defs) && defs.replace(moduleIdRe, '$1') || - moduleIdRe.test(gdefs) && gdefs.replace(moduleIdRe, '$1') + (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 += '?esmkgdefs=' + gdefs + : urlDummy + "#-#" + moduleId; + } else if (gdefs && gdefs !== "0") { + if (!resolved.url.startsWith("node:")) { + resolved.url += "?esmkgdefs=" + gdefs; } } if (isstrict3.test(treeidspec) && !moduleId) - throw esmockErr.errModuleIdNotMocked(resolved.url, treeidspec.split('?')[0]) + throw esmockErr.errModuleIdNotMocked( + resolved.url, + treeidspec.split("?")[0] + ); - return resolved -} + return resolved; +}; -const loaderVerifyUrl = urlDummy + '?esmock-loader=true' -const loaderIsVerified = (memo => async () => memo = memo || ( - (await import(loaderVerifyUrl)).default === true))() +const loaderVerifyUrl = urlDummy + "?esmock-loader=true"; +const loaderIsVerified = ( + (memo) => async () => + (memo = memo || (await import(loaderVerifyUrl)).default === true) +)(); const load = async (url, context, nextLoad) => { if (url === loaderVerifyUrl) { return { - format: 'module', + format: "module", shortCircuit: true, responseURL: url, - source: 'export default true' - } + source: "export default true", + }; } - const treeidspec = treeidspecFromUrl(url) || url - const treeid = treeidspec && - (treeidspec.match(esmkTreeIdRe) || [])[0] + const treeidspec = treeidspecFromUrl(url) || url; + const treeid = treeidspec && (treeidspec.match(esmkTreeIdRe) || [])[0]; if (treeid) { - const [specifier, importedNames] = parseImportsTree(treeidspec) + const [specifier, importedNames] = parseImportsTree(treeidspec); if (importedNames && importedNames.length) { - const nextLoadRes = await nextLoad(url, context) - if (!iscommonjsmoduleRe.test(nextLoadRes.format)) - return nextLoadRes + const nextLoadRes = await nextLoad(url, context); + if (!iscommonjsmoduleRe.test(nextLoadRes.format)) return nextLoadRes; // nextLoadRes.source sometimes 'undefined' and other times 'null' :( - const sourceIsNullLike = ( - nextLoadRes.source === null || nextLoadRes.source === undefined) + const sourceIsNullLike = + nextLoadRes.source === null || nextLoadRes.source === undefined; const source = sourceIsNullLike ? String(await fs.readFile(new URL(url))) - : String(nextLoadRes.source) - const hbang = (source.match(hashbangRe) || [])[0] || '' - const sourcesafe = hbang ? source.replace(hashbangRe, '') : source - const importexpr = nextLoadRes.format === 'commonjs' - ? `const {${importedNames}} = global.esmockCacheGet("${specifier}");` - : `import {${importedNames}} from '${specifier}';` + : String(nextLoadRes.source); + const hbang = (source.match(hashbangRe) || [])[0] || ""; + const sourcesafe = hbang ? source.replace(hashbangRe, "") : source; + const importexpr = + nextLoadRes.format === "commonjs" + ? `const {${importedNames}} = global.esmockCacheGet("${specifier}");` + : `import {${importedNames}} from '${specifier}';`; return { format: nextLoadRes.format, shortCircuit: true, responseURL: encodeURI(url), - source: hbang + importexpr + sourcesafe - } + source: hbang + importexpr + sourcesafe, + }; } } - if (esmkdefsRe.test(url)) // parent of mocked modules - return nextLoad(url, context) + if (esmkdefsRe.test(url)) + // parent of mocked modules + return nextLoad(url, context); - url = url.replace(esmkgdefsAndAfterRe, '') + url = url.replace(esmkgdefsAndAfterRe, ""); if (url.startsWith(urlDummy)) { - url = url.replace(withHashRe, '') + url = url.replace(withHashRe, ""); if (isnotfoundRe.test(url)) - url = url.replace(urlDummy, `file:///${url.match(esmkModuleIdRe)[1]}`) + url = url.replace(urlDummy, `file:///${url.match(esmkModuleIdRe)[1]}`); } - const exportedNames = exportNamesRe.test(url) && url - .replace(exportNamesRe, '$1') - .replace(esmkImportListItemRe, '') - .split(',') + const exportedNames = + exportNamesRe.test(url) && + url + .replace(exportNamesRe, "$1") + .replace(esmkImportListItemRe, "") + .split(","); if (exportedNames && exportedNames[0]) { if (mockKeysSource[url]) { return { // ...await nextLoad(url, context), - format: 'json', + format: "json", shortCircuit: true, responseURL: encodeURI(url), - source: mockKeysSource[url] - } + source: mockKeysSource[url], + }; } return { - format: 'module', + format: "module", shortCircuit: true, responseURL: encodeURI(url), - source: exportedNames.map(name => name === 'default' - ? `export default global.esmockCacheGet("${url}").default` - : `export const ${name} = global.esmockCacheGet("${url}").${name}` - ).join('\n') - } + source: exportedNames + .map((name) => + name === "default" + ? `export default global.esmockCacheGet("${url}").default` + : `export const ${name} = global.esmockCacheGet("${url}").${name}` + ) + .join("\n"), + }; } if (treeid && !url.includes(treeid)) { // long querystring reduces readability of runtime error stacktrace // smaller querystring `esmk=$id` does not clutter stacktrace - url = url + '?esmk=' + treeid.split('=')[1] + url = url + "?esmk=" + treeid.split("=")[1]; } - return nextLoad(url, context) -} + return nextLoad(url, context); +}; // node lt 16.12 require getSource, node gte 16.12 warn remove getSource -const getSource = isLT1612 && load +const getSource = isLT1612 && load; export { load, @@ -242,5 +262,5 @@ export { getSource, initialize, globalPreload, - loaderIsVerified as default -} + loaderIsVerified as default, +}; diff --git a/tests/package.json b/tests/package.json index 805efe5f..93f52bd5 100644 --- a/tests/package.json +++ b/tests/package.json @@ -37,6 +37,7 @@ "install:test-ava": "cd tests-ava && npm install", "install:test-uvu": "cd tests-uvu && npm install", "install:test-tsm": "cd tests-tsm && npm install", + "install:test-tsx": "cd tests-tsx && npm install", "install:test-node": "cd tests-node && npm install", "install:test-jest": "cd tests-jest && npm install", "install:test-jest-ts": "cd tests-jest-ts && npm install", @@ -49,8 +50,10 @@ "test:test-uvu": "cd tests-uvu && npm test", "test:test-mocha": "cd tests-mocha && npm test", "test:test-ava": "cd tests-ava && npm test", + "test:test-tsx": "cd tests-tsx && npm test", "test:node19-tsm": " cd tests-tsm && npm test", "test:node18-test-tsm": "npm run isnodenight || npm run test:node19-tsm", + "test:node18-test-tsx": "cd tests-tsx && npm run test", "test:node18-test-node": "cd tests-node && npm test", "test:node18-test-jest": "cd tests-jest && npm test", "test:node18-test-jest-ts": "cd tests-jest-ts && npm test", diff --git a/tests/tests-FAIL-tsx/esmock.node.tsx.test.ts b/tests/tests-FAIL-tsx/esmock.node.tsx.test.ts deleted file mode 100644 index ac032abd..00000000 --- a/tests/tests-FAIL-tsx/esmock.node.tsx.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import test from 'node:test' -import assert from 'assert' -import esmock from 'esmock' - -test('should mock js when using tsx', async () => { - const main = await esmock('../local/main.js', { - path: { - basename: () => 'hellow' - } - }) - - assert.strictEqual(main.pathbasenamewrap(), 'hellow') -}) - -// tsx fails :/ https://github.com/esbuild-kit/tsx/issues/264 -// -// test('should mock ts when using tsx - unknown file extension', async () => { -// const main = await esmock('../local/main-ts.ts', { -// path: { -// basename: () => 'hellow' -// } -// }) -// -// assert.strictEqual(main.pathbasenamewrap(), 'hellow') -// }) diff --git a/tests/tests-FAIL-tsx/package.json b/tests/tests-FAIL-tsx/package.json deleted file mode 100644 index 80ab97dd..00000000 --- a/tests/tests-FAIL-tsx/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "module", - "description": "esmock unit tests, tsx with node", - "repository": { - "type": "git", - "url": "https://github.com/iambumblehead/esmock.git" - }, - "dependencies": { - "esmock": "file:..", - "tsx": "^3.12.7" - }, - "scripts": { - "test": "node --loader=tsx --loader=esmock --test esmock.node.tsx.test.ts" - } -} diff --git a/tests/tests-tsx/esmock.node.tsx.test.ts b/tests/tests-tsx/esmock.node.tsx.test.ts new file mode 100644 index 00000000..558f0a03 --- /dev/null +++ b/tests/tests-tsx/esmock.node.tsx.test.ts @@ -0,0 +1,23 @@ +import test from 'node:test' +import assert from 'assert' +import esmock from 'esmock' + +test('should mock js when using tsx', async () => { + const main = await esmock('../local/main.js', { + path: { + basename: () => 'hellow' + } + }) + + assert.strictEqual(main.pathbasenamewrap(), 'hellow') +}) + +test('should mock ts when using tsx', async () => { + const main = await esmock('../local/main-ts.ts', { + path: { + basename: () => 'hellow' + } + }) + + assert.strictEqual(main.pathbasenamewrap(), 'hellow') +}) diff --git a/tests/tests-tsx/package.json b/tests/tests-tsx/package.json new file mode 100644 index 00000000..3deafcd3 --- /dev/null +++ b/tests/tests-tsx/package.json @@ -0,0 +1,21 @@ +{ + "type": "module", + "description": "esmock unit tests, tsx with node", + "repository": { + "type": "git", + "url": "https://github.com/iambumblehead/esmock.git" + }, + "dependencies": { + "esmock": "file:..", + "tsx": "^4.15.5" + }, + "scripts": { + "isloaderavailable": "node -e \"(([mj, mn]) => (+mj < 18 || (+mj === 20 && +mn < 6) || (+mj === 18 && +mn < 19)))(process.versions.node.split('.')) || process.exit(1)\"", + "test:loader": "node --loader=tsx/esm --loader=esmock --test esmock.node.tsx.test.ts", + "test:current": "node --import=tsx/esm --test esmock.node.tsx.test.ts", + "test": "npm run isloaderavailable && npm run test:loader || npm run test:current" + }, + "devDependencies": { + "@types/node": "^20.14.2" + } +} diff --git a/tests/tests-FAIL-tsx/tsconfig.json b/tests/tests-tsx/tsconfig.json similarity index 67% rename from tests/tests-FAIL-tsx/tsconfig.json rename to tests/tests-tsx/tsconfig.json index 5b17f4eb..266f26f0 100644 --- a/tests/tests-FAIL-tsx/tsconfig.json +++ b/tests/tests-tsx/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "module": "ESNext", - "moduleResolution": "node" + "moduleResolution": "node", + "lib": ["ES2015"] } }