diff --git a/CHANGELOG.md b/CHANGELOG.md index 20bc209d..ddde3942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # changelog - * 1.9.5 + * 1.9.6 _Aug.19.2022_ + * support parent url to facilitate sourcemap usage, [113](https://github.com/iambumblehead/esmock/issues/113) + * 1.9.5 _Aug.19.2022_ * support cjs packges that define [main relative directory only](https://github.com/iambumblehead/esmock/issues/119) * 1.9.4 _Aug.15.2022_ * support core modules [w/ node: prefix](https://github.com/iambumblehead/resolvewithplus/pull/27), credit @gmahomarf diff --git a/package.json b/package.json index a984b61b..d6db42dd 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "test-ci": "npm run test:install && npm run test:all-ci", "test-cover": "npm run test:install && c8 npm run test:all", "lint": "eslint .", + "lint-fix": "eslint --fix .", "mini:pkg": "npm pkg delete scripts devDependencies", "prepublishOnly": "npm run lint && npm run test-ci && npm run mini:pkg" } diff --git a/src/esmock.d.ts b/src/esmock.d.ts index ac3352b7..1eaab502 100644 --- a/src/esmock.d.ts +++ b/src/esmock.d.ts @@ -1,2 +1,76 @@ -declare function esmock(path: string, localmock?: any, globalmock?: any): any; +/** + * Mocks imports for the module specified by {@link modulePath}. + * + * The provided mocks replace the imported modules _fully_. + * + * @param modulePath The module whose imports will be mocked. + * @param parent A URL to resolve specifiers relative to; typically `import.meta.url`. + * If not specified, it will be inferred via the stack, which may not work + * if source maps are in use. + * @param mockDefs A mapping of import specifiers to mocked module objects; these mocks will + * only be used for imports resolved in the module specified by {@link modulePath}. + * @param globalDefs A mapping of import specifiers to mocked module objects; these mocks will + * apply to imports within the module specified by {@link modulePath}, as well + * as any transitively imported modules. + * @param opt + * @returns The result of importing {@link modulePath}, similar to `import(modulePath)`. + */ +declare function esmock(modulePath: string, parent: string, mockDefs?: Record, globalDefs?: Record, opt?: esmock.Options): any; +declare function esmock(modulePath: string, mockDefs?: Record, globalDefs?: Record, opt?: esmock.Options): any; + +declare namespace esmock { + interface Options { + partial?: boolean | undefined; + purge?: boolean | undefined; + } + + /** + * Mocks imports for the module specified by {@link modulePath}. + * + * The provided mocks replace the imported modules _partially_, allowing some exports to + * be overridden while the rest are provided by the real module. + * + * @param modulePath The module whose imports will be mocked. + * @param parent A URL to resolve specifiers relative to; typically `import.meta.url`. + * If not specified, it will be inferred via the stack, which may not work + * if source maps are in use. + * @param mockDefs A mapping of import specifiers to mocked module objects; these mocks will + * only be used for imports resolved in the module specified by {@link modulePath}. + * @param globalDefs A mapping of import specifiers to mocked module objects; these mocks will + * apply to imports within the module specified by {@link modulePath}, as well + * as any transitively imported modules. + * @param opt + * @returns The result of importing {@link modulePath}, similar to `import(modulePath)`. + */ + function px(modulePath: string, parent: string, mockDefs?: Record, globalDefs?: Record, opt?: esmock.Options): any; + function px(modulePath: string, mockDefs?: Record, globalDefs?: Record, opt?: esmock.Options): any; + + /** + * Mocks dynamic imports for the module specified by {@link modulePath}. + * + * After using this function, consider calling {@link esmock.purge} to free memory. + * + * @param modulePath The module whose imports will be mocked. + * @param parent A URL to resolve specifiers relative to; typically `import.meta.url`. + * If not specified, it will be inferred via the stack, which may not work + * if source maps are in use. + * @param mockDefs A mapping of import specifiers to mocked module objects; these mocks will + * only be used for imports resolved in the module specified by {@link modulePath}. + * @param globalDefs A mapping of import specifiers to mocked module objects; these mocks will + * apply to imports within the module specified by {@link modulePath}, as well + * as any transitively imported modules. + * @param opt + * @returns The result of importing {@link modulePath}, similar to `import(modulePath)`. + */ + function p(modulePath: string, parent: string, mockDefs?: Record, globalDefs?: Record, opt?: esmock.Options): any; + function p(modulePath: string, mockDefs?: Record, globalDefs?: Record, opt?: esmock.Options): any; + + /** + * Unregisters a dynamic mock created by {@link esmock.p}. + * + * @param mockModule A module object that was previously returned by {@link esmock.p}. + */ + function purge(mockModule: any): void; +} + export default esmock; diff --git a/src/esmock.js b/src/esmock.js index f868eaa3..f81f486f 100644 --- a/src/esmock.js +++ b/src/esmock.js @@ -8,8 +8,21 @@ import { esmockCache } from './esmockCache.js' -const esmock = async (modulePath, mockDefs, globalDefs, opt = {}, err) => { - const calleePath = (err || new Error).stack.split('\n')[2] +const argsToObj = args => { + // Distinguish between the two overloads; see esmock.d.ts. + let modulePath, parent, mockDefs, globalDefs, opt + if (typeof args[1] === "string") { + [ modulePath, parent, mockDefs, globalDefs, opt ] = args + } else { + [ modulePath, mockDefs, globalDefs, opt ] = args + } + return { modulePath, parent, mockDefs, globalDefs, opt } +} + +const _esmock = async (argsObj, err) => { + const { modulePath, parent, mockDefs, globalDefs, opt = {} } = argsObj + + const calleePath = (parent || err.stack.split('\n')[2]) .replace(/^.*file:\/\//, '') // rm every before filepath .replace(/:[\d]*:[\d]*.*$/, '') // rm line and row number .replace(/^.*:/, '') // rm windows-style drive location @@ -29,11 +42,19 @@ const esmock = async (modulePath, mockDefs, globalDefs, opt = {}, err) => { return esmockModuleImportedSanitize(importedModule, modulePathKey) } -esmock.px = async (modulePath, mockDefs, globalDefs, opt) => esmock( - modulePath, mockDefs, globalDefs, { ...opt, partial: true }, new Error) +const esmock = async (...args) => _esmock(argsToObj(args), new Error) -esmock.p = async (modulePath, mockDefs, globalDefs, opt) => esmock( - modulePath, mockDefs, globalDefs, { ...opt, purge: false }, new Error) +esmock.px = async (...args) => { + const argsObj = argsToObj(args) + argsObj.opt = { ...argsObj.opt, partial: true } + return _esmock(argsObj, new Error) +} + +esmock.p = async (...args) => { + const argsObj = argsToObj(args) + argsObj.opt = { ...argsObj.opt, purge: false } + return _esmock(argsObj, new Error) +} esmock.purge = mockModule => { if (mockModule && /object|function/.test(typeof mockModule) diff --git a/tests/package.json b/tests/package.json index bd4f23ff..86a0c9c4 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,6 +7,10 @@ "url": "https://github.com/iambumblehead/esmock.git" }, "main": "package.json.esmock.export.js", + "exports": { + "types": "./package.json.esmock.export.d.ts", + "import": "./package.json.esmock.export.js" + }, "dependencies": { "pg": "^8.7.3", "eslint": "^8.12.0", @@ -27,6 +31,7 @@ "install:test-tsm": "cd tests-tsm && npm install", "install:test-node": "cd tests-node && npm install", "install:test-jest": "cd tests-jest && npm install", + "install:test-source-map": "cd tests-source-map && npm install", "install:test-nodets": "cd tests-nodets && npm install", "install:all": "node --version && npm install && npm-run-all install:test*", "test:test-ava": "cd tests-ava && npm test", @@ -35,6 +40,7 @@ "test:node18-test-node": "cd tests-node && npm test", "test:node18-test-jest": "cd tests-jest && npm test", "test:node18-test-nodets": "cd tests-nodets && npm test", + "test:node18-test-source-map": "cd tests-source-map && npm test", "test:node18:all": "npm run isnodelt18 || npm-run-all test:node18-test*", "test:all": "npm-run-all test:test* && npm run test:node18:all", "test:all-cover": "c8 --src=../src/* npm run test:all", diff --git a/tests/package.json.esmock.export.d.ts b/tests/package.json.esmock.export.d.ts new file mode 100644 index 00000000..347022d7 --- /dev/null +++ b/tests/package.json.esmock.export.d.ts @@ -0,0 +1,3 @@ +import esmock from "../src/esmock.js"; + +export default esmock; diff --git a/tests/tests-source-map/.gitignore b/tests/tests-source-map/.gitignore new file mode 100644 index 00000000..53c37a16 --- /dev/null +++ b/tests/tests-source-map/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/tests/tests-source-map/README.md b/tests/tests-source-map/README.md new file mode 100644 index 00000000..5c6ec491 --- /dev/null +++ b/tests/tests-source-map/README.md @@ -0,0 +1,7 @@ +# esmock-source-map-bug + +To run with source maps, run `npm run test`. esmock would previously fail unless the provided module path was absolute + +To run without source maps, run `npm run test-no-maps`. This works. + +https://github.com/iambumblehead/esmock/issues/113 diff --git a/tests/tests-source-map/package.json b/tests/tests-source-map/package.json new file mode 100644 index 00000000..b32cfc1c --- /dev/null +++ b/tests/tests-source-map/package.json @@ -0,0 +1,34 @@ +{ + "type": "module", + "description": "esmock unit tests, ava with sourcemap", + "repository": { + "type": "git", + "url": "https://github.com/iambumblehead/esmock.git" + }, + "dependencies": { + "esmock": "file:..", + "@ava/typescript": "^3.0.1", + "@tsconfig/node14": "^1.0.3", + "@types/node": "^18.7.3", + "ava": "^4.3.1", + "cross-env": "^7.0.3", + "rimraf": "^3.0.2", + "typescript": "^4.7.4" + }, + "scripts": { + "test": "rimraf dist && tsc && cross-env NODE_OPTIONS=--loader=esmock NODE_NO_WARNINGS=1 ava", + "test-no-maps": "rimraf dist && tsc --sourceMap false && cross-env NODE_OPTIONS=--loader=esmock NODE_NO_WARNINGS=1 ava" + }, + "ava": { + "typescript": { + "rewritePaths": { + "src/": "dist/" + }, + "compile": false + }, + "environmentVariables": { + "NO_COLOR": "1", + "FORCE_COLOR": "0" + } + } +} diff --git a/tests/tests-source-map/src/__tests__/index.test.ts b/tests/tests-source-map/src/__tests__/index.test.ts new file mode 100644 index 00000000..04a32f70 --- /dev/null +++ b/tests/tests-source-map/src/__tests__/index.test.ts @@ -0,0 +1,30 @@ +import test from "ava"; +import esmock from "esmock"; +import { fileURLToPath } from "url"; + +import type * as indexType from "../index.js"; + +const expectedHostname = "my-machine"; + +test("using absolute path", async (t) => { + const urlFull = fileURLToPath(new URL("../index.js", import.meta.url)); + const indexModule = await esmock(urlFull, { + os: { + hostname: () => expectedHostname, + }, + }); + + const getHostname: typeof indexType.getHostname = indexModule.getHostname; + t.is(getHostname(), expectedHostname); +}); + +test("using relative path", async (t) => { + const indexModule = await esmock("../index.js", import.meta.url, { + os: { + hostname: () => expectedHostname, + }, + }); + + const getHostname: typeof indexType.getHostname = indexModule.getHostname; + t.is(getHostname(), expectedHostname); +}); diff --git a/tests/tests-source-map/src/index.ts b/tests/tests-source-map/src/index.ts new file mode 100644 index 00000000..77f829d5 --- /dev/null +++ b/tests/tests-source-map/src/index.ts @@ -0,0 +1,5 @@ +import os from "os"; + +export function getHostname() { + return os.hostname(); +} diff --git a/tests/tests-source-map/tsconfig.json b/tests/tests-source-map/tsconfig.json new file mode 100644 index 00000000..061d11dd --- /dev/null +++ b/tests/tests-source-map/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "@tsconfig/node14/tsconfig.json", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "rootDir": "src", + "outDir": "dist", + "declarationDir": "dist", + "declaration": true, + "stripInternal": true, + "sourceMap": true, + + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "importsNotUsedAsValues": "error" + }, + "exclude": ["dist"] +}