lib: refactor ES module loader for readability
PR-URL: https://github.com/nodejs/node/pull/16579 Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
parent
e22a8f17a5
commit
14181a3368
@ -1207,6 +1207,14 @@ for strict compliance with the API specification (which in some cases may accept
|
|||||||
`func(undefined)` and `func()` are treated identically, and the
|
`func(undefined)` and `func()` are treated identically, and the
|
||||||
[`ERR_INVALID_ARG_TYPE`][] error code may be used instead.
|
[`ERR_INVALID_ARG_TYPE`][] error code may be used instead.
|
||||||
|
|
||||||
|
<a id="ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK"></a>
|
||||||
|
### ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK
|
||||||
|
|
||||||
|
> Stability: 1 - Experimental
|
||||||
|
|
||||||
|
Used when an [ES6 module][] loader hook specifies `format: 'dynamic` but does
|
||||||
|
not provide a `dynamicInstantiate` hook.
|
||||||
|
|
||||||
<a id="ERR_MISSING_MODULE"></a>
|
<a id="ERR_MISSING_MODULE"></a>
|
||||||
### ERR_MISSING_MODULE
|
### ERR_MISSING_MODULE
|
||||||
|
|
||||||
|
@ -376,6 +376,9 @@ E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe');
|
|||||||
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks');
|
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks');
|
||||||
E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented');
|
E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented');
|
||||||
E('ERR_MISSING_ARGS', missingArgs);
|
E('ERR_MISSING_ARGS', missingArgs);
|
||||||
|
E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK',
|
||||||
|
'The ES Module loader may not return a format of \'dynamic\' when no ' +
|
||||||
|
'dynamicInstantiate function was provided');
|
||||||
E('ERR_MISSING_MODULE', 'Cannot find module %s');
|
E('ERR_MISSING_MODULE', 'Cannot find module %s');
|
||||||
E('ERR_MODULE_RESOLUTION_LEGACY', '%s not found by import in %s.' +
|
E('ERR_MODULE_RESOLUTION_LEGACY', '%s not found by import in %s.' +
|
||||||
' Legacy behavior in require() would have found it at %s');
|
' Legacy behavior in require() would have found it at %s');
|
||||||
|
@ -10,7 +10,8 @@ const ModuleRequest = require('internal/loader/ModuleRequest');
|
|||||||
const errors = require('internal/errors');
|
const errors = require('internal/errors');
|
||||||
const debug = require('util').debuglog('esm');
|
const debug = require('util').debuglog('esm');
|
||||||
|
|
||||||
function getBase() {
|
// Returns a file URL for the current working directory.
|
||||||
|
function getURLStringForCwd() {
|
||||||
try {
|
try {
|
||||||
return getURLFromFilePath(`${process.cwd()}/`).href;
|
return getURLFromFilePath(`${process.cwd()}/`).href;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -23,22 +24,44 @@ function getBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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. */
|
||||||
class Loader {
|
class Loader {
|
||||||
constructor(base = getBase()) {
|
constructor(base = getURLStringForCwd()) {
|
||||||
this.moduleMap = new ModuleMap();
|
|
||||||
if (typeof base !== 'string') {
|
if (typeof base !== 'string') {
|
||||||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string');
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.moduleMap = new ModuleMap();
|
||||||
this.base = base;
|
this.base = base;
|
||||||
this.resolver = ModuleRequest.resolve.bind(null);
|
// The resolver has the signature
|
||||||
|
// (specifier : string, parentURL : string, defaultResolve)
|
||||||
|
// -> Promise<{ url : string,
|
||||||
|
// format: anything in Loader.validFormats }>
|
||||||
|
// where defaultResolve is ModuleRequest.resolve (having the same
|
||||||
|
// signature itself).
|
||||||
|
// If `.format` on the returned value is 'dynamic', .dynamicInstantiate
|
||||||
|
// will be used as described below.
|
||||||
|
this.resolver = ModuleRequest.resolve;
|
||||||
|
// This hook is only called when resolve(...).format is 'dynamic' and has
|
||||||
|
// the signature
|
||||||
|
// (url : string) -> Promise<{ exports: { ... }, execute: function }>
|
||||||
|
// Where `exports` is an object whose property names define the exported
|
||||||
|
// names of the generated module. `execute` is a function that receives
|
||||||
|
// an object with the same keys as `exports`, whose values are get/set
|
||||||
|
// functions for the actual exported values.
|
||||||
this.dynamicInstantiate = undefined;
|
this.dynamicInstantiate = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) {
|
hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) {
|
||||||
|
// Use .bind() to avoid giving access to the Loader instance when it is
|
||||||
|
// called as this.resolver(...);
|
||||||
this.resolver = resolve.bind(null);
|
this.resolver = resolve.bind(null);
|
||||||
this.dynamicInstantiate = dynamicInstantiate;
|
this.dynamicInstantiate = dynamicInstantiate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Typechecking wrapper around .resolver().
|
||||||
async resolve(specifier, parentURL = this.base) {
|
async resolve(specifier, parentURL = this.base) {
|
||||||
if (typeof parentURL !== 'string') {
|
if (typeof parentURL !== 'string') {
|
||||||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
|
||||||
@ -48,10 +71,11 @@ class Loader {
|
|||||||
const { url, format } = await this.resolver(specifier, parentURL,
|
const { url, format } = await this.resolver(specifier, parentURL,
|
||||||
ModuleRequest.resolve);
|
ModuleRequest.resolve);
|
||||||
|
|
||||||
if (typeof format !== 'string') {
|
if (!Loader.validFormats.includes(format)) {
|
||||||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format',
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format',
|
||||||
['esm', 'cjs', 'builtin', 'addon', 'json']);
|
Loader.validFormats);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof url !== 'string') {
|
if (typeof url !== 'string') {
|
||||||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string');
|
||||||
}
|
}
|
||||||
@ -72,14 +96,20 @@ class Loader {
|
|||||||
return { url, format };
|
return { url, format };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// May create a new ModuleJob instance if one did not already exist.
|
||||||
async getModuleJob(specifier, parentURL = this.base) {
|
async getModuleJob(specifier, parentURL = this.base) {
|
||||||
const { url, format } = await this.resolve(specifier, parentURL);
|
const { url, format } = await this.resolve(specifier, parentURL);
|
||||||
let job = this.moduleMap.get(url);
|
let job = this.moduleMap.get(url);
|
||||||
if (job === undefined) {
|
if (job === undefined) {
|
||||||
let loaderInstance;
|
let loaderInstance;
|
||||||
if (format === 'dynamic') {
|
if (format === 'dynamic') {
|
||||||
|
const { dynamicInstantiate } = this;
|
||||||
|
if (typeof dynamicInstantiate !== 'function') {
|
||||||
|
throw new errors.Error('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK');
|
||||||
|
}
|
||||||
|
|
||||||
loaderInstance = async (url) => {
|
loaderInstance = async (url) => {
|
||||||
const { exports, execute } = await this.dynamicInstantiate(url);
|
const { exports, execute } = await dynamicInstantiate(url);
|
||||||
return createDynamicModule(exports, url, (reflect) => {
|
return createDynamicModule(exports, url, (reflect) => {
|
||||||
debug(`Loading custom loader ${url}`);
|
debug(`Loading custom loader ${url}`);
|
||||||
execute(reflect.exports);
|
execute(reflect.exports);
|
||||||
@ -100,5 +130,6 @@ class Loader {
|
|||||||
return module.namespace();
|
return module.namespace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic'];
|
||||||
Object.setPrototypeOf(Loader.prototype, null);
|
Object.setPrototypeOf(Loader.prototype, null);
|
||||||
module.exports = Loader;
|
module.exports = Loader;
|
||||||
|
@ -1,27 +1,35 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { ModuleWrap } = internalBinding('module_wrap');
|
||||||
const { SafeSet, SafePromise } = require('internal/safe_globals');
|
const { SafeSet, SafePromise } = require('internal/safe_globals');
|
||||||
|
const assert = require('assert');
|
||||||
const resolvedPromise = SafePromise.resolve();
|
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 {
|
class ModuleJob {
|
||||||
/**
|
// `loader` is the Loader instance used for loading dependencies.
|
||||||
* @param {module: ModuleWrap?, compiled: Promise} moduleProvider
|
// `moduleProvider` is a function
|
||||||
*/
|
|
||||||
constructor(loader, url, moduleProvider) {
|
constructor(loader, url, moduleProvider) {
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.hadError = false;
|
this.hadError = false;
|
||||||
|
|
||||||
// linked == promise for dependency jobs, with module populated,
|
// This is a Promise<{ module, reflect }>, whose fields will be copied
|
||||||
// module wrapper linked
|
// onto `this` by `link()` below once it has been resolved.
|
||||||
this.moduleProvider = moduleProvider;
|
this.modulePromise = moduleProvider(url);
|
||||||
this.modulePromise = this.moduleProvider(url);
|
|
||||||
this.module = undefined;
|
this.module = undefined;
|
||||||
this.reflect = undefined;
|
this.reflect = undefined;
|
||||||
const linked = async () => {
|
|
||||||
|
// Wait for the ModuleWrap instance being linked with all dependencies.
|
||||||
|
const link = async () => {
|
||||||
const dependencyJobs = [];
|
const dependencyJobs = [];
|
||||||
({ module: this.module,
|
({ module: this.module,
|
||||||
reflect: this.reflect } = await this.modulePromise);
|
reflect: this.reflect } = await this.modulePromise);
|
||||||
|
assert(this.module instanceof ModuleWrap);
|
||||||
this.module.link(async (dependencySpecifier) => {
|
this.module.link(async (dependencySpecifier) => {
|
||||||
const dependencyJobPromise =
|
const dependencyJobPromise =
|
||||||
this.loader.getModuleJob(dependencySpecifier, url);
|
this.loader.getModuleJob(dependencySpecifier, url);
|
||||||
@ -29,63 +37,57 @@ class ModuleJob {
|
|||||||
const dependencyJob = await dependencyJobPromise;
|
const dependencyJob = await dependencyJobPromise;
|
||||||
return (await dependencyJob.modulePromise).module;
|
return (await dependencyJob.modulePromise).module;
|
||||||
});
|
});
|
||||||
|
if (enableDebug) {
|
||||||
|
// Make sure all dependencies are entered into the list synchronously.
|
||||||
|
Object.freeze(dependencyJobs);
|
||||||
|
}
|
||||||
return SafePromise.all(dependencyJobs);
|
return SafePromise.all(dependencyJobs);
|
||||||
};
|
};
|
||||||
this.linked = linked();
|
// Promise for the list of all dependencyJobs.
|
||||||
|
this.linked = link();
|
||||||
|
|
||||||
// instantiated == deep dependency jobs wrappers instantiated,
|
// instantiated == deep dependency jobs wrappers instantiated,
|
||||||
// module wrapper instantiated
|
// module wrapper instantiated
|
||||||
this.instantiated = undefined;
|
this.instantiated = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
instantiate() {
|
async instantiate() {
|
||||||
if (this.instantiated) {
|
if (this.instantiated) {
|
||||||
return this.instantiated;
|
return this.instantiated;
|
||||||
}
|
}
|
||||||
return this.instantiated = new Promise(async (resolve, reject) => {
|
return this.instantiated = this._instantiate();
|
||||||
const jobsInGraph = new SafeSet();
|
}
|
||||||
let jobsReadyToInstantiate = 0;
|
|
||||||
// (this must be sync for counter to work)
|
// This method instantiates the module associated with this job and its
|
||||||
const queueJob = (moduleJob) => {
|
// entire dependency graph, i.e. creates all the module namespaces and the
|
||||||
if (jobsInGraph.has(moduleJob)) {
|
// exported/imported variables.
|
||||||
return;
|
async _instantiate() {
|
||||||
}
|
const jobsInGraph = new SafeSet();
|
||||||
jobsInGraph.add(moduleJob);
|
|
||||||
moduleJob.linked.then((dependencyJobs) => {
|
const addJobsToDependencyGraph = async (moduleJob) => {
|
||||||
for (const dependencyJob of dependencyJobs) {
|
if (jobsInGraph.has(moduleJob)) {
|
||||||
queueJob(dependencyJob);
|
return;
|
||||||
}
|
}
|
||||||
checkComplete();
|
jobsInGraph.add(moduleJob);
|
||||||
}, (e) => {
|
const dependencyJobs = await moduleJob.linked;
|
||||||
if (!this.hadError) {
|
return Promise.all(dependencyJobs.map(addJobsToDependencyGraph));
|
||||||
this.error = e;
|
};
|
||||||
this.hadError = true;
|
try {
|
||||||
}
|
await addJobsToDependencyGraph(this);
|
||||||
checkComplete();
|
} catch (e) {
|
||||||
});
|
if (!this.hadError) {
|
||||||
};
|
this.error = e;
|
||||||
const checkComplete = () => {
|
this.hadError = true;
|
||||||
if (++jobsReadyToInstantiate === jobsInGraph.size) {
|
}
|
||||||
// I believe we only throw once the whole tree is finished loading?
|
throw e;
|
||||||
// or should the error bail early, leaving entire tree to still load?
|
}
|
||||||
if (this.hadError) {
|
this.module.instantiate();
|
||||||
reject(this.error);
|
for (const dependencyJob of jobsInGraph) {
|
||||||
} else {
|
// Calling `this.module.instantiate()` instantiates not only the
|
||||||
try {
|
// ModuleWrap in this module, but all modules in the graph.
|
||||||
this.module.instantiate();
|
dependencyJob.instantiated = resolvedPromise;
|
||||||
for (const dependencyJob of jobsInGraph) {
|
}
|
||||||
dependencyJob.instantiated = resolvedPromise;
|
return this.module;
|
||||||
}
|
|
||||||
resolve(this.module);
|
|
||||||
} catch (e) {
|
|
||||||
e.stack;
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
queueJob(this);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
@ -10,39 +10,49 @@ const createDynamicModule = (exports, url = '', evaluate) => {
|
|||||||
`creating ESM facade for ${url} with exports: ${ArrayJoin(exports, ', ')}`
|
`creating ESM facade for ${url} with exports: ${ArrayJoin(exports, ', ')}`
|
||||||
);
|
);
|
||||||
const names = ArrayMap(exports, (name) => `${name}`);
|
const names = ArrayMap(exports, (name) => `${name}`);
|
||||||
// sanitized ESM for reflection purposes
|
// Create two modules: One whose exports are get- and set-able ('reflective'),
|
||||||
const src = `export let executor;
|
// and one which re-exports all of these but additionally may
|
||||||
${ArrayJoin(ArrayMap(names, (name) => `export let $${name}`), ';\n')}
|
// run an executor function once everything is set up.
|
||||||
;(() => [
|
const src = `
|
||||||
fn => executor = fn,
|
export let executor;
|
||||||
{ exports: { ${
|
${ArrayJoin(ArrayMap(names, (name) => `export let $${name};`), '\n')}
|
||||||
ArrayJoin(ArrayMap(names, (name) => `${name}: {
|
/* This function is implicitly returned as the module's completion value */
|
||||||
get: () => $${name},
|
(() => ({
|
||||||
set: v => $${name} = v
|
setExecutor: fn => executor = fn,
|
||||||
}`), ',\n')
|
reflect: {
|
||||||
} } }
|
exports: { ${
|
||||||
]);
|
ArrayJoin(ArrayMap(names, (name) => `
|
||||||
`;
|
${name}: {
|
||||||
|
get: () => $${name},
|
||||||
|
set: v => $${name} = v
|
||||||
|
}`), ', \n')}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));`;
|
||||||
const reflectiveModule = new ModuleWrap(src, `cjs-facade:${url}`);
|
const reflectiveModule = new ModuleWrap(src, `cjs-facade:${url}`);
|
||||||
reflectiveModule.instantiate();
|
reflectiveModule.instantiate();
|
||||||
const [setExecutor, reflect] = reflectiveModule.evaluate()();
|
const { setExecutor, reflect } = reflectiveModule.evaluate()();
|
||||||
// public exposed ESM
|
// public exposed ESM
|
||||||
const reexports = `import { executor,
|
const reexports = `
|
||||||
|
import {
|
||||||
|
executor,
|
||||||
${ArrayMap(names, (name) => `$${name}`)}
|
${ArrayMap(names, (name) => `$${name}`)}
|
||||||
} from "";
|
} from "";
|
||||||
export {
|
export {
|
||||||
${ArrayJoin(ArrayMap(names, (name) => `$${name} as ${name}`), ', ')}
|
${ArrayJoin(ArrayMap(names, (name) => `$${name} as ${name}`), ', ')}
|
||||||
}
|
}
|
||||||
// add await to this later if top level await comes along
|
if (typeof executor === "function") {
|
||||||
typeof executor === "function" ? executor() : void 0;`;
|
// add await to this later if top level await comes along
|
||||||
|
executor()
|
||||||
|
}`;
|
||||||
if (typeof evaluate === 'function') {
|
if (typeof evaluate === 'function') {
|
||||||
setExecutor(() => evaluate(reflect));
|
setExecutor(() => evaluate(reflect));
|
||||||
}
|
}
|
||||||
const runner = new ModuleWrap(reexports, `${url}`);
|
const module = new ModuleWrap(reexports, `${url}`);
|
||||||
runner.link(async () => reflectiveModule);
|
module.link(async () => reflectiveModule);
|
||||||
runner.instantiate();
|
module.instantiate();
|
||||||
return {
|
return {
|
||||||
module: runner,
|
module,
|
||||||
reflect
|
reflect
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user