-
Notifications
You must be signed in to change notification settings - Fork 30.6k
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
lib: refactor ES module loader for readability (JS side) #16579
Changes from all commits
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 |
---|---|---|
@@ -1,91 +1,93 @@ | ||
'use strict'; | ||
|
||
const { ModuleWrap } = internalBinding('module_wrap'); | ||
const { SafeSet, SafePromise } = require('internal/safe_globals'); | ||
const assert = require('assert'); | ||
const resolvedPromise = SafePromise.resolve(); | ||
|
||
const enableDebug = (process.env.NODE_DEBUG || '').match(/\besm\b/) || | ||
process.features.debug; | ||
|
||
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of | ||
* its dependencies, over time. */ | ||
class ModuleJob { | ||
/** | ||
* @param {module: ModuleWrap?, compiled: Promise} moduleProvider | ||
*/ | ||
// `loader` is the Loader instance used for loading dependencies. | ||
// `moduleProvider` is a function | ||
constructor(loader, url, moduleProvider) { | ||
this.loader = loader; | ||
this.error = null; | ||
this.hadError = false; | ||
|
||
// linked == promise for dependency jobs, with module populated, | ||
// module wrapper linked | ||
this.moduleProvider = moduleProvider; | ||
this.modulePromise = this.moduleProvider(url); | ||
// This is a Promise<{ module, reflect }>, whose fields will be copied | ||
// onto `this` by `link()` below once it has been resolved. | ||
this.modulePromise = moduleProvider(url); | ||
this.module = undefined; | ||
this.reflect = undefined; | ||
const linked = async () => { | ||
|
||
// Wait for the ModuleWrap instance being linked with all dependencies. | ||
const link = async () => { | ||
const dependencyJobs = []; | ||
({ module: this.module, | ||
reflect: this.reflect } = await this.modulePromise); | ||
assert(this.module instanceof ModuleWrap); | ||
this.module.link(async (dependencySpecifier) => { | ||
const dependencyJobPromise = | ||
this.loader.getModuleJob(dependencySpecifier, url); | ||
dependencyJobs.push(dependencyJobPromise); | ||
const dependencyJob = await dependencyJobPromise; | ||
return (await dependencyJob.modulePromise).module; | ||
}); | ||
if (enableDebug) { | ||
// Make sure all dependencies are entered into the list synchronously. | ||
Object.freeze(dependencyJobs); | ||
} | ||
return SafePromise.all(dependencyJobs); | ||
}; | ||
this.linked = linked(); | ||
// Promise for the list of all dependencyJobs. | ||
this.linked = link(); | ||
|
||
// instantiated == deep dependency jobs wrappers instantiated, | ||
// module wrapper instantiated | ||
this.instantiated = undefined; | ||
} | ||
|
||
instantiate() { | ||
async instantiate() { | ||
if (this.instantiated) { | ||
return this.instantiated; | ||
} | ||
return this.instantiated = new Promise(async (resolve, reject) => { | ||
const jobsInGraph = new SafeSet(); | ||
let jobsReadyToInstantiate = 0; | ||
// (this must be sync for counter to work) | ||
const queueJob = (moduleJob) => { | ||
if (jobsInGraph.has(moduleJob)) { | ||
return; | ||
} | ||
jobsInGraph.add(moduleJob); | ||
moduleJob.linked.then((dependencyJobs) => { | ||
for (const dependencyJob of dependencyJobs) { | ||
queueJob(dependencyJob); | ||
} | ||
checkComplete(); | ||
}, (e) => { | ||
if (!this.hadError) { | ||
this.error = e; | ||
this.hadError = true; | ||
} | ||
checkComplete(); | ||
}); | ||
}; | ||
const checkComplete = () => { | ||
if (++jobsReadyToInstantiate === jobsInGraph.size) { | ||
// I believe we only throw once the whole tree is finished loading? | ||
// or should the error bail early, leaving entire tree to still load? | ||
if (this.hadError) { | ||
reject(this.error); | ||
} else { | ||
try { | ||
this.module.instantiate(); | ||
for (const dependencyJob of jobsInGraph) { | ||
dependencyJob.instantiated = resolvedPromise; | ||
} | ||
resolve(this.module); | ||
} catch (e) { | ||
e.stack; | ||
reject(e); | ||
} | ||
} | ||
} | ||
}; | ||
queueJob(this); | ||
}); | ||
return this.instantiated = this._instantiate(); | ||
} | ||
|
||
// This method instantiates the module associated with this job and its | ||
// entire dependency graph, i.e. creates all the module namespaces and the | ||
// exported/imported variables. | ||
async _instantiate() { | ||
const jobsInGraph = new SafeSet(); | ||
|
||
const addJobsToDependencyGraph = async (moduleJob) => { | ||
if (jobsInGraph.has(moduleJob)) { | ||
return; | ||
} | ||
jobsInGraph.add(moduleJob); | ||
const dependencyJobs = await moduleJob.linked; | ||
return Promise.all(dependencyJobs.map(addJobsToDependencyGraph)); | ||
}; | ||
try { | ||
await addJobsToDependencyGraph(this); | ||
} catch (e) { | ||
if (!this.hadError) { | ||
this.error = e; | ||
this.hadError = true; | ||
} | ||
throw e; | ||
} | ||
this.module.instantiate(); | ||
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. Amazing to see this simplification. Not having to call instantiate for each module in the right order is a much nicer v8 API! 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. Just in case it wasn’t clear, we only had one call to 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. Huh, it seems I was simply trying to post-justify my less elegant approach :) 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. Heh, didn’t know this was your code ;) |
||
for (const dependencyJob of jobsInGraph) { | ||
// Calling `this.module.instantiate()` instantiates not only the | ||
// ModuleWrap in this module, but all modules in the graph. | ||
dependencyJob.instantiated = resolvedPromise; | ||
} | ||
return this.module; | ||
} | ||
|
||
async run() { | ||
|
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.
Your comment makes me wonder if this would be better called
getOrCreateModuleJob
.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 think this is fine,
get
kind of leaves the details open of how the returned module job got into existence :)