-
Notifications
You must be signed in to change notification settings - Fork 28.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
module: disallow CJS <-> ESM edges in a cycle from require(esm) #52264
Conversation
Review requested:
|
Just to confirm - can this deal with the case where the require(esm) module is to a parent of a cycle that hasn't been executed yet? That is, the invariant should specifically be that none of the require(esm) module, nor any of its transitive dependencies are currently part of an ESM execution operation. Consider the graph:
Where we import That is, my intuition here would be that a full upfront graph analysis must be done at require(esm) time to ensure acyclic behaviour. |
This patch disallows CJS <-> ESM edges when they come from require(esm) requested in ESM evalaution. Drive-by: don't reuse the cache for imported CJS modules to stash source code of required ESM because the former is also used for cycle detection.
f22f103
to
f414613
Compare
f414613
to
971affd
Compare
Yes this can be detected. Added a test. |
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 for verifying the test case.
function urlToFilename(url) { | ||
if (url && StringPrototypeStartsWith(url, 'file://')) { | ||
return fileURLToPath(url); | ||
} | ||
return url; | ||
} |
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.
We could use that util in more places:
node/lib/internal/modules/esm/translators.js
Line 244 in d6b57f6
return StringPrototypeStartsWith(resolvedURL, 'file://') ? fileURLToPath(resolvedURL) : resolvedURL; |
node/lib/internal/modules/esm/translators.js
Line 266 in d6b57f6
const filename = StringPrototypeStartsWith(url, 'file://') ? fileURLToPath(url) : url; |
Can happen in a follow-up PR.
Landed in db17461 |
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 for this, and especially thanks for the code docs!
Sorry, just realised this review was pending / never got submitted 😞
already being evaluated. | ||
|
||
To avoid the cycle, the `require()` call involved in a cycle should not happen | ||
at the top-level of either a ES Module (via `createRequire()`) or a CommonJS |
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.
at the top-level of either a ES Module (via `createRequire()`) or a CommonJS | |
at the top-level of either an ES Module (via `createRequire()`) or a CommonJS |
parseCachedModule.loaded = true; | ||
// If it's cached by the ESM loader as a way to indirectly pass | ||
// the module in to avoid creating it twice, the loading request | ||
// come from imported CJS. In that case use the importedCJSCache |
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.
// come from imported CJS. In that case use the importedCJSCache | |
// came from imported CJS. In that case use the importedCJSCache |
* @param {import('../cjs/loader.js').Module} mod CJS module wrapper of the ESM. | ||
* @param {string} filename Resolved filename of the module being require()'d | ||
* @param {string} source Source code. TODO(joyeecheung): pass the raw buffer. | ||
* @param {string} isMain Whether this module is a main module. | ||
* @returns {ModuleNamespaceObject} | ||
* @param {import('../cjs/loader.js').Module|undefined} parent Parent module, if any. |
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.
nit: we could DRY this up with a @typedef
// This module job is already created: | ||
// 1. If it was loaded by `require()` before, at this point the instantiation | ||
// is already completed and we can check the whether it is in a cycle | ||
// (in that case the module status is kEvaluaing), and whether the | ||
// required graph is synchronous. | ||
// 2. If it was loaded by `import` before, only allow it if it's already evaluated | ||
// to forbid cycles. | ||
// TODO(joyeecheung): ensure that imported synchronous graphs are evaluated | ||
// synchronously so that any previously imported synchronous graph is already | ||
// evaluated at this point. |
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.
w00t! thanks for this!
} | ||
throw new ERR_REQUIRE_CYCLE_MODULE(message); | ||
} | ||
// Othersie the module could be imported before but the evaluation may be already |
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.
// Othersie the module could be imported before but the evaluation may be already | |
// Otherwise the module could be imported before but the evaluation may be already |
const { responseURL, source } = loadResult; | ||
let { format: finalFormat } = loadResult; | ||
const { format: finalFormat } = loadResult; |
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.
nit: collapsable
const { responseURL, source } = loadResult; | |
let { format: finalFormat } = loadResult; | |
const { format: finalFormat } = loadResult; | |
const { | |
format: finalFormat, | |
responseURL, | |
source, | |
} = loadResult; |
This patch disallows CJS <-> ESM edges when they come from require(esm) requested in ESM evalaution. Drive-by: don't reuse the cache for imported CJS modules to stash source code of required ESM because the former is also used for cycle detection. PR-URL: nodejs#52264 Fixes: nodejs#52145 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This patch disallows CJS <-> ESM cycle edges when they come from require(esm) requested in ESM evalaution.
Drive-by: don't reuse the cache for imported CJS modules to stash source code of required ESM because the former is also used for cycle detection.
Fixes: #52145