Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mock node modules with node: URL scheme #14297

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

KhaledElmorsy
Copy link
Contributor

Summary

Allow modules that use node: paths, i.e. node:fs, to be mocked and mapped to manual mocks appropriately.

With Node adopting the aforementioned URL scheme, more users will migrate to it and expect their tools to support it.

Jest already incorporates checks for the URL scheme but not for mapping mocks and module names.

With this change, Jest strips the URL when resolving mocks and assigning moduleIDs, making any of the following test setups in the next section valid and point to the same module.

Source issue

Test plan

The change resolves the URL to the module name, so the following 3 e2e tests were added to verify that by checking if the fs module was mocked in the following 3 scenarios.

Realistically, the first scenario is the target use case.

// URL scheme for imports and mocks
const fs = require('node:fs')
jest.mock('node:fs')
// URL scheme only when mocking
const fs = require('fs')
jest.mock('node:fs')
// URL scheme only when importing
const fs = require('node:fs')
jest.mock('fs')

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Jul 4, 2023

CLA Signed

The committers listed above are authorized under a signed CLA.

@netlify
Copy link

netlify bot commented Jul 4, 2023

Deploy Preview for jestjs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 630d01a
🔍 Latest deploy log https://app.netlify.com/sites/jestjs/deploys/64a6e82f6ebee20008db2610
😎 Deploy Preview https://deploy-preview-14297--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

CHANGELOG.md Outdated Show resolved Hide resolved
KhaledElmorsy added a commit to KhaledElmorsy/jest that referenced this pull request Jul 5, 2023
… "node:" URL paths (jestjs#14297)

Allow modules imported and mocked with 'node:*' URLs to be manually mocked by resolving just the path when generating module ID's for setting and checking mocks, and when resolving module names when importing mocks.
packages/jest-resolve/src/resolver.ts Outdated Show resolved Hide resolved
packages/jest-resolve/src/resolver.ts Outdated Show resolved Hide resolved
KhaledElmorsy added a commit to KhaledElmorsy/jest that referenced this pull request Jul 6, 2023
… "node:" URL paths (jestjs#14297)

Allow modules imported and mocked with 'node:*' URLs to be manually mocked by resolving just the path when generating module ID's for setting and checking mocks, and when resolving module names when importing mocks.
@KhaledElmorsy KhaledElmorsy marked this pull request as draft July 6, 2023 08:49
… "node:" URL paths (jestjs#14297)

Allow modules imported and mocked with 'node:*' URLs to be manually mocked by resolving just the path when generating module ID's for setting and checking mocks, and when resolving module names when importing mocks.
@KhaledElmorsy KhaledElmorsy marked this pull request as ready for review July 6, 2023 17:43
@mrazauskas
Copy link
Contributor

mrazauskas commented Jul 8, 2023

Did you check if jest.unmock() works? It uses resolver’s getModuleID() method and this one is not touched in your patch. That’s why I’m asking.

I found a branch where I was play with this fix. Seemed like it was enough to strip 'node:' before running the logic of getMockModule(), getMockModuleAsync(), getModuleID() and getModuleIDAsync() methods. I had no time to write proper tests, so it can be that I missed something. Just wondering why the changes you made are so complex? It felt like the current logic should handle everything if 'node:' is removed.

@KhaledElmorsy
Copy link
Contributor Author

KhaledElmorsy commented Jul 8, 2023

Did you check if jest.unmock() works? It uses resolver’s getModuleID() method and this one is not touched in your patch. That’s why I’m asking.

I found a branch where I was play with this fix. Seemed like it was enough to strip 'node:' before running the logic of getMockModule(), getMockModuleAsync(), getModuleID() and getModuleIDAsync() methods. I had no time to write proper tests, so it can be that I missed something. Just wondering why the changes you made are so complex? It felt like the current logic should handle everything if 'node:' is removed.

I strip node: in Resolver._getAbsolutePath which getModuleID uses and has a branch for core modules, so it felt right to extend the processing in that branch to generalize it to other methods that could use _getAbsolutePath.

The two spots where I strip node: are _getAbsolutePath and in Runtime.requireMock before the moduleName gets passed to getMockModule.

The last change was editing getMockModule(Async) to always return either mocks or null, because the current logic, after not finding a mock with the passed module name returns either the original module name or the result of passing the original module name into resolveStubModuleName. But those two values aren't mocks and force the methods that consume getMockModule to get really convoluted to know what getMockModule returned.

@KhaledElmorsy
Copy link
Contributor Author

@mrazauskas I hope I answered your questions well enough. Is there anything more I should do on this PR?

@mrazauskas
Copy link
Contributor

Thanks for the details and for reminder.

I was trying to look from the distance and to think a bit. These lines made me think:

private _requireCoreModule(moduleName: string, supportPrefix: boolean) {
const moduleWithoutNodePrefix =
supportPrefix && moduleName.startsWith('node:')
? moduleName.slice('node:'.length)
: moduleName;

Now strings are mostly normalized in jest-resolve, but sometimes this will happen in jest-runtime. Also it can happen in higher or in lower layer. Hm.. The code works, of course. But is jest-resolve or jest-runtime broken? In general it seems like they are not. They are just tricked by user input.

What if we would normalize the user input as the first thing at all the entry spots? That would ensure that we pass clean up data to modules which already do the right job.

@mrazauskas
Copy link
Contributor

By the way, you don’t have to force push. Commits are squashed when PRs are merged.

@KhaledElmorsy
Copy link
Contributor Author

I definitely see what you mean in doing it in one place and that would be definite improvement to the codebase. There's a few different methods that take a path as input that would need to changed, like jest.mock, jest.unmock, etc. Some pass it directly to Resolver.getModuleID which normalizes and others like Runtime.requireMockOrModule send it in many different directions. Both would benefit from pre-normalization.

Having the normalization in only the upper Runtime would mean updating those method that pass directly to the resolver and removing its normalization logic. It's doable and should be done but it'll be a lot of changes and needs a good look.

I was actually meaning to replace the snippet you linked with Resolver. normalizeCoreModuleSpecifier and I'm kicking myself for forgetting (good catch).

I think we should push this fix as is, after replacing that snippet, and then implement the more centralized and widely used normalizer in Runtime in a separate PR. Maybe it can have its own open issue so that anyone can jump on that major refactor, if I can't dedicate the time for it.

What do you think?

@KhaledElmorsy
Copy link
Contributor Author

KhaledElmorsy commented Jul 12, 2023

@mrazauskas
Copy link
Contributor

You did not forget. This is why I saw these lines. jest-runtime was build before the node: scheme. Some logic is tweaked a bit, but in general it does not support the node: scheme.

Why not to implement the support in one go? Not sure why do you see this as major refactor? Just a line should be added for each method which take module name string. What is "centralized and widely used normalizer"? Is this those five lines of normalizeCoreModuleSpecifier() method or you have plans to normalize something more?

That was just an idea. Feel free rethink it or ignore it. Also worth mentioning that I don’t have rights to merge PRs. I can only leave comments here and there (;

@KhaledElmorsy
Copy link
Contributor Author

I was thinking of potential normalization for other cases than node:, but I agree, it's probably just that for now.

I gave this some more thought and I think both approaches have merits. If a dozen runtime methods pass to the resolver which normalizes in only two methods with only the snippet method being the outlier in the Runtime, then it's also clean to consolidate it in the resolver like it is. Both approaches are viable, but I'm okay with leaving it is as is.

Also, I really appreciate your comments and insights, it's always good to be met with different approaches to consider and apply.

@KhaledElmorsy
Copy link
Contributor Author

@SimenB could you please approve/merge this PR?

@mrazauskas
Copy link
Contributor

mrazauskas commented Jul 15, 2023

By the way, what happens if jest.mock('node:fs') is called on Node.js v14.15 (the lowest supported by Jest)? Calling require('node:fs') throws an error.

@mrazauskas
Copy link
Contributor

I was experimenting with somewhat bigger idea to add support for URL strings and URL objects. That would work for node: and file: schemas. The change is rather minimal. Curious what you think? See #14332

@KhaledElmorsy
Copy link
Contributor Author

While I haven't seen URLs being used to import modules, if it works, then there's no reason not to support it fully.

I you should pause until this PR is merged because of the refactors & corrections, and then expand the resolution logic it adds to account for URL's as you did. I personally feel like these additions could have been added to this PR given how similar the approaches are.

But still, it's good to think of more general applications and consider the bigger picture, as well finding the most direct way to address issues.

@thedavidprice
Copy link

Hi @KhaledElmorsy @mrazauskas We're blocked on an ESM transition due to #14040 If we come up with a patch, would you like us to try and coordinate between this PR and #14332 or simply proceed independently?

cc @virtuoushub

@mrazauskas
Copy link
Contributor

Not sure I follow. How is the node: prefix related with ESM? Also could you elaborate what is exactly blocking you? There is some inconvenience, but that does not look blocking for me.

@virtuoushub
Copy link

Not sure I follow. How is the node: prefix related with ESM? Also could you elaborate what is exactly blocking you? There is some inconvenience, but that does not look blocking for me.

Not directly related to ESM, but as a precursor to ESM work, I recently opened a PR in redwood to prefer the node protocol. That PR is what is currently blocked.

When importing builtin modules, it's better to use the node: protocol as it makes it perfectly clear that the package is a Node.js builtin module.

see also: https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-node-protocol.md

@tdeekens
Copy link

I am not sure if this relates but I am running into Cannot find module '/[...]/babel.config.js' in Jest test runs. This only happens after the only change is that I refactored a medium size codebase to use the node: protocol.

@mrazauskas
Copy link
Contributor

@tdeekens Hard to say. I don’t think manual mocks need babel.config.js. That can be something else. Try creating minimal reproduction repo, perhaps?

Copy link

This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions bot added the Stale label Nov 30, 2023
@Tobbe
Copy link

Tobbe commented Dec 2, 2023

What's needed to move this PR along?

Copy link

github-actions bot commented Mar 1, 2024

This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions bot added the Stale label Mar 1, 2024
@SimenB SimenB added Pinned and removed Stale labels Mar 1, 2024
@SimenB SimenB added this to the Jest 30 milestone Mar 1, 2024
byCedric added a commit to expo/expo that referenced this pull request Sep 25, 2024
# Why

In #14297, I used `node:` imports in `@expo/json-file`. Unfortunately,
Jest doesn't pick this up when mocking `__mocks__/fs.ts`, it simply
loads `node:fs` instead, see: jestjs/jest#14297

# How

- Added `jest.mock('node:fs')` as workaround until Jest has something
for `node:` mocking

# Test Plan

See CI

# Checklist

<!--
Please check the appropriate items below if they apply to your diff.
This is required for changes to Expo modules.
-->

- [x] Documentation is up to date to reflect these changes (eg:
https://docs.expo.dev and README.md).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants