Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions lib/internal/errors/error_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

const {
FunctionPrototypeBind,
MathMax,
MathMin,
NumberIsFinite,
RegExpPrototypeSymbolReplace,
StringPrototypeRepeat,
StringPrototypeSlice,
} = primordials;

Expand All @@ -14,6 +19,80 @@ const {
getSourceLine,
} = require('internal/source_map/source_map_cache');

const kSourceLineMaxLength = 120;
const kSourceLineContext = 40;
const kLineEllipsis = '...';

function createSourceUnderline(sourceLine, startColumn, underlineLength) {
const prefix = RegExpPrototypeSymbolReplace(
/[^\t]/g, StringPrototypeSlice(sourceLine, 0, startColumn), ' ');
return prefix + StringPrototypeRepeat('^', underlineLength);
}

function clipSourceLine(sourceLine, startColumn, underlineLength) {
if (sourceLine.length <= kSourceLineMaxLength) {
return {
sourceLine,
startColumn,
underlineLength,
};
}

const targetEnd = startColumn + underlineLength;
const windowStart = MathMax(0, startColumn - kSourceLineContext);
const windowEnd = MathMin(
sourceLine.length,
windowStart + kSourceLineMaxLength,
targetEnd + kSourceLineContext,
);

const leftEllipsis = windowStart > 0 ? kLineEllipsis : '';
const rightEllipsis = windowEnd < sourceLine.length ? kLineEllipsis : '';
const clippedLine = leftEllipsis +
StringPrototypeSlice(sourceLine, windowStart, windowEnd) +
rightEllipsis;
const clippedStartColumn = leftEllipsis.length + startColumn - windowStart;
const clippedUnderlineLength = MathMax(
1,
MathMin(underlineLength, windowEnd - startColumn),
);

return {
sourceLine: clippedLine,
startColumn: clippedStartColumn,
underlineLength: clippedUnderlineLength,
};
}

/**
* Format a source line with a caret underline for an error message.
* @param {string} filename The file containing the source line.
* @param {number} lineNumber The 1-based line number.
* @param {string} sourceLine The source line text.
* @param {number} startColumn The 0-based underline start column.
* @param {number} underlineLength The underline length.
* @returns {string|undefined}
*/
function getErrorSourceMessage(filename, lineNumber, sourceLine, startColumn, underlineLength) {
if (typeof sourceLine !== 'string' ||
!NumberIsFinite(lineNumber) ||
!NumberIsFinite(startColumn) ||
!NumberIsFinite(underlineLength)) {
return;
}

startColumn = MathMax(0, startColumn);
underlineLength = MathMax(1, underlineLength);

const clipped = clipSourceLine(sourceLine, startColumn, underlineLength);
const arrow = createSourceUnderline(
clipped.sourceLine,
clipped.startColumn,
clipped.underlineLength,
);
return `${filename}:${lineNumber}\n${clipped.sourceLine}\n${arrow}\n`;
}

/**
* Get the source location of an error. If source map is enabled, resolve the source location
* based on the source map.
Expand Down Expand Up @@ -162,4 +241,5 @@ function getErrorSourceExpression(error) {
module.exports = {
getErrorSourceLocation,
getErrorSourceExpression,
getErrorSourceMessage,
};
67 changes: 64 additions & 3 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const {
Error,
FunctionPrototypeCall,
JSONParse,
Number,
ObjectDefineProperty,
ObjectFreeze,
ObjectGetOwnPropertyDescriptor,
Expand All @@ -57,7 +58,6 @@ const {
StringPrototypeCharCodeAt,
StringPrototypeEndsWith,
StringPrototypeIndexOf,
StringPrototypeRepeat,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
Expand Down Expand Up @@ -142,6 +142,7 @@ const {
kEmptyObject,
setOwnProperty,
getLazy,
getStructuredStack,
isWindows,
isUnderNodeModules,
} = require('internal/util');
Expand Down Expand Up @@ -183,6 +184,8 @@ const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const packageJsonReader = require('internal/modules/package_json_reader');
const { getOptionValue, getEmbedderOptions } = require('internal/options');
const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES);
const lazyGetErrorSourceMessage =
getLazy(() => require('internal/errors/error_source').getErrorSourceMessage);

const {
vm_dynamic_import_default_internal,
Expand Down Expand Up @@ -1475,6 +1478,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
const err = new Error(message);
err.code = 'MODULE_NOT_FOUND';
err.requireStack = requireStack;
decorateModuleNotFoundError(err, requireStack, request);
throw err;
};

Expand Down Expand Up @@ -1518,6 +1522,60 @@ function createEsmNotFoundErr(request, path) {
return err;
}

function decorateModuleNotFoundError(err, requireStack, request) {
const parentPath = requireStack[0];
if (!parentPath || StringPrototypeIndexOf(parentPath, path.sep) === -1) {
return;
}

const stack = getStructuredStack();
let line;
let col;
for (let i = 0; i < stack.length; i++) {
const frame = stack[i];
if (frame.getFileName() === parentPath) {
line = frame.getLineNumber();
col = frame.getColumnNumber();
break;
}
}

if (!line || !col) {
return;
}

let source;
try {
source = fs.readFileSync(parentPath, 'utf8');
} catch {
return;
}

const sourceLine = StringPrototypeSplit(source, '\n', line)[line - 1];
if (sourceLine === undefined) {
return;
}

let column = StringPrototypeIndexOf(sourceLine, request, col - 1);
if (column === -1) {
column = StringPrototypeIndexOf(sourceLine, request);
}
if (column === -1) {
column = col - 1;
}

const message = lazyGetErrorSourceMessage()(
parentPath,
Number(line),
sourceLine,
column,
request.length,
);
if (message !== undefined) {
setArrowMessage(err, message);
}
}

function getExtensionForFormat(format) {
switch (format) {
case 'addon':
Expand Down Expand Up @@ -1879,8 +1937,11 @@ function reconstructErrorStack(err, parentPath, parentSource) {
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
if (line && col) {
const srcLine = StringPrototypeSplit(parentSource, '\n', line)[line - 1];
const frame = `${parentPath}:${line}\n${srcLine}\n${StringPrototypeRepeat(' ', col - 1)}^\n`;
setArrowMessage(err, frame);
const message = lazyGetErrorSourceMessage()(
parentPath, Number(line), srcLine, col - 1, 1);
if (message !== undefined) {
setArrowMessage(err, message);
}
}
}

Expand Down
106 changes: 99 additions & 7 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ const {
ArrayPrototypeReduce,
FunctionPrototypeCall,
JSONStringify,
Number,
ObjectSetPrototypeOf,
Promise,
PromisePrototypeThen,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
StringPrototypeIndexOf,
StringPrototypeSplit,
StringPrototypeStartsWith,
encodeURIComponent,
hardenRegExp,
} = primordials;
Expand All @@ -24,16 +29,21 @@ const { imported_cjs_symbol } = internalBinding('symbols');

const assert = require('internal/assert');
const {
ERR_REQUIRE_ASYNC_MODULE,
ERR_REQUIRE_CYCLE_MODULE,
ERR_REQUIRE_ESM,
ERR_REQUIRE_ESM_RACE_CONDITION,
ERR_UNKNOWN_MODULE_FORMAT,
} = require('internal/errors').codes;
codes: {
ERR_REQUIRE_ASYNC_MODULE,
ERR_REQUIRE_CYCLE_MODULE,
ERR_REQUIRE_ESM,
ERR_REQUIRE_ESM_RACE_CONDITION,
ERR_UNKNOWN_MODULE_FORMAT,
},
setArrowMessage,
} = require('internal/errors');
const { getOptionValue } = require('internal/options');
const { isURL, pathToFileURL } = require('internal/url');
const { readFileSync } = require('fs');
const {
getDeprecationWarningEmitter,
getLazy,
kEmptyObject,
} = require('internal/util');
const {
Expand Down Expand Up @@ -78,6 +88,83 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
});

const { isPromise } = require('internal/util/types');
const lazyGetErrorSourceMessage =
getLazy(() => require('internal/errors/error_source').getErrorSourceMessage);

function getOrCreateModuleJobWithStackTraceLimit(loader, parentURL, request, limit) {
const originalLimit = Error.stackTraceLimit;
try {
if (originalLimit < limit) {
Error.stackTraceLimit = limit;
}
return loader.getOrCreateModuleJob(parentURL, request);
} finally {
Error.stackTraceLimit = originalLimit;
}
}

function decorateDynamicImportModuleNotFoundError(error, parentURL, specifier) {
if (error?.code !== 'ERR_MODULE_NOT_FOUND' ||
typeof parentURL !== 'string' ||
!StringPrototypeStartsWith(parentURL, 'file://')) {
return;
}

let filename;
try {
filename = urlToFilename(parentURL);
} catch {
return;
}

const stackLines = StringPrototypeSplit(error.stack, '\n');
let frame;
for (let i = 0; i < stackLines.length; i++) {
if (StringPrototypeStartsWith(stackLines[i], ' at ') &&
(StringPrototypeIndexOf(stackLines[i], parentURL) !== -1 ||
StringPrototypeIndexOf(stackLines[i], filename) !== -1)) {
frame = stackLines[i];
break;
}
}

const { 1: line, 2: col } =
RegExpPrototypeExec(/:(\d+):(\d+)\)?$/, frame) || [];
if (!line || !col) {
return;
}

let source;
try {
source = readFileSync(filename, 'utf8');
} catch {
return;
}

const sourceLine = StringPrototypeSplit(source, '\n', line)[line - 1];
if (sourceLine === undefined) {
return;
}

let column = StringPrototypeIndexOf(sourceLine, specifier, col - 1);
if (column === -1) {
column = StringPrototypeIndexOf(sourceLine, specifier);
}
if (column === -1) {
column = col - 1;
}

const message = lazyGetErrorSourceMessage()(
filename,
Number(line),
sourceLine,
column,
specifier.length,
);
if (message !== undefined) {
setArrowMessage(error, message);
}
}

/**
* @typedef {import('./hooks.js').AsyncLoaderHookWorker} AsyncLoaderHookWorker
Expand Down Expand Up @@ -612,11 +699,16 @@ class ModuleLoader {
const request = { specifier, phase, attributes: importAttributes, __proto__: null };
let moduleJob;
try {
moduleJob = await this.getOrCreateModuleJob(parentURL, request);
const maybeModuleJob =
typeof parentURL === 'string' && StringPrototypeStartsWith(parentURL, 'file://') ?
getOrCreateModuleJobWithStackTraceLimit(this, parentURL, request, 100) :
this.getOrCreateModuleJob(parentURL, request);
moduleJob = await maybeModuleJob;
} catch (e) {
if (e?.code === 'ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED') {
return new Promise(() => {});
}
decorateDynamicImportModuleNotFoundError(e, parentURL, specifier);
throw e;
}
if (phase === kSourcePhase) {
Expand Down
Loading
Loading