UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

371 lines (336 loc) 12.8 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/modules/esm/module_job.js import { ModuleWrap, kEvaluated } from "nstdlib/stub/binding/module_wrap"; import { privateSymbols as __privateSymbols__ } from "nstdlib/stub/binding/util"; import { decorateErrorStack, kEmptyObject } from "nstdlib/lib/internal/util"; import { getSourceMapsEnabled } from "nstdlib/lib/internal/source_map/source_map_cache"; import * as assert from "nstdlib/lib/internal/assert"; import { setHasStartedUserESMExecution } from "nstdlib/lib/internal/modules/helpers"; import * as __hoisted_internal_modules_package_json_reader__ from "nstdlib/lib/internal/modules/package_json_reader"; const { entry_point_module_private_symbol } = __privateSymbols__; const resolvedPromise = Promise.resolve(); const noop = Function.prototype; let hasPausedEntry = false; const CJSGlobalLike = [ "require", "module", "exports", "__filename", "__dirname", ]; const isCommonJSGlobalLikeNotDefinedError = (errorMessage) => Array.prototype.some.call( CJSGlobalLike, (globalLike) => errorMessage === `${globalLike} is not defined`, ); class ModuleJobBase { constructor( url, importAttributes, moduleWrapMaybePromise, isMain, inspectBrk, ) { this.importAttributes = importAttributes; this.isMain = isMain; this.inspectBrk = inspectBrk; this.url = url; this.module = moduleWrapMaybePromise; } } /* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of * its dependencies, over time. */ class ModuleJob extends ModuleJobBase { #loader = null; // `loader` is the Loader instance used for loading dependencies. constructor( loader, url, importAttributes = { __proto__: null }, moduleProvider, isMain, inspectBrk, sync = false, ) { const modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); super(url, importAttributes, modulePromise, isMain, inspectBrk); this.#loader = loader; // Expose the promise to the ModuleWrap directly for linking below. // `this.module` is also filled in below. this.modulePromise = modulePromise; if (sync) { this.module = this.modulePromise; this.modulePromise = Promise.resolve(this.module); } else { this.modulePromise = Promise.resolve(this.modulePromise); } // Promise for the list of all dependencyJobs. this.linked = this._link(); // This promise is awaited later anyway, so silence // 'unhandled rejection' warnings. Promise.prototype.then.call(this.linked, undefined, noop); // instantiated == deep dependency jobs wrappers are instantiated, // and module wrapper is instantiated. this.instantiated = undefined; } /** * Iterates the module requests and links with the loader. * @returns {Promise<ModuleJob[]>} Dependency module jobs. */ async _link() { this.module = await this.modulePromise; assert(this.module instanceof ModuleWrap); const moduleRequests = this.module.getModuleRequests(); // Explicitly keeping track of dependency jobs is needed in order // to flatten out the dependency graph below in `_instantiate()`, // so that circular dependencies can't cause a deadlock by two of // these `link` callbacks depending on each other. // Create an ArrayLike to avoid calling into userspace with `.then` // when returned from the async function. const dependencyJobs = Array(moduleRequests.length); Object.setPrototypeOf(dependencyJobs, null); // Specifiers should be aligned with the moduleRequests array in order. const specifiers = Array(moduleRequests.length); const modulePromises = Array(moduleRequests.length); // Iterate with index to avoid calling into userspace with `Symbol.iterator`. for (let idx = 0; idx < moduleRequests.length; idx++) { const { specifier, attributes } = moduleRequests[idx]; const dependencyJobPromise = this.#loader.getModuleJob( specifier, this.url, attributes, ); const modulePromise = Promise.prototype.then.call( dependencyJobPromise, (job) => { { /* debug */ } dependencyJobs[idx] = job; return job.modulePromise; }, ); modulePromises[idx] = modulePromise; specifiers[idx] = specifier; } const modules = await Promise.all(modulePromises); this.module.link(specifiers, modules); return dependencyJobs; } instantiate() { if (this.instantiated === undefined) { this.instantiated = this._instantiate(); } return this.instantiated; } async _instantiate() { const jobsInGraph = new Set(); const addJobsToDependencyGraph = async (moduleJob) => { { /* debug */ } if (jobsInGraph.has(moduleJob)) { return; } jobsInGraph.add(moduleJob); const dependencyJobs = await moduleJob.linked; return Promise.all(dependencyJobs, addJobsToDependencyGraph); }; await addJobsToDependencyGraph(this); try { if (!hasPausedEntry && this.inspectBrk) { hasPausedEntry = true; const initWrapper = require("binding/inspector").callAndPauseOnStart; initWrapper(this.module.instantiate, this.module); } else { this.module.instantiate(); } } catch (e) { decorateErrorStack(e); // TODO(@bcoe): Add source map support to exception that occurs as result // of missing named export. This is currently not possible because // stack trace originates in module_job, not the file itself. A hidden // symbol with filename could be set in node_errors.cc to facilitate this. if ( !getSourceMapsEnabled() && String.prototype.includes.call( e.message, " does not provide an export named", ) ) { const splitStack = String.prototype.split.call(e.stack, "\n"); const parentFileUrl = RegExp.prototype[Symbol.replace].call( /:\d+$/, splitStack[0], "", ); const { 1: childSpecifier, 2: name } = RegExp.prototype.exec.call( /module '(.*)' does not provide an export named '(.+)'/, e.message, ); const { url: childFileURL } = await this.#loader.resolve( childSpecifier, parentFileUrl, kEmptyObject, ); let format; try { // This might throw for non-CommonJS modules because we aren't passing // in the import attributes and some formats require them; but we only // care about CommonJS for the purposes of this error message. ({ format } = await this.#loader.load(childFileURL)); } catch { // Continue regardless of error. } if (format === "commonjs") { const importStatement = splitStack[1]; // TODO(@ctavan): The original error stack only provides the single // line which causes the error. For multi-line import statements we // cannot generate an equivalent object destructuring assignment by // just parsing the error stack. const oneLineNamedImports = RegExp.prototype.exec.call( /{.*}/, importStatement, ); const destructuringAssignment = oneLineNamedImports && RegExp.prototype[Symbol.replace].call( /\s+as\s+/g, oneLineNamedImports, ": ", ); e.message = `Named export '${name}' not found. The requested module` + ` '${childSpecifier}' is a CommonJS module, which may not support` + " all module.exports as named exports.\nCommonJS modules can " + "always be imported via the default export, for example using:" + `\n\nimport pkg from '${childSpecifier}';\n${ destructuringAssignment ? `const ${destructuringAssignment} = pkg;\n` : "" }`; const newStack = String.prototype.split.call(e.stack, "\n"); newStack[3] = `SyntaxError: ${e.message}`; e.stack = Array.prototype.join.call(newStack, "\n"); } } throw e; } 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; } } runSync() { assert(this.module instanceof ModuleWrap); if (this.instantiated !== undefined) { return { __proto__: null, module: this.module }; } this.module.instantiate(); this.instantiated = Promise.resolve(); const timeout = -1; const breakOnSigint = false; setHasStartedUserESMExecution(); this.module.evaluate(timeout, breakOnSigint); return { __proto__: null, module: this.module }; } async run(isEntryPoint = false) { await this.instantiate(); if (isEntryPoint) { globalThis[entry_point_module_private_symbol] = this.module; } const timeout = -1; const breakOnSigint = false; setHasStartedUserESMExecution(); try { await this.module.evaluate(timeout, breakOnSigint); } catch (e) { if ( e?.name === "ReferenceError" && isCommonJSGlobalLikeNotDefinedError(e.message) ) { e.message += " in ES module scope"; if (String.prototype.startsWith.call(e.message, "require ")) { e.message += ", you can use import instead"; } const packageConfig = String.prototype.startsWith.call(this.module.url, "file://") && RegExp.prototype.exec.call( /\.js(\?[^#]*)?(#.*)?$/, this.module.url, ) !== null && __hoisted_internal_modules_package_json_reader__.getPackageScopeConfig( this.module.url, ); if (packageConfig.type === "module") { e.message += "\nThis file is being treated as an ES module because it has a " + `'.js' file extension and '${packageConfig.pjsonPath}' contains ` + '"type": "module". To treat it as a CommonJS script, rename it ' + "to use the '.cjs' file extension."; } } throw e; } return { __proto__: null, module: this.module }; } } // This is a fully synchronous job and does not spawn additional threads in any way. // All the steps are ensured to be synchronous and it throws on instantiating // an asynchronous graph. class ModuleJobSync extends ModuleJobBase { #loader = null; constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) { super(url, importAttributes, moduleWrap, isMain, inspectBrk, true); this.#loader = loader; assert(this.module instanceof ModuleWrap); // Store itself into the cache first before linking in case there are circular // references in the linking. loader.loadCache.set(url, importAttributes.type, this); try { const moduleRequests = this.module.getModuleRequests(); // Specifiers should be aligned with the moduleRequests array in order. const specifiers = Array(moduleRequests.length); const modules = Array(moduleRequests.length); const jobs = Array(moduleRequests.length); for (let i = 0; i < moduleRequests.length; ++i) { const { specifier, attributes } = moduleRequests[i]; const job = this.#loader.getModuleJobForRequire( specifier, url, attributes, ); specifiers[i] = specifier; modules[i] = job.module; jobs[i] = job; } this.module.link(specifiers, modules); this.linked = jobs; } finally { // Restore it - if it succeeds, we'll reset in the caller; Otherwise it's // not cached and if the error is caught, subsequent attempt would still fail. loader.loadCache.delete(url, importAttributes.type); } } get modulePromise() { return Promise.resolve(this.module); } async run() { const status = this.module.getStatus(); assert( status === kEvaluated, `A require()-d module that is imported again must be evaluated. Status = ${status}`, ); return { __proto__: null, module: this.module }; } runSync() { this.module.instantiateSync(); setHasStartedUserESMExecution(); const namespace = this.module.evaluateSync(); return { __proto__: null, module: this.module, namespace }; } } Object.setPrototypeOf(ModuleJobBase.prototype, null); export { ModuleJob }; export { ModuleJobSync }; export { ModuleJobBase };