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

Explain dual-instantiation #11

Merged
merged 4 commits into from
Jan 21, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,18 +281,14 @@ To preserve backward compatibility, we expect that `node file.js` will continue

A package can be “dual mode” if its `package.json` contains both a `"main"` field and an `"exports"` field (or some other ESM-signifying field). An `import` statement of such a package will treat the package as ESM and ignore the `"main"` field. To explicitly import a dual-mode package via its CommonJS entry point, [`module.createRequireFromPath`][nodejs-docs-modules-create-require-from-path] could be used.

The ESM and CommonJS versions of a dual-mode package are really two distinct packages, and would be treated as such by Node if both were imported. It is an implementation detail to be worked out what Node should do in such a situation. Of particular concern are race conditions that could occur if it might be unpredictable which version (ESM or CommonJS) of a dual-mode package gets loaded first.
The ESM and CommonJS module systems have independent namespaces. Therefore the ESM and CommonJS versions of a file have separate identities when instantiated. The module loader's previous invariant was: a file will only be loaded at-most once within a Node application. This invariant now becomes: a version of a file (ESM or CommonJS) will only be loaded at-most once within a Node application.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused on "previous invariant". What invariant? Also, are we talking purely about how CJS works, because the loader does not guarantee a file on disk is only loaded once (throw on require/case insensitive filesystems/removal from require.cache).

Also, unclear on if

a version of a file (ESM or CommonJS) will only be loaded at-most once within a Node application.

Is trying to state that a file/url can only be loaded once and only in either CJS or ESM but not in both. It seems unlikely that we can stop CJS from loading all sorts of things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification. I have attempted a fix.

I'm trying to refer to the resolved specifier identity that gets used as a key in the virtual registry of all instantiated modules (CJS and ESM). This key is effectively being expanded to include a bit that indicates whether the module is CJS or ESM. Maybe you can guide me on the precise terminology.

An alternative mental model (not written up) would be view them as distinct registries (ESM & CJS) and say that require only operates on the CJS registry whereas import/import() can operate on both registries.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2 "Registry"s of sorts, the Global Module Map in ESM and require.cache in CJS. They are not intertwined in part due to the ecosystem reliance on require.cache which is not compatible with how ESM modules are keyed or cached.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reworded in terms of two registries. Please let me know if this is better.


Users that only rely on Node's interpretation of a module, via `import` and `import()`, will never trigger simultaneous instantiation of both versions. Today, the only way to encounter this dual-instantiation scenario is if some part of the application uses `import`/`import()` to load a module **and** some other part of the application overrides Node's interpretation by using `require()` or [`module.createRequireFromPath`][nodejs-docs-modules-create-require-from-path] to load that same module.
robpalme marked this conversation as resolved.
Show resolved Hide resolved

Different modes of the same package _should_ be importable into different package scopes, for example if a user’s project imports ESM `lodash` and that project has a dependency which itself imports CommonJS `lodash`. This corresponds with how different package scopes today can import different versions of the same package, such a user’s project having dependencies of `lodash@2` and `request`, with `request` then having a dependency of `lodash@1`, and both the user’s project and `request` each receive the version of `lodash` that they expect.
The choice to allow dual-instantiation was made to provide well-defined determinstic behaviour. Alternative behaviours, such as throwing a runtime exception upon encountering the scenario, were deemed brittle and likely to cause user frustration. Nevertheless, dual instantiation is not an encouraged pattern. Users should ideally avoid dual-instantiation by migrating consumers away from `require` to use `import` or `import()`.

#### Further Considerations

<details><summary>“Double importing” of files</summary>

There is the possibility of `import` and `createRequireFromPath` both importing the same file into the same package scope, potentially the former as ESM and the latter as CommonJS. Allowing this would likely cause issues, and a solution would need to be worked out to handle this situation.

</details>

<details><summary>“Loose” CommonJS files (files outside of packages)</summary>

Currently, `module.createRequireFromPath` can be used to import CommonJS files that aren’t inside a CommonJS package scope. To import the file via an `import` statement, a symlink could also be created from inside a CommonJS package scope to the desired “loose” CommonJS file, or the file could simply be moved inside a CommonJS package scope. Seeing as there is low user demand for ESM files importing CommonJS files outside of CommonJS packages, we feel that these options are sufficient.
Expand Down