-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
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
feat: override Module#_compile #10072
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've glanced over this without giving it a detailed review yet. Let's talk about the general approach first. IIUC you are making sure that the explicitly compiled module does not collide with a really existing file path by loading it in an isolated registry context? I think to avoid edge cases like the one you already predicted, a better approach would be having a code path that does not mutate the runtime state in the first place, which we can then use for _compile
.
'exports.value = 12;', | ||
`${runtime.__mockRootPath}/dynamic.js`, | ||
); | ||
expect(module.exports).toEqual({value: 12}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this actually verify that the module was run in the runtime, with access to the environment etc.?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It really just exists to verify that it doesn't break basic functionality. The other test does verify that the module was run in the runtime because it verifies that jest
is available. I do intend to add another test that checks that jsdom is present, just as soon as I figure out how to do that in this test setup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I started with that approach, but realized that it wouldn't support certain cyclic module reference patterns like |
I would try to be like Node as much as possible. In Node,
prints
So the compiled |
I'm on board with that! I think I had a bad assumption about Node's default behavior and had been assuming that the new import { Module } from 'module';
const m = new Module();
require.cache = id;
m._compile(fs.readFileSync(id, 'utf-8'), id); So that if // x.js
require('./y');
// y.js
// This would mutate a new copy of x.js in the ModuleRegistry, rather than
// getting a reference to the x.js that loaded y.js.
require('./x.js').value = 'something'; |
The only implementation detail I'm a little hung up on is the EDIT: ok, put together a solution that seems reasonable; let me know if the |
f3b9ae6
to
869f8f3
Compare
Hook into the jest module loading mechanisms for the `Module#_compile` functionality provided by Node. Fixes jestjs#10069.
869f8f3
to
24938b2
Compare
Ready for review minus the appropriate tests for verifying test environment. Would love some guidance there - not sure how to control |
// Skip cache for core and dynamically loaded modules. | ||
if (!options.isCoreModule && typeof options.filenameOverride !== 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The alternative here is to explicitly handle the case that the file doesn't exist when calling statSync
in getScriptCacheKey
, but it seemed best to just skip the cache for dynamically loaded modules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah def not the stat solution, there could still be a regular file for the path of the module that was compiled. Can we not use the regular filename arg and just flag it as dynamically compiled in the options though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also goes for all the filenameOverride ?? from.filename
places - if we set the module's filename
when it is compiled, can we not use that and just flag it as dynamically compiled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although I guess that would deviate from Node, where the filename
is always null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that was my concern.
Regarding |
packages/jest-runtime/src/index.ts
Outdated
@@ -950,16 +957,17 @@ class Runtime { | |||
|
|||
private _execModule( | |||
localModule: InitialModule, | |||
moduleSource: string | undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name should probably indicate that this is only for modules explicitly compiled, not on the regular code path where the module is actually loaded from disk
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about runtimeModuleSource
? dynamicModuleSource
would work too but that starts to sound reminiscent of dynamic imports.
// Skip cache for core and dynamically loaded modules. | ||
if (!options.isCoreModule && typeof options.filenameOverride !== 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah def not the stat solution, there could still be a regular file for the path of the module that was compiled. Can we not use the regular filename arg and just flag it as dynamically compiled in the options though?
// Skip cache for core and dynamically loaded modules. | ||
if (!options.isCoreModule && typeof options.filenameOverride !== 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also goes for all the filenameOverride ?? from.filename
places - if we set the module's filename
when it is compiled, can we not use that and just flag it as dynamically compiled?
// Skip cache for core and dynamically loaded modules. | ||
if (!options.isCoreModule && typeof options.filenameOverride !== 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although I guess that would deviate from Node, where the filename
is always null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! As a sanity check, could you add an integration test as well that works as you want using _compile
and getting the correct env etc on the inside?
packages/jest-runtime/src/__tests__/runtime_compile_module.test.js
Outdated
Show resolved
Hide resolved
packages/jest-runtime/src/__tests__/runtime_compile_module.test.js
Outdated
Show resolved
Hide resolved
packages/jest-runtime/src/index.ts
Outdated
private _execModule( | ||
localModule: InitialModule, | ||
runtimeModuleSource: string | undefined, | ||
options: InternalModuleOptions | undefined, | ||
moduleRegistry: ModuleRegistry, | ||
from: Config.Path | null, | ||
) { | ||
): any | undefined { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private _execModule<T = unknown>(
localModule: InitialModule,
runtimeModuleSource: string | undefined,
options: InternalModuleOptions | undefined,
moduleRegistry: ModuleRegistry,
from: Config.Path | null,
) {
): T | undefined {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gets me an error from tsc
-
packages/jest-runtime/src/index.ts:1062:7 - error TS2322: Type 'unknown' is not assignable to type 'T | undefined'.
Type 'unknown' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.
1062 return compiledFunction.call(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1063 localModule.exports,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...
1079 }),
~~~~~~~~~~~
1080 );
~~~~~~~~
What do you recommend here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do as T
at the 1080 line to cast
// should we implement the class ourselves? | ||
class Module extends nativeModule.Module {} | ||
class Module extends nativeModule.Module { | ||
_compile(content: string, filename: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_compile(content: string, filename: string) { | |
_compile(content: unknown, filename: unknown) { |
I believe your typeof
s narrow the type correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really familiar with Typescript - I'm guessing that it'll infer the type based on type guards? This feels like a convention I'm not aware of - wouldn't we rather have the types be explicit for readability?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's more honest since we get it from user input. And the typeof
s will narrow it to string
} | ||
|
||
return runtime._execModule( | ||
this, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jest-runtime
's internal InitialModule
is not instanceof Module
, I believe? It probably should, but if not (and it's hard to make it so) I think constructing a new object here rather than passing this
makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're suggesting we make a fake Module
instead, and provide that to the application code instead of leveraging the existing jest-runtime
-specific instance? That seems reasonable to me, and might (?) fix the issues around filename
and filenameOverride
that @jeysal identified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current jest-runtime
one should be Module
ideally, yeah, then reused here. I think? 😅
Co-authored-by: Simen Bekkhus <sbekkhus91@gmail.com>
Co-authored-by: Simen Bekkhus <sbekkhus91@gmail.com>
Co-authored-by: Simen Bekkhus <sbekkhus91@gmail.com>
This PR is stale because it has been open 1 year with no activity. Remove stale label or comment or this will be closed in 30 days. |
This PR was closed because it has been stalled for 30 days with no activity. Please open a new PR if the issue is still relevant, linking to this one. |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Hook into the jest module loading mechanisms for the
Module#_compile
functionality provided by Node. Fixes #10069.Summary
Provides a consistent experience when interacting with uncommon features that load modules. See #10069.
Test plan
See unit tests.
This is probably incomplete; some of the edge cases of the semantics aren't clear to me, such as what should happen with the isolated registry when the
require
call happens outside the context of theisolateModules
call.