Skip to content

Commit e5294fb

Browse files
committed
esm: fix CJS named export error message for sync module jobs
1 parent a66268e commit e5294fb

File tree

1 file changed

+63
-45
lines changed

1 file changed

+63
-45
lines changed

lib/internal/modules/esm/module_job.js

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,58 @@ const explainCommonJSGlobalLikeNotDefinedError = (e, url, hasTopLevelAwait) => {
126126
}
127127
};
128128

129+
/**
130+
* If `error` is a SyntaxError from V8 for a missing named export on a CJS module, rewrite its message to the friendlier
131+
* "Named export '...' not found..." form. Must be called after `decorateErrorStack(error)` so that the arrow (source
132+
* context with the import statement text) has been prepended to `error.stack`.
133+
* @param {Error} error
134+
* @param {ModuleWrap} module The parent module that triggered the instantiation.
135+
* @param {boolean[]} commonJsDeps Per-request array indicating whether each dependency is a CJS module, aligned with
136+
* `module.getModuleRequests()`.
137+
*/
138+
const handleCJSNamedExportError = (error, module, commonJsDeps) => {
139+
// TODO(@bcoe): Add source map support to exception that occurs as result
140+
// of missing named export. This is currently not possible because
141+
// stack trace originates in module_job, not the file itself. A hidden
142+
// symbol with filename could be set in node_errors.cc to facilitate this.
143+
if (!getSourceMapsSupport().enabled &&
144+
StringPrototypeIncludes(error.message,
145+
' does not provide an export named')) {
146+
const splitStack = StringPrototypeSplit(error.stack, '\n', 2);
147+
const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
148+
/module '(.*)' does not provide an export named '(.+)'/,
149+
error.message);
150+
const moduleRequests = module.getModuleRequests();
151+
let isCommonJS = false;
152+
for (let i = 0; i < moduleRequests.length; ++i) {
153+
if (moduleRequests[i].specifier === childSpecifier) {
154+
isCommonJS = commonJsDeps[i];
155+
break;
156+
}
157+
}
158+
if (isCommonJS) {
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+
error.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(error.stack, '\n');
175+
newStack[3] = `SyntaxError: ${error.message}`;
176+
error.stack = ArrayPrototypeJoin(newStack, '\n');
177+
}
178+
}
179+
};
180+
129181
class ModuleJobBase {
130182
constructor(loader, url, importAttributes, phase, isMain, inspectBrk) {
131183
assert(typeof phase === 'number');
@@ -325,50 +377,10 @@ class ModuleJob extends ModuleJobBase {
325377
} else {
326378
this.module.instantiate();
327379
}
328-
} catch (e) {
329-
decorateErrorStack(e);
330-
// TODO(@bcoe): Add source map support to exception that occurs as result
331-
// of missing named export. This is currently not possible because
332-
// stack trace originates in module_job, not the file itself. A hidden
333-
// symbol with filename could be set in node_errors.cc to facilitate this.
334-
if (!getSourceMapsSupport().enabled &&
335-
StringPrototypeIncludes(e.message,
336-
' does not provide an export named')) {
337-
const splitStack = StringPrototypeSplit(e.stack, '\n', 2);
338-
const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
339-
/module '(.*)' does not provide an export named '(.+)'/,
340-
e.message);
341-
const moduleRequests = this.module.getModuleRequests();
342-
let isCommonJS = false;
343-
for (let i = 0; i < moduleRequests.length; ++i) {
344-
if (moduleRequests[i].specifier === childSpecifier) {
345-
isCommonJS = this.commonJsDeps[i];
346-
break;
347-
}
348-
}
349-
350-
if (isCommonJS) {
351-
const importStatement = splitStack[1];
352-
// TODO(@ctavan): The original error stack only provides the single
353-
// line which causes the error. For multi-line import statements we
354-
// cannot generate an equivalent object destructuring assignment by
355-
// just parsing the error stack.
356-
const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement);
357-
const destructuringAssignment = oneLineNamedImports &&
358-
RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': ');
359-
e.message = `Named export '${name}' not found. The requested module` +
360-
` '${childSpecifier}' is a CommonJS module, which may not support` +
361-
' all module.exports as named exports.\nCommonJS modules can ' +
362-
'always be imported via the default export, for example using:' +
363-
`\n\nimport pkg from '${childSpecifier}';\n${
364-
destructuringAssignment ?
365-
`const ${destructuringAssignment} = pkg;\n` : ''}`;
366-
const newStack = StringPrototypeSplit(e.stack, '\n');
367-
newStack[3] = `SyntaxError: ${e.message}`;
368-
e.stack = ArrayPrototypeJoin(newStack, '\n');
369-
}
370-
}
371-
throw e;
380+
} catch (error) {
381+
decorateErrorStack(error);
382+
handleCJSNamedExportError(error, this.module, this.commonJsDeps);
383+
throw error;
372384
}
373385

374386
for (const dependencyJob of jobsInGraph) {
@@ -510,7 +522,13 @@ class ModuleJobSync extends ModuleJobBase {
510522
}
511523
if (status < kInstantiated) {
512524
// Fresh module: instantiate it now (links were already resolved synchronously in constructor)
513-
this.module.instantiate();
525+
try {
526+
this.module.instantiate();
527+
} catch (error) {
528+
decorateErrorStack(error);
529+
handleCJSNamedExportError(error, this.module, this.commonJsDeps);
530+
throw error;
531+
}
514532
}
515533
// `status === kInstantiated`: either just instantiated above, or previously instantiated
516534
// but evaluation was deferred (e.g. TLA detected by a prior `runSync()` call)

0 commit comments

Comments
 (0)