-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
esm: remove CLI flag limitation to programmatic registration #48439
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
Changes from 2 commits
28055ff
d4a5dd8
378c5a5
d2ad85b
02c6090
cb72d94
77e91dc
582f50a
ec5d529
e6e8cac
3819d08
9ff1de7
6fa7dc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,16 +11,18 @@ const { | |
} = primordials; | ||
|
||
const { | ||
ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE, | ||
ERR_UNKNOWN_MODULE_FORMAT, | ||
} = require('internal/errors').codes; | ||
const { getOptionValue } = require('internal/options'); | ||
const { pathToFileURL } = require('internal/url'); | ||
const { emitExperimentalWarning } = require('internal/util'); | ||
const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); | ||
const { | ||
getDefaultConditions, | ||
} = require('internal/modules/esm/utils'); | ||
let defaultResolve, defaultLoad, importMetaInitializer; | ||
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { | ||
debug = fn; | ||
}); | ||
|
||
function newModuleMap() { | ||
const ModuleMap = require('internal/modules/esm/module_map'); | ||
|
@@ -99,22 +101,10 @@ class DefaultModuleLoader { | |
source, | ||
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href, | ||
) { | ||
const evalInstance = (url) => { | ||
const { ModuleWrap } = internalBinding('module_wrap'); | ||
const { setCallbackForWrap } = require('internal/modules/esm/utils'); | ||
const module = new ModuleWrap(url, undefined, source, 0, 0); | ||
setCallbackForWrap(module, { | ||
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), | ||
importModuleDynamically: (specifier, { url }, importAssertions) => { | ||
return this.import(specifier, url, importAssertions); | ||
}, | ||
}); | ||
|
||
return module; | ||
}; | ||
const { ModuleWrap } = internalBinding('module_wrap'); | ||
const moduleWrapper = new ModuleWrap(url, undefined, source, 0, 0); | ||
const ModuleJob = require('internal/modules/esm/module_job'); | ||
const job = new ModuleJob( | ||
this, url, undefined, evalInstance, false, false); | ||
const job = new ModuleJob(url, moduleWrapper, undefined, false, false); | ||
this.moduleMap.set(url, undefined, job); | ||
const { module } = await job.run(); | ||
|
||
|
@@ -153,9 +143,7 @@ class DefaultModuleLoader { | |
this.moduleMap.set(url, undefined, job = job()); | ||
} | ||
|
||
if (job === undefined) { | ||
job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format); | ||
} | ||
job ??= this.#createModuleJob(url, resolvedImportAssertions, parentURL, format); | ||
|
||
return job; | ||
} | ||
|
@@ -171,26 +159,7 @@ class DefaultModuleLoader { | |
* `resolve` hook | ||
* @returns {Promise<ModuleJob>} The (possibly pending) module job | ||
*/ | ||
#createModuleJob(url, importAssertions, parentURL, format) { | ||
const moduleProvider = async (url, isMain) => { | ||
const { | ||
format: finalFormat, | ||
responseURL, | ||
source, | ||
} = await this.load(url, { | ||
format, | ||
importAssertions, | ||
}); | ||
|
||
const translator = getTranslators().get(finalFormat); | ||
|
||
if (!translator) { | ||
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); | ||
} | ||
|
||
return FunctionPrototypeCall(translator, this, responseURL, source, isMain); | ||
}; | ||
|
||
async #createModuleJob(url, importAssertions, parentURL, format) { | ||
const inspectBrk = ( | ||
parentURL === undefined && | ||
getOptionValue('--inspect-brk') | ||
|
@@ -200,12 +169,28 @@ class DefaultModuleLoader { | |
process.send({ 'watch:import': [url] }); | ||
} | ||
|
||
const { | ||
format: finalFormat, | ||
responseURL, | ||
source, | ||
} = await this.load(url, { | ||
format, | ||
importAssertions, | ||
}); | ||
|
||
const translator = getTranslators().get(finalFormat); | ||
|
||
if (!translator) { | ||
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); | ||
} | ||
|
||
const moduleWrapper = FunctionPrototypeCall(translator, this, responseURL, source, isMain); | ||
|
||
const ModuleJob = require('internal/modules/esm/module_job'); | ||
const job = new ModuleJob( | ||
this, | ||
url, | ||
moduleWrapper, | ||
importAssertions, | ||
moduleProvider, | ||
parentURL === undefined, | ||
inspectBrk, | ||
); | ||
|
@@ -285,10 +270,21 @@ class CustomizedModuleLoader extends DefaultModuleLoader { | |
/** | ||
* Instantiate a module loader that uses user-provided custom loader hooks. | ||
*/ | ||
constructor() { | ||
constructor({ | ||
cjsCache, | ||
evalIndex, | ||
moduleMap, | ||
} = kEmptyObject) { | ||
super(); | ||
|
||
getHooksProxy(); | ||
if (cjsCache != null) { this.cjsCache = cjsCache; } | ||
if (evalIndex != null) { this.evalIndex = evalIndex; } | ||
if (moduleMap != null) { this.moduleMap = moduleMap; } | ||
|
||
if (!hooksProxy) { | ||
const { HooksProxy } = require('internal/modules/esm/hooks'); | ||
hooksProxy = new HooksProxy(); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -357,40 +353,29 @@ let emittedExperimentalWarning = false; | |
* A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is | ||
* only one used for loading the main module and everything in its dependency graph, though separate instances of this | ||
* class might be instantiated as part of bootstrap for other purposes. | ||
* @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them. | ||
* @param {boolean} forceCustomizedLoaderInMain Ignore whether custom loader(s) have been provided | ||
* via CLI and instantiate a CustomizedModuleLoader instance regardless. | ||
* @returns {DefaultModuleLoader | CustomizedModuleLoader} | ||
*/ | ||
function createModuleLoader(useCustomLoadersIfPresent = true) { | ||
if (useCustomLoadersIfPresent && | ||
// Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader; | ||
// doing so would cause an infinite loop. | ||
!require('internal/modules/esm/utils').isLoaderWorker()) { | ||
const userLoaderPaths = getOptionValue('--experimental-loader'); | ||
if (userLoaderPaths.length > 0) { | ||
function createModuleLoader(customizationSetup) { | ||
// Don't spawn a new worker if we're already in a worker thread (doing so would cause an infinite loop). | ||
if (!require('internal/modules/esm/utils').isLoaderWorker()) { | ||
if ( | ||
customizationSetup || | ||
getOptionValue('--experimental-loader').length > 0 | ||
) { | ||
if (!emittedExperimentalWarning) { | ||
emitExperimentalWarning('Custom ESM Loaders'); | ||
emittedExperimentalWarning = true; | ||
} | ||
return new CustomizedModuleLoader(); | ||
debug('instantiating CustomizedModuleLoader'); | ||
return new CustomizedModuleLoader(customizationSetup); | ||
} | ||
} | ||
|
||
return new DefaultModuleLoader(); | ||
} | ||
|
||
/** | ||
* Get the HooksProxy instance. If it is not defined, then create a new one. | ||
* @returns {HooksProxy} | ||
*/ | ||
function getHooksProxy() { | ||
if (!hooksProxy) { | ||
const { HooksProxy } = require('internal/modules/esm/hooks'); | ||
hooksProxy = new HooksProxy(); | ||
} | ||
|
||
return hooksProxy; | ||
} | ||
|
||
/** | ||
* Register a single loader programmatically. | ||
* @param {string} specifier | ||
|
@@ -405,19 +390,19 @@ function getHooksProxy() { | |
* ``` | ||
*/ | ||
function register(specifier, parentURL = 'data:') { | ||
// TODO: Remove this limitation in a follow-up before `register` is released publicly | ||
if (getOptionValue('--experimental-loader').length < 1) { | ||
throw new ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE(); | ||
} | ||
let moduleLoader = require('internal/process/esm_loader').esmLoader; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Slack we were discussing creating a function Also in general this PR isn’t ready for review yet, I think it’s just where @JakobJingleheimer finished at the end of the night. It doesn’t work yet, and we’re not sure which of the two approaches described in the top post will work or be more readable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it's a draft. Not sure why GitHub tagged people for review. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the bot who does the tagging, not GitHub. The webhook is executed when you open a PR, regardless of its status. You probably already know that, but you don't have to open a PR, you can also push commits to your forks and open a PR later if you don't want comments for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, it doesn't have to be configured that way (at work, we have the same kind of thing, and it only runs when the PR is not a draft—eg opened as non-draft or switches from draft to non-draft). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn’t realize there was a conversation was going on Slack. I mostly left these comments because I’m trying to familiarize myself with the loader implementation and I believe asking/commenting is the best way of learning. Sorry for any disturbance caused by my comments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just meant this isn't ready for cleanup notes yet. We need to figure out which approach to take and get it working. If you have suggestions for that (see initial post) please feel free 😀 |
||
|
||
const moduleLoader = require('internal/process/esm_loader').esmLoader; | ||
if (!(moduleLoader instanceof CustomizedModuleLoader)) { | ||
debug('register called on DefaultModuleLoader; switching to CustomizedModuleLoader'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit:
|
||
moduleLoader = createModuleLoader(moduleLoader); | ||
require('internal/process/esm_loader').esmLoader = moduleLoader; | ||
} | ||
|
||
moduleLoader.register(`${specifier}`, parentURL); | ||
} | ||
|
||
module.exports = { | ||
DefaultModuleLoader, | ||
createModuleLoader, | ||
getHooksProxy, | ||
register, | ||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -8,7 +8,6 @@ const { | |||||
ObjectSetPrototypeOf, | ||||||
PromiseResolve, | ||||||
PromisePrototypeThen, | ||||||
ReflectApply, | ||||||
RegExpPrototypeExec, | ||||||
RegExpPrototypeSymbolReplace, | ||||||
SafePromiseAllReturnArrayLike, | ||||||
|
@@ -20,13 +19,17 @@ const { | |||||
} = primordials; | ||||||
|
||||||
const { ModuleWrap } = internalBinding('module_wrap'); | ||||||
const { setCallbackForWrap } = require('internal/modules/esm/utils'); | ||||||
|
||||||
const { decorateErrorStack } = require('internal/util'); | ||||||
const { | ||||||
getSourceMapsEnabled, | ||||||
} = require('internal/source_map/source_map_cache'); | ||||||
const assert = require('internal/assert'); | ||||||
const resolvedPromise = PromiseResolve(); | ||||||
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { | ||||||
debug = fn; | ||||||
}); | ||||||
|
||||||
const noop = FunctionPrototype; | ||||||
|
||||||
|
@@ -45,22 +48,43 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) => | |||||
(globalLike) => errorMessage === `${globalLike} is not defined`, | ||||||
); | ||||||
|
||||||
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of | ||||||
* its dependencies, over time. */ | ||||||
/** | ||||||
* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of its dependencies, over | ||||||
* time. | ||||||
*/ | ||||||
class ModuleJob { | ||||||
// `loader` is the Loader instance used for loading dependencies. | ||||||
// `moduleProvider` is a function | ||||||
constructor(loader, url, importAssertions = { __proto__: null }, | ||||||
moduleProvider, isMain, inspectBrk) { | ||||||
this.loader = loader; | ||||||
/** | ||||||
* Deep dependency jobs wrappers are instantiated, and module wrapper is instantiated. | ||||||
*/ | ||||||
instantiated; | ||||||
|
||||||
module; | ||||||
|
||||||
constructor( | ||||||
url, | ||||||
moduleWrapper, | ||||||
importAssertions = { __proto__: null }, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The import attributes may be modified by a user loader, so using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed multiple args to the constructor, so it wasn't just fixing the whitespace mangling. If you'd like the mangling fixed separately, I can re-mangle the 2 lines and fix in a follow-up (but the purpose of this PR is also clean-up that was deferred from the previous PR). |
||||||
isMain, | ||||||
inspectBrk, | ||||||
) { | ||||||
this.importAssertions = importAssertions; | ||||||
this.isMain = isMain; | ||||||
this.inspectBrk = inspectBrk; | ||||||
this.modulePromise = moduleWrapper; | ||||||
|
||||||
debug('new ModuleJob(%o)', { url, importAssertions, isMain, inspectBrk }); | ||||||
|
||||||
this.module = undefined; | ||||||
// Expose the promise to the ModuleWrap directly for linking below. | ||||||
// `this.module` is also filled in below. | ||||||
this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); | ||||||
|
||||||
setCallbackForWrap(this.modulePromise, { | ||||||
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this does not matter here: we provide and destructure known keys, so it would never reach the prototype. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that would mean it doesn't matter now, but if anyone ever read a new key in the future without adding that key to every single callsite, it'd be a vulnerability. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's valid but a larger issue, that I think is out of scope here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any advantage to not coding defensively here, especially in the module system? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe @aduh95 recently said it's a de-op There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say it can be a de-op only if you sometimes pass objects with null prototype and sometimes don't. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let’s please not haggle over cleanup stuff yet. We need to get this PR working first. Also isn’t there a lint rule for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The linter rule only enforces which style is used for null objects - it doesn't enforce that null objects are used. Totally fair tho to not haggle over cleanup stuff yet; there's no rush to address this. |
||||||
importModuleDynamically: (specifier, { url }, importAssertions) => { | ||||||
const moduleLoader = require('internal/process/esm_loader').esmLoader; | ||||||
debug('importModuleDynamically: %o', { specifier, url, moduleLoader }); | ||||||
return moduleLoader.import(specifier, url, importAssertions); | ||||||
}, | ||||||
}); | ||||||
JakobJingleheimer marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
// Wait for the ModuleWrap instance being linked with all dependencies. | ||||||
const link = async () => { | ||||||
|
@@ -73,7 +97,8 @@ class ModuleJob { | |||||
// these `link` callbacks depending on each other. | ||||||
const dependencyJobs = []; | ||||||
const promises = this.module.link((specifier, assertions) => { | ||||||
const job = this.loader.getModuleJob(specifier, url, assertions); | ||||||
const moduleLoader = require('internal/process/esm_loader').esmLoader; | ||||||
anonrig marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
const job = moduleLoader.getModuleJob(specifier, url, assertions); | ||||||
ArrayPrototypePush(dependencyJobs, job); | ||||||
return job.modulePromise; | ||||||
}); | ||||||
|
@@ -88,16 +113,11 @@ class ModuleJob { | |||||
// This promise is awaited later anyway, so silence | ||||||
// 'unhandled rejection' warnings. | ||||||
PromisePrototypeThen(this.linked, undefined, noop); | ||||||
|
||||||
// instantiated == deep dependency jobs wrappers are instantiated, | ||||||
// and module wrapper is instantiated. | ||||||
this.instantiated = undefined; | ||||||
} | ||||||
|
||||||
instantiate() { | ||||||
if (this.instantiated === undefined) { | ||||||
this.instantiated = this._instantiate(); | ||||||
} | ||||||
this.instantiated ??= this._instantiate(); | ||||||
|
||||||
return this.instantiated; | ||||||
} | ||||||
|
||||||
|
@@ -139,16 +159,16 @@ class ModuleJob { | |||||
const { 1: childSpecifier, 2: name } = RegExpPrototypeExec( | ||||||
/module '(.*)' does not provide an export named '(.+)'/, | ||||||
e.message); | ||||||
const { url: childFileURL } = await this.loader.resolve( | ||||||
const moduleLoader = require('internal/process/esm_loader').esmLoader; | ||||||
const { url: childFileURL } = await moduleLoader.resolve( | ||||||
childSpecifier, parentFileUrl, | ||||||
); | ||||||
let format; | ||||||
try { | ||||||
// This might throw for non-CommonJS modules because we aren't passing | ||||||
// in the import assertions and some formats require them; but we only | ||||||
// care about CommonJS for the purposes of this error message. | ||||||
({ format } = | ||||||
await this.loader.load(childFileURL)); | ||||||
({ format } = await moduleLoader.load(childFileURL)); | ||||||
} catch { | ||||||
// Continue regardless of error. | ||||||
} | ||||||
|
Uh oh!
There was an error while loading. Please reload this page.