-
-
Notifications
You must be signed in to change notification settings - Fork 33.6k
Description
Spun off from #53619 (comment); I wanted to start a new issue for this as it’s arguably a separate issue from unflagging
--experimental-detect-module. cc @cjihrig @nodejs/test_runner
Regarding the module mocking - the mock loader (
lib/test/mock_loader.js) uses that information to determine whether to generate source code in CJS or ESM. I think there are a few options to fix it:
- Maybe it doesn't really matter and it can be removed?
- Pick a default when the hint is undefined.
- Make it an option that the user can provide as input.
It seems like @cjihrig above is referring to this? https://nodejs.org/docs/latest/api/test.html#mockmodulespecifier-options. Which seems to be implemented here?
node/lib/internal/test_runner/mock/mock.js
Lines 489 to 516 in 02bd866
| module(specifier, options = kEmptyObject) { | |
| emitExperimentalWarning('Module mocking'); | |
| validateString(specifier, 'specifier'); | |
| validateObject(options, 'options'); | |
| debug('module mock entry, specifier = "%s", options = %o', specifier, options); | |
| const { | |
| cache = false, | |
| namedExports = kEmptyObject, | |
| defaultExport, | |
| } = options; | |
| const hasDefaultExport = 'defaultExport' in options; | |
| validateBoolean(cache, 'options.cache'); | |
| validateObject(namedExports, 'options.namedExports'); | |
| const sharedState = setupSharedModuleState(); | |
| const mockSpecifier = StringPrototypeStartsWith(specifier, 'node:') ? | |
| StringPrototypeSlice(specifier, 5) : specifier; | |
| // Get the file that called this function. We need four stack frames: | |
| // vm context -> getStructuredStack() -> this function -> actual caller. | |
| const caller = getStructuredStack()[3]?.getFileName(); | |
| const { format, url } = sharedState.moduleLoader.resolveSync( | |
| mockSpecifier, caller, null, | |
| ); | |
| debug('module mock, url = "%s", format = "%s", caller = "%s"', url, format, caller); | |
| validateOneOf(format, 'format', kSupportedFormats); |
Per the docs, mock.module supports defining mocks for both module systems based on passed-in objects (not source code). Then we generate source code here?
Lines 163 to 175 in 02bd866
| async function createSourceFromMock(mock) { | |
| // Create mock implementation from provided exports. | |
| const { exportNames, format, hasDefaultExport, url } = mock; | |
| const useESM = format === 'module'; | |
| const source = `${testImportSource(useESM)} | |
| if (!$__test.mock._mockExports.has('${url}')) { | |
| throw new Error(${JSONStringify(`mock exports not found for "${url}"`)}); | |
| } | |
| const $__exports = $__test.mock._mockExports.get(${JSONStringify(url)}); | |
| ${defaultExportSource(useESM, hasDefaultExport)} | |
| ${namedExportsSource(useESM, exportNames)} | |
| `; |
At first I was confused as to how this works, since ESM is unlike CommonJS in that ESM can have a default export that’s something other than an object whose properties are the named exports; so how can this one API generate mocks that apply to both module systems while still supporting this ESM-only feature. But then I noticed that an exception gets thrown if the mock is created with both defaultExport and namedExports defined and the module is required. So I guess really it supports both module systems only if you give it input that can be consumed by both systems.
But maybe that’s the answer: if the user defines one of defaultExport or namedExports, it creates the source as CommonJS, and either maps the provided defaultExport to module.exports or it adds each of the namedExports onto module.exports. If the user defines both defaultExport and namedExports, the mock is created as ESM and the restrictions mentioned in the docs apply.
Alternatively the mock could always be created as ESM, though then you have the issue that for it to be require-able --experimental-require-module needs to be enabled. But maybe that behavior could be opted into by using this API. Since the user is passing in an object I assume we don’t need to worry about top-level await.
Or we could just ask the user what format to generate the source code in. We kind of already are, though the users probably don’t realize it, since we ask for the URL of the module to be mocked and base the format on that. Though it seems like you’re trying to keep this as simple as possible so if there’s a way to avoid adding another option, I assume you’d prefer to determine this implicitly. I think determining format based on defaultExport / namedExports is a better source to use rather than how the file URL resolves.