| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f12db24 commit 2019b02
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -981,6 +981,28 @@ strict compliance with the API specification (which in some cases may accept | |||
| 981 | 981 | `func(undefined)` and `func()` are treated identically, and the | |
| 982 | 982 | [`ERR_INVALID_ARG_TYPE`][] error code may be used instead. | |
| 983 | 983 | ||
| 984 | + <a id="ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK"></a> | ||
| 985 | + ### ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK | ||
| 986 | + | ||
| 987 | + > Stability: 1 - Experimental | ||
| 988 | + | ||
| 989 | + Used when an [ES6 module][] loader hook specifies `format: 'dynamic` but does | ||
| 990 | + not provide a `dynamicInstantiate` hook. | ||
| 991 | + | ||
| 992 | + <a id="ERR_MISSING_MODULE"></a> | ||
| 993 | + ### ERR_MISSING_MODULE | ||
| 994 | + | ||
| 995 | + > Stability: 1 - Experimental | ||
| 996 | + | ||
| 997 | + Used when an [ES6 module][] cannot be resolved. | ||
| 998 | + | ||
| 999 | + <a id="ERR_MODULE_RESOLUTION_LEGACY"></a> | ||
| 1000 | + ### ERR_MODULE_RESOLUTION_LEGACY | ||
| 1001 | + | ||
| 1002 | + > Stability: 1 - Experimental | ||
| 1003 | + | ||
| 1004 | + Used when a failure occurs resolving imports in an [ES6 module][]. | ||
| 1005 | + | ||
| 984 | 1006 | <a id="ERR_NAPI_CONS_FUNCTION"></a> | |
| 985 | 1007 | ### ERR_NAPI_CONS_FUNCTION | |
| 986 | 1008 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -257,6 +257,9 @@ E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected'); | |||
| 257 | 257 | E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe'); | |
| 258 | 258 | E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks'); | |
| 259 | 259 | E('ERR_MISSING_ARGS', missingArgs); | |
| 260 | + E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', | ||
| 261 | + 'The ES Module loader may not return a format of \'dynamic\' when no ' + | ||
| 262 | + 'dynamicInstantiate function was provided'); | ||
| 260 | 263 | E('ERR_MISSING_MODULE', 'Cannot find module %s'); | |
| 261 | 264 | E('ERR_MODULE_RESOLUTION_LEGACY', '%s not found by import in %s.' + | |
| 262 | 265 | ' Legacy behavior in require() would have found it at %s'); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,7 +10,8 @@ const ModuleRequest = require('internal/loader/ModuleRequest'); | |||
| 10 | 10 | const errors = require('internal/errors'); | |
| 11 | 11 | const debug = require('util').debuglog('esm'); | |
| 12 | 12 | ||
| 13 | - function getBase() { | ||
| 13 | + // Returns a file URL for the current working directory. | ||
| 14 | + function getURLStringForCwd() { | ||
| 14 | 15 | try { | |
| 15 | 16 | return getURLFromFilePath(`${process.cwd()}/`).href; | |
| 16 | 17 | } catch (e) { | |
@@ -23,22 +24,44 @@ function getBase() { | |||
| 23 | 24 | } | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 27 | + /* A Loader instance is used as the main entry point for loading ES modules. | ||
| 28 | + * Currently, this is a singleton -- there is only one used for loading | ||
| 29 | + * the main module and everything in its dependency graph. */ | ||
| 26 | 30 | class Loader { | |
| 27 | - constructor(base = getBase()) { | ||
| 28 | - this.moduleMap = new ModuleMap(); | ||
| 31 | + constructor(base = getURLStringForCwd()) { | ||
| 29 | 32 | if (typeof base !== 'string') { | |
| 30 | 33 | throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string'); | |
| 31 | 34 | } | |
| 35 | + | ||
| 36 | + this.moduleMap = new ModuleMap(); | ||
| 32 | 37 | this.base = base; | |
| 33 | - this.resolver = ModuleRequest.resolve.bind(null); | ||
| 38 | + // The resolver has the signature | ||
| 39 | + // (specifier : string, parentURL : string, defaultResolve) | ||
| 40 | + // -> Promise<{ url : string, | ||
| 41 | + // format: anything in Loader.validFormats }> | ||
| 42 | + // where defaultResolve is ModuleRequest.resolve (having the same | ||
| 43 | + // signature itself). | ||
| 44 | + // If `.format` on the returned value is 'dynamic', .dynamicInstantiate | ||
| 45 | + // will be used as described below. | ||
| 46 | + this.resolver = ModuleRequest.resolve; | ||
| 47 | + // This hook is only called when resolve(...).format is 'dynamic' and has | ||
| 48 | + // the signature | ||
| 49 | + // (url : string) -> Promise<{ exports: { ... }, execute: function }> | ||
| 50 | + // Where `exports` is an object whose property names define the exported | ||
| 51 | + // names of the generated module. `execute` is a function that receives | ||
| 52 | + // an object with the same keys as `exports`, whose values are get/set | ||
| 53 | + // functions for the actual exported values. | ||
| 34 | 54 | this.dynamicInstantiate = undefined; | |
| 35 | 55 | } | |
| 36 | 56 | ||
| 37 | 57 | hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) { | |
| 58 | + // Use .bind() to avoid giving access to the Loader instance when it is | ||
| 59 | + // called as this.resolver(...); | ||
| 38 | 60 | this.resolver = resolve.bind(null); | |
| 39 | 61 | this.dynamicInstantiate = dynamicInstantiate; | |
| 40 | 62 | } | |
| 41 | 63 | ||
| 64 | + // Typechecking wrapper around .resolver(). | ||
| 42 | 65 | async resolve(specifier, parentURL = this.base) { | |
| 43 | 66 | if (typeof parentURL !== 'string') { | |
| 44 | 67 | throw new errors.TypeError('ERR_INVALID_ARG_TYPE', | |
@@ -48,10 +71,11 @@ class Loader { | |||
| 48 | 71 | const { url, format } = await this.resolver(specifier, parentURL, | |
| 49 | 72 | ModuleRequest.resolve); | |
| 50 | 73 | ||
| 51 | - if (typeof format !== 'string') { | ||
| 74 | + if (!Loader.validFormats.includes(format)) { | ||
| 52 | 75 | throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format', | |
| 53 | - ['esm', 'cjs', 'builtin', 'addon', 'json']); | ||
| 76 | + Loader.validFormats); | ||
| 54 | 77 | } | |
| 78 | + | ||
| 55 | 79 | if (typeof url !== 'string') { | |
| 56 | 80 | throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string'); | |
| 57 | 81 | } | |
@@ -72,14 +96,20 @@ class Loader { | |||
| 72 | 96 | return { url, format }; | |
| 73 | 97 | } | |
| 74 | 98 | ||
| 99 | + // May create a new ModuleJob instance if one did not already exist. | ||
| 75 | 100 | async getModuleJob(specifier, parentURL = this.base) { | |
| 76 | 101 | const { url, format } = await this.resolve(specifier, parentURL); | |
| 77 | 102 | let job = this.moduleMap.get(url); | |
| 78 | 103 | if (job === undefined) { | |
| 79 | 104 | let loaderInstance; | |
| 80 | 105 | if (format === 'dynamic') { | |
| 106 | + const { dynamicInstantiate } = this; | ||
| 107 | + if (typeof dynamicInstantiate !== 'function') { | ||
| 108 | + throw new errors.Error('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK'); | ||
| 109 | + } | ||
| 110 | + | ||
| 81 | 111 | loaderInstance = async (url) => { | |
| 82 | - const { exports, execute } = await this.dynamicInstantiate(url); | ||
| 112 | + const { exports, execute } = await dynamicInstantiate(url); | ||
| 83 | 113 | return createDynamicModule(exports, url, (reflect) => { | |
| 84 | 114 | debug(`Loading custom loader ${url}`); | |
| 85 | 115 | execute(reflect.exports); | |
@@ -100,5 +130,6 @@ class Loader { | |||
| 100 | 130 | return module.namespace(); | |
| 101 | 131 | } | |
| 102 | 132 | } | |
| 133 | + Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic']; | ||
| 103 | 134 | Object.setPrototypeOf(Loader.prototype, null); | |
| 104 | 135 | module.exports = Loader; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,91 +1,94 @@ | |||
| 1 | 1 | 'use strict'; | |
| 2 | 2 | ||
| 3 | + const { ModuleWrap } = | ||
| 4 | + require('internal/process').internalBinding('module_wrap'); | ||
| 3 | 5 | const { SafeSet, SafePromise } = require('internal/safe_globals'); | |
| 6 | + const assert = require('assert'); | ||
| 4 | 7 | const resolvedPromise = SafePromise.resolve(); | |
| 5 | 8 | ||
| 9 | + const enableDebug = (process.env.NODE_DEBUG || '').match(/\besm\b/) || | ||
| 10 | + process.features.debug; | ||
| 11 | + | ||
| 12 | + /* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of | ||
| 13 | + * its dependencies, over time. */ | ||
| 6 | 14 | class ModuleJob { | |
| 7 | - /** | ||
| 8 | - * @param {module: ModuleWrap?, compiled: Promise} moduleProvider | ||
| 9 | - */ | ||
| 15 | + // `loader` is the Loader instance used for loading dependencies. | ||
| 16 | + // `moduleProvider` is a function | ||
| 10 | 17 | constructor(loader, url, moduleProvider) { | |
| 11 | 18 | this.loader = loader; | |
| 12 | 19 | this.error = null; | |
| 13 | 20 | this.hadError = false; | |
| 14 | 21 | ||
| 15 | - // linked == promise for dependency jobs, with module populated, | ||
| 16 | - // module wrapper linked | ||
| 17 | - this.moduleProvider = moduleProvider; | ||
| 18 | - this.modulePromise = this.moduleProvider(url); | ||
| 22 | + // This is a Promise<{ module, reflect }>, whose fields will be copied | ||
| 23 | + // onto `this` by `link()` below once it has been resolved. | ||
| 24 | + this.modulePromise = moduleProvider(url); | ||
| 19 | 25 | this.module = undefined; | |
| 20 | 26 | this.reflect = undefined; | |
| 21 | - const linked = async () => { | ||
| 27 | + | ||
| 28 | + // Wait for the ModuleWrap instance being linked with all dependencies. | ||
| 29 | + const link = async () => { | ||
| 22 | 30 | const dependencyJobs = []; | |
| 23 | 31 | ({ module: this.module, | |
| 24 | 32 | reflect: this.reflect } = await this.modulePromise); | |
| 33 | + assert(this.module instanceof ModuleWrap); | ||
| 25 | 34 | this.module.link(async (dependencySpecifier) => { | |
| 26 | 35 | const dependencyJobPromise = | |
| 27 | 36 | this.loader.getModuleJob(dependencySpecifier, url); | |
| 28 | 37 | dependencyJobs.push(dependencyJobPromise); | |
| 29 | 38 | const dependencyJob = await dependencyJobPromise; | |
| 30 | 39 | return (await dependencyJob.modulePromise).module; | |
| 31 | 40 | }); | |
| 41 | + if (enableDebug) { | ||
| 42 | + // Make sure all dependencies are entered into the list synchronously. | ||
| 43 | + Object.freeze(dependencyJobs); | ||
| 44 | + } | ||
| 32 | 45 | return SafePromise.all(dependencyJobs); | |
| 33 | 46 | }; | |
| 34 | - this.linked = linked(); | ||
| 47 | + // Promise for the list of all dependencyJobs. | ||
| 48 | + this.linked = link(); | ||
| 35 | 49 | ||
| 36 | 50 | // instantiated == deep dependency jobs wrappers instantiated, | |
| 37 | 51 | // module wrapper instantiated | |
| 38 | 52 | this.instantiated = undefined; | |
| 39 | 53 | } | |
| 40 | 54 | ||
| 41 | - instantiate() { | ||
| 55 | + async instantiate() { | ||
| 42 | 56 | if (this.instantiated) { | |
| 43 | 57 | return this.instantiated; | |
| 44 | 58 | } | |
| 45 | - return this.instantiated = new Promise(async (resolve, reject) => { | ||
| 46 | - const jobsInGraph = new SafeSet(); | ||
| 47 | - let jobsReadyToInstantiate = 0; | ||
| 48 | - // (this must be sync for counter to work) | ||
| 49 | - const queueJob = (moduleJob) => { | ||
| 50 | - if (jobsInGraph.has(moduleJob)) { | ||
| 51 | - return; | ||
| 52 | - } | ||
| 53 | - jobsInGraph.add(moduleJob); | ||
| 54 | - moduleJob.linked.then((dependencyJobs) => { | ||
| 55 | - for (const dependencyJob of dependencyJobs) { | ||
| 56 | - queueJob(dependencyJob); | ||
| 57 | - } | ||
| 58 | - checkComplete(); | ||
| 59 | - }, (e) => { | ||
| 60 | - if (!this.hadError) { | ||
| 61 | - this.error = e; | ||
| 62 | - this.hadError = true; | ||
| 63 | - } | ||
| 64 | - checkComplete(); | ||
| 65 | - }); | ||
| 66 | - }; | ||
| 67 | - const checkComplete = () => { | ||
| 68 | - if (++jobsReadyToInstantiate === jobsInGraph.size) { | ||
| 69 | - // I believe we only throw once the whole tree is finished loading? | ||
| 70 | - // or should the error bail early, leaving entire tree to still load? | ||
| 71 | - if (this.hadError) { | ||
| 72 | - reject(this.error); | ||
| 73 | - } else { | ||
| 74 | - try { | ||
| 75 | - this.module.instantiate(); | ||
| 76 | - for (const dependencyJob of jobsInGraph) { | ||
| 77 | - dependencyJob.instantiated = resolvedPromise; | ||
| 78 | - } | ||
| 79 | - resolve(this.module); | ||
| 80 | - } catch (e) { | ||
| 81 | - e.stack; | ||
| 82 | - reject(e); | ||
| 83 | - } | ||
| 84 | - } | ||
| 85 | - } | ||
| 86 | - }; | ||
| 87 | - queueJob(this); | ||
| 88 | - }); | ||
| 59 | + return this.instantiated = this._instantiate(); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + // This method instantiates the module associated with this job and its | ||
| 63 | + // entire dependency graph, i.e. creates all the module namespaces and the | ||
| 64 | + // exported/imported variables. | ||
| 65 | + async _instantiate() { | ||
| 66 | + const jobsInGraph = new SafeSet(); | ||
| 67 | + | ||
| 68 | + const addJobsToDependencyGraph = async (moduleJob) => { | ||
| 69 | + if (jobsInGraph.has(moduleJob)) { | ||
| 70 | + return; | ||
| 71 | + } | ||
| 72 | + jobsInGraph.add(moduleJob); | ||
| 73 | + const dependencyJobs = await moduleJob.linked; | ||
| 74 | + return Promise.all(dependencyJobs.map(addJobsToDependencyGraph)); | ||
| 75 | + }; | ||
| 76 | + try { | ||
| 77 | + await addJobsToDependencyGraph(this); | ||
| 78 | + } catch (e) { | ||
| 79 | + if (!this.hadError) { | ||
| 80 | + this.error = e; | ||
| 81 | + this.hadError = true; | ||
| 82 | + } | ||
| 83 | + throw e; | ||
| 84 | + } | ||
| 85 | + this.module.instantiate(); | ||
| 86 | + for (const dependencyJob of jobsInGraph) { | ||
| 87 | + // Calling `this.module.instantiate()` instantiates not only the | ||
| 88 | + // ModuleWrap in this module, but all modules in the graph. | ||
| 89 | + dependencyJob.instantiated = resolvedPromise; | ||
| 90 | + } | ||
| 91 | + return this.module; | ||
| 89 | 92 | } | |
| 90 | 93 | ||
| 91 | 94 | async run() { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,39 +11,49 @@ const createDynamicModule = (exports, url = '', evaluate) => { | |||
| 11 | 11 | `creating ESM facade for ${url} with exports: ${ArrayJoin(exports, ', ')}` | |
| 12 | 12 | ); | |
| 13 | 13 | const names = ArrayMap(exports, (name) => `${name}`); | |
| 14 | - // sanitized ESM for reflection purposes | ||
| 15 | - const src = `export let executor; | ||
| 16 | - ${ArrayJoin(ArrayMap(names, (name) => `export let $${name}`), ';\n')} | ||
| 17 | - ;(() => [ | ||
| 18 | - fn => executor = fn, | ||
| 19 | - { exports: { ${ | ||
| 20 | - ArrayJoin(ArrayMap(names, (name) => `${name}: { | ||
| 21 | - get: () => $${name}, | ||
| 22 | - set: v => $${name} = v | ||
| 23 | - }`), ',\n') | ||
| 24 | - } } } | ||
| 25 | - ]); | ||
| 26 | - `; | ||
| 14 | + // Create two modules: One whose exports are get- and set-able ('reflective'), | ||
| 15 | + // and one which re-exports all of these but additionally may | ||
| 16 | + // run an executor function once everything is set up. | ||
| 17 | + const src = ` | ||
| 18 | + export let executor; | ||
| 19 | + ${ArrayJoin(ArrayMap(names, (name) => `export let $${name};`), '\n')} | ||
| 20 | + /* This function is implicitly returned as the module's completion value */ | ||
| 21 | + (() => ({ | ||
| 22 | + setExecutor: fn => executor = fn, | ||
| 23 | + reflect: { | ||
| 24 | + exports: { ${ | ||
| 25 | + ArrayJoin(ArrayMap(names, (name) => ` | ||
| 26 | + ${name}: { | ||
| 27 | + get: () => $${name}, | ||
| 28 | + set: v => $${name} = v | ||
| 29 | + }`), ', \n')} | ||
| 30 | + } | ||
| 31 | + } | ||
| 32 | + }));`; | ||
| 27 | 33 | const reflectiveModule = new ModuleWrap(src, `cjs-facade:${url}`); | |
| 28 | 34 | reflectiveModule.instantiate(); | |
| 29 | - const [setExecutor, reflect] = reflectiveModule.evaluate()(); | ||
| 35 | + const { setExecutor, reflect } = reflectiveModule.evaluate()(); | ||
| 30 | 36 | // public exposed ESM | |
| 31 | - const reexports = `import { executor, | ||
| 37 | + const reexports = ` | ||
| 38 | + import { | ||
| 39 | + executor, | ||
| 32 | 40 | ${ArrayMap(names, (name) => `$${name}`)} | |
| 33 | 41 | } from ""; | |
| 34 | 42 | export { | |
| 35 | 43 | ${ArrayJoin(ArrayMap(names, (name) => `$${name} as ${name}`), ', ')} | |
| 36 | 44 | } | |
| 37 | - // add await to this later if top level await comes along | ||
| 38 | - typeof executor === "function" ? executor() : void 0;`; | ||
| 45 | + if (typeof executor === "function") { | ||
| 46 | + // add await to this later if top level await comes along | ||
| 47 | + executor() | ||
| 48 | + }`; | ||
| 39 | 49 | if (typeof evaluate === 'function') { | |
| 40 | 50 | setExecutor(() => evaluate(reflect)); | |
| 41 | 51 | } | |
| 42 | - const runner = new ModuleWrap(reexports, `${url}`); | ||
| 43 | - runner.link(async () => reflectiveModule); | ||
| 44 | - runner.instantiate(); | ||
| 52 | + const module = new ModuleWrap(reexports, `${url}`); | ||
| 53 | + module.link(async () => reflectiveModule); | ||
| 54 | + module.instantiate(); | ||
| 45 | 55 | return { | |
| 46 | - module: runner, | ||
| 56 | + module, | ||
| 47 | 57 | reflect | |
| 48 | 58 | }; | |
| 49 | 59 | }; | |
| Back | FazBrowse Home | New Git URL |
0 commit comments