• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeJoin,
5  ArrayPrototypePush,
6  ArrayPrototypeSome,
7  FunctionPrototype,
8  ObjectSetPrototypeOf,
9  PromiseResolve,
10  PromisePrototypeThen,
11  ReflectApply,
12  RegExpPrototypeExec,
13  RegExpPrototypeSymbolReplace,
14  SafePromiseAllReturnArrayLike,
15  SafePromiseAllReturnVoid,
16  SafeSet,
17  StringPrototypeIncludes,
18  StringPrototypeSplit,
19  StringPrototypeStartsWith,
20} = primordials;
21
22const { ModuleWrap } = internalBinding('module_wrap');
23
24const { decorateErrorStack, kEmptyObject } = require('internal/util');
25const {
26  getSourceMapsEnabled,
27} = require('internal/source_map/source_map_cache');
28const assert = require('internal/assert');
29const resolvedPromise = PromiseResolve();
30
31const noop = FunctionPrototype;
32
33let hasPausedEntry = false;
34
35const CJSGlobalLike = [
36  'require',
37  'module',
38  'exports',
39  '__filename',
40  '__dirname',
41];
42const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
43  ArrayPrototypeSome(
44    CJSGlobalLike,
45    (globalLike) => errorMessage === `${globalLike} is not defined`,
46  );
47
48/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of
49 * its dependencies, over time. */
50class ModuleJob {
51  // `loader` is the Loader instance used for loading dependencies.
52  // `moduleProvider` is a function
53  constructor(loader, url, importAttributes = { __proto__: null },
54              moduleProvider, isMain, inspectBrk) {
55    this.loader = loader;
56    this.importAttributes = importAttributes;
57    this.isMain = isMain;
58    this.inspectBrk = inspectBrk;
59
60    this.module = undefined;
61    // Expose the promise to the ModuleWrap directly for linking below.
62    // `this.module` is also filled in below.
63    this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
64
65    // Wait for the ModuleWrap instance being linked with all dependencies.
66    const link = async () => {
67      this.module = await this.modulePromise;
68      assert(this.module instanceof ModuleWrap);
69
70      // Explicitly keeping track of dependency jobs is needed in order
71      // to flatten out the dependency graph below in `_instantiate()`,
72      // so that circular dependencies can't cause a deadlock by two of
73      // these `link` callbacks depending on each other.
74      const dependencyJobs = [];
75      const promises = this.module.link(async (specifier, attributes) => {
76        const job = await this.loader.getModuleJob(specifier, url, attributes);
77        ArrayPrototypePush(dependencyJobs, job);
78        return job.modulePromise;
79      });
80
81      if (promises !== undefined) {
82        await SafePromiseAllReturnVoid(promises);
83      }
84
85      return SafePromiseAllReturnArrayLike(dependencyJobs);
86    };
87    // Promise for the list of all dependencyJobs.
88    this.linked = link();
89    // This promise is awaited later anyway, so silence
90    // 'unhandled rejection' warnings.
91    PromisePrototypeThen(this.linked, undefined, noop);
92
93    // instantiated == deep dependency jobs wrappers are instantiated,
94    // and module wrapper is instantiated.
95    this.instantiated = undefined;
96  }
97
98  instantiate() {
99    if (this.instantiated === undefined) {
100      this.instantiated = this._instantiate();
101    }
102    return this.instantiated;
103  }
104
105  async _instantiate() {
106    const jobsInGraph = new SafeSet();
107    const addJobsToDependencyGraph = async (moduleJob) => {
108      if (jobsInGraph.has(moduleJob)) {
109        return;
110      }
111      jobsInGraph.add(moduleJob);
112      const dependencyJobs = await moduleJob.linked;
113      return SafePromiseAllReturnVoid(dependencyJobs, addJobsToDependencyGraph);
114    };
115    await addJobsToDependencyGraph(this);
116
117    try {
118      if (!hasPausedEntry && this.inspectBrk) {
119        hasPausedEntry = true;
120        const initWrapper = internalBinding('inspector').callAndPauseOnStart;
121        initWrapper(this.module.instantiate, this.module);
122      } else {
123        this.module.instantiate();
124      }
125    } catch (e) {
126      decorateErrorStack(e);
127      // TODO(@bcoe): Add source map support to exception that occurs as result
128      // of missing named export. This is currently not possible because
129      // stack trace originates in module_job, not the file itself. A hidden
130      // symbol with filename could be set in node_errors.cc to facilitate this.
131      if (!getSourceMapsEnabled() &&
132          StringPrototypeIncludes(e.message,
133                                  ' does not provide an export named')) {
134        const splitStack = StringPrototypeSplit(e.stack, '\n');
135        const parentFileUrl = RegExpPrototypeSymbolReplace(
136          /:\d+$/,
137          splitStack[0],
138          '',
139        );
140        const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
141          /module '(.*)' does not provide an export named '(.+)'/,
142          e.message);
143        const { url: childFileURL } = await this.loader.resolve(
144          childSpecifier,
145          parentFileUrl,
146          kEmptyObject,
147        );
148        let format;
149        try {
150          // This might throw for non-CommonJS modules because we aren't passing
151          // in the import attributes and some formats require them; but we only
152          // care about CommonJS for the purposes of this error message.
153          ({ format } =
154            await this.loader.load(childFileURL));
155        } catch {
156          // Continue regardless of error.
157        }
158
159        if (format === 'commonjs') {
160          const importStatement = splitStack[1];
161          // TODO(@ctavan): The original error stack only provides the single
162          // line which causes the error. For multi-line import statements we
163          // cannot generate an equivalent object destructuring assignment by
164          // just parsing the error stack.
165          const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement);
166          const destructuringAssignment = oneLineNamedImports &&
167            RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': ');
168          e.message = `Named export '${name}' not found. The requested module` +
169            ` '${childSpecifier}' is a CommonJS module, which may not support` +
170            ' all module.exports as named exports.\nCommonJS modules can ' +
171            'always be imported via the default export, for example using:' +
172            `\n\nimport pkg from '${childSpecifier}';\n${
173              destructuringAssignment ?
174                `const ${destructuringAssignment} = pkg;\n` : ''}`;
175          const newStack = StringPrototypeSplit(e.stack, '\n');
176          newStack[3] = `SyntaxError: ${e.message}`;
177          e.stack = ArrayPrototypeJoin(newStack, '\n');
178        }
179      }
180      throw e;
181    }
182
183    for (const dependencyJob of jobsInGraph) {
184      // Calling `this.module.instantiate()` instantiates not only the
185      // ModuleWrap in this module, but all modules in the graph.
186      dependencyJob.instantiated = resolvedPromise;
187    }
188  }
189
190  async run() {
191    await this.instantiate();
192    const timeout = -1;
193    const breakOnSigint = false;
194    try {
195      await this.module.evaluate(timeout, breakOnSigint);
196    } catch (e) {
197      if (e?.name === 'ReferenceError' &&
198          isCommonJSGlobalLikeNotDefinedError(e.message)) {
199        e.message += ' in ES module scope';
200
201        if (StringPrototypeStartsWith(e.message, 'require ')) {
202          e.message += ', you can use import instead';
203        }
204
205        const packageConfig =
206          StringPrototypeStartsWith(this.module.url, 'file://') &&
207            RegExpPrototypeExec(/\.js(\?[^#]*)?(#.*)?$/, this.module.url) !== null &&
208            require('internal/modules/esm/resolve')
209              .getPackageScopeConfig(this.module.url);
210        if (packageConfig.type === 'module') {
211          e.message +=
212            '\nThis file is being treated as an ES module because it has a ' +
213            `'.js' file extension and '${packageConfig.pjsonPath}' contains ` +
214            '"type": "module". To treat it as a CommonJS script, rename it ' +
215            'to use the \'.cjs\' file extension.';
216        }
217      }
218      throw e;
219    }
220    return { __proto__: null, module: this.module };
221  }
222}
223ObjectSetPrototypeOf(ModuleJob.prototype, null);
224module.exports = ModuleJob;
225