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