From 2142429fedd059a185423643e0badbe807273e86 Mon Sep 17 00:00:00 2001 From: Xuguang Mei Date: Wed, 22 Apr 2026 18:15:12 +0800 Subject: [PATCH 1/2] module: show user location for missing module errors, resolve #38892 --- lib/internal/errors/error_source.js | 80 +++++++++++++ lib/internal/modules/cjs/loader.js | 67 ++++++++++- lib/internal/modules/esm/loader.js | 106 ++++++++++++++++-- lib/internal/modules/esm/module_job.js | 84 +++++++++++++- src/module_wrap.cc | 14 ++- ...ynamic-import-not-found-error-location.mjs | 48 ++++++++ ...st-esm-module-not-found-error-location.mjs | 25 +++++ .../test-module-not-found-error-location.js | 47 ++++++++ 8 files changed, 454 insertions(+), 17 deletions(-) create mode 100644 test/es-module/test-esm-dynamic-import-not-found-error-location.mjs create mode 100644 test/es-module/test-esm-module-not-found-error-location.mjs create mode 100644 test/parallel/test-module-not-found-error-location.js diff --git a/lib/internal/errors/error_source.js b/lib/internal/errors/error_source.js index eddd6af230801b..934a5c354fe457 100644 --- a/lib/internal/errors/error_source.js +++ b/lib/internal/errors/error_source.js @@ -2,6 +2,11 @@ const { FunctionPrototypeBind, + MathMax, + MathMin, + NumberIsFinite, + RegExpPrototypeSymbolReplace, + StringPrototypeRepeat, StringPrototypeSlice, } = primordials; @@ -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. @@ -162,4 +241,5 @@ function getErrorSourceExpression(error) { module.exports = { getErrorSourceLocation, getErrorSourceExpression, + getErrorSourceMessage, }; diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 827655bedb65bf..13c3445117f669 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -38,6 +38,7 @@ const { Error, FunctionPrototypeCall, JSONParse, + Number, ObjectDefineProperty, ObjectFreeze, ObjectGetOwnPropertyDescriptor, @@ -57,7 +58,6 @@ const { StringPrototypeCharCodeAt, StringPrototypeEndsWith, StringPrototypeIndexOf, - StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, @@ -142,6 +142,7 @@ const { kEmptyObject, setOwnProperty, getLazy, + getStructuredStack, isWindows, isUnderNodeModules, } = require('internal/util'); @@ -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, @@ -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; }; @@ -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': @@ -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); + } } } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 876ea6535f2187..54cee52e3fbbb2 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -6,10 +6,15 @@ const { ArrayPrototypeReduce, FunctionPrototypeCall, JSONStringify, + Number, ObjectSetPrototypeOf, Promise, PromisePrototypeThen, + RegExpPrototypeExec, RegExpPrototypeSymbolReplace, + StringPrototypeIndexOf, + StringPrototypeSplit, + StringPrototypeStartsWith, encodeURIComponent, hardenRegExp, } = primordials; @@ -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 { @@ -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 @@ -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) { diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 11d5b0a3cb66f9..62f3fbf2f9dca2 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -11,10 +11,12 @@ const { PromiseResolve, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, + RegExpPrototypeSymbolSplit, SafePromiseAllReturnArrayLike, SafePromiseAllReturnVoid, SafeSet, StringPrototypeIncludes, + StringPrototypeIndexOf, StringPrototypeSplit, StringPrototypeStartsWith, globalThis, @@ -40,7 +42,10 @@ const { /** * @typedef {import('./utils.js').ModuleRequestType} ModuleRequestType */ -const { decorateErrorStack } = require('internal/util'); +const { + decorateErrorStack, + getLazy, +} = require('internal/util'); const { isPromise } = require('internal/util/types'); const { getSourceMapsSupport, @@ -54,9 +59,15 @@ const { const { getOptionValue } = require('internal/options'); const noop = FunctionPrototype; const { - ERR_REQUIRE_ASYNC_MODULE, - ERR_REQUIRE_ESM_RACE_CONDITION, -} = require('internal/errors').codes; + codes: { + ERR_REQUIRE_ASYNC_MODULE, + ERR_REQUIRE_ESM_RACE_CONDITION, + }, + setArrowMessage, +} = require('internal/errors'); +const { readFileSync } = require('fs'); +const lazyGetErrorSourceMessage = + getLazy(() => require('internal/errors/error_source').getErrorSourceMessage); let hasPausedEntry = false; const CJSGlobalLike = [ @@ -72,6 +83,60 @@ const findCommonJSGlobalLikeNotDefinedError = (errorMessage) => (globalLike) => errorMessage === `${globalLike} is not defined`, ); +/** + * @param {Error} error + * @param {string} parentURL + * @param {ModuleRequest} request + * @returns {void} + */ +function decorateModuleNotFoundError(error, parentURL, request) { + if (error?.code !== 'ERR_MODULE_NOT_FOUND' || + !StringPrototypeStartsWith(parentURL, 'file://') || + typeof request.lineNumber !== 'number' || + typeof request.columnNumber !== 'number') { + return; + } + + let filename; + try { + filename = urlToFilename(parentURL); + } catch { + return; + } + + let source; + try { + source = readFileSync(filename, 'utf8'); + } catch { + return; + } + + const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source); + const sourceLine = lines[request.lineNumber]; + if (sourceLine === undefined) { + return; + } + + let column = StringPrototypeIndexOf(sourceLine, request.specifier, request.columnNumber); + if (column === -1) { + column = StringPrototypeIndexOf(sourceLine, request.specifier); + } + if (column === -1) { + column = request.columnNumber; + } + + const message = lazyGetErrorSourceMessage()( + filename, + request.lineNumber + 1, + sourceLine, + column, + request.specifier.length, + ); + if (message !== undefined) { + setArrowMessage(error, message); + } +} + /** * @@ -160,7 +225,13 @@ class ModuleJobBase { const request = moduleRequests[idx]; // TODO(joyeecheung): split this into two iterators, one for resolving and one for loading so // that hooks can pre-fetch sources off-thread. - const job = this.loader.getOrCreateModuleJob(this.url, request, requestType); + let job; + try { + job = this.loader.getOrCreateModuleJob(this.url, request, requestType); + } catch (e) { + decorateModuleNotFoundError(e, this.url, request); + throw e; + } debug(`ModuleJobBase.syncLink() ${this.url} -> ${request.specifier}`, job); assert(!isPromise(job)); assert(job.module instanceof ModuleWrap); @@ -283,6 +354,9 @@ class ModuleJob extends ModuleJobBase { ArrayPrototypePush(evaluationDepJobs, job); } return job.modulePromise; + }, (e) => { + decorateModuleNotFoundError(e, this.url, request); + throw e; }); modulePromises[idx] = modulePromise; } diff --git a/src/module_wrap.cc b/src/module_wrap.cc index ef69fe133fad61..cd4ae22078220f 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -43,6 +43,7 @@ using v8::Just; using v8::JustVoid; using v8::Local; using v8::LocalVector; +using v8::Location; using v8::Maybe; using v8::MaybeLocal; using v8::MemorySpan; @@ -595,7 +596,10 @@ static Local createImportAttributesContainer( } static Local createModuleRequestsContainer( - Realm* realm, Isolate* isolate, Local raw_requests) { + Realm* realm, + Isolate* isolate, + Local module, + Local raw_requests) { EscapableHandleScope scope(isolate); Local context = realm->context(); LocalVector requests(isolate, raw_requests->Length()); @@ -606,6 +610,8 @@ static Local createModuleRequestsContainer( raw_requests->Get(realm->context(), i).As(); Local specifier = module_request->GetSpecifier(); + int source_offset = module_request->GetSourceOffset(); + Location location = module->SourceOffsetToLocation(source_offset); // Contains the import attributes for this request in the form: // [key1, value1, source_offset1, key2, value2, source_offset2, ...]. @@ -618,11 +624,15 @@ static Local createModuleRequestsContainer( realm->isolate_data()->specifier_string(), realm->isolate_data()->attributes_string(), realm->isolate_data()->phase_string(), + FIXED_ONE_BYTE_STRING(isolate, "lineNumber"), + FIXED_ONE_BYTE_STRING(isolate, "columnNumber"), }; Local values[] = { specifier, attributes, Integer::New(isolate, to_phase_constant(phase)), + Integer::New(isolate, location.GetLineNumber()), + Integer::New(isolate, location.GetColumnNumber()), }; DCHECK_EQ(arraysize(names), arraysize(values)); @@ -646,7 +656,7 @@ void ModuleWrap::GetModuleRequests(const FunctionCallbackInfo& args) { Local module = obj->module_.Get(isolate); args.GetReturnValue().Set(createModuleRequestsContainer( - realm, isolate, module->GetModuleRequests())); + realm, isolate, module, module->GetModuleRequests())); } // moduleWrap.link(moduleWraps) diff --git a/test/es-module/test-esm-dynamic-import-not-found-error-location.mjs b/test/es-module/test-esm-dynamic-import-not-found-error-location.mjs new file mode 100644 index 00000000000000..faadef3aee34aa --- /dev/null +++ b/test/es-module/test-esm-dynamic-import-not-found-error-location.mjs @@ -0,0 +1,48 @@ +import { spawnPromisified } from '../common/index.mjs'; +import tmpdir from '../common/tmpdir.js'; + +import assert from 'node:assert'; +import { writeFile } from 'node:fs/promises'; +import { execPath } from 'node:process'; + +tmpdir.refresh(); + +const missingPackage = 'this-package-does-not-exist'; + +async function assertDynamicImportNotFoundLocation(entry, sourceLine, underline) { + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [entry]); + + assert.strictEqual(code, 1); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, ''); + assert.ok(stderr.includes(`${entry}:2`)); + assert.match(stderr, new RegExp(sourceLine)); + assert.match(stderr, underline); + assert.match(stderr, /ERR_MODULE_NOT_FOUND/); +} + +const esmEntry = tmpdir.resolve('dynamic-entry.mjs'); +await writeFile(esmEntry, [ + 'const ok = 1;', + `await import(${JSON.stringify(missingPackage)});`, + 'console.log(ok);', +].join('\n')); + +await assertDynamicImportNotFoundLocation( + esmEntry, + `await import\\(${JSON.stringify(missingPackage)}\\);`, + / {14}\^{27}/, +); + +const cjsEntry = tmpdir.resolve('dynamic-entry.cjs'); +await writeFile(cjsEntry, [ + 'const ok = 1;', + `import(${JSON.stringify(missingPackage)});`, + 'ok;', +].join('\n')); + +await assertDynamicImportNotFoundLocation( + cjsEntry, + `import\\(${JSON.stringify(missingPackage)}\\);`, + / {8}\^{27}/, +); diff --git a/test/es-module/test-esm-module-not-found-error-location.mjs b/test/es-module/test-esm-module-not-found-error-location.mjs new file mode 100644 index 00000000000000..5ee5929c0e9706 --- /dev/null +++ b/test/es-module/test-esm-module-not-found-error-location.mjs @@ -0,0 +1,25 @@ +import { spawnPromisified } from '../common/index.mjs'; +import tmpdir from '../common/tmpdir.js'; + +import assert from 'node:assert'; +import { writeFile } from 'node:fs/promises'; +import { execPath } from 'node:process'; + +tmpdir.refresh(); + +const entry = tmpdir.resolve('entry.mjs'); +await writeFile(entry, [ + 'import ok from "node:fs";', + 'import missing from "this-package-does-not-exist";', + 'console.log(ok, missing);', +].join('\n')); + +const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [entry]); + +assert.strictEqual(code, 1); +assert.strictEqual(signal, null); +assert.strictEqual(stdout, ''); +assert.ok(stderr.includes(`${entry}:2`)); +assert.match(stderr, /import missing from "this-package-does-not-exist";/); +assert.match(stderr, / {21}\^{27}/); +assert.match(stderr, /ERR_MODULE_NOT_FOUND/); diff --git a/test/parallel/test-module-not-found-error-location.js b/test/parallel/test-module-not-found-error-location.js new file mode 100644 index 00000000000000..139ea3a2b2fd74 --- /dev/null +++ b/test/parallel/test-module-not-found-error-location.js @@ -0,0 +1,47 @@ +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); + +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const { writeFileSync } = require('fs'); + +tmpdir.refresh(); + +const entry = tmpdir.resolve('entry.cjs'); +writeFileSync(entry, [ + 'const ok = require("node:fs");', + 'const missing = require("this-package-does-not-exist");', + 'console.log(ok, missing);', +].join('\n')); + +const { status, signal, stdout, stderr } = spawnSync(process.execPath, [entry], { + encoding: 'utf8', +}); + +assert.strictEqual(status, 1); +assert.strictEqual(signal, null); +assert.strictEqual(stdout, ''); +assert.ok(stderr.includes(`${entry}:2`)); +assert.match(stderr, /const missing = require\("this-package-does-not-exist"\);/); +assert.match(stderr, / {25}\^{27}/); +assert.match(stderr, /MODULE_NOT_FOUND/); +assert.match(stderr, /Require stack:/); + +const longLineEntry = tmpdir.resolve('long-line-entry.cjs'); +writeFileSync(longLineEntry, [ + `const prefix = "${'x'.repeat(140)}"; require("this-package-does-not-exist");`, +].join('\n')); + +const longLineResult = spawnSync(process.execPath, [longLineEntry], { + encoding: 'utf8', +}); + +assert.strictEqual(longLineResult.status, 1); +assert.strictEqual(longLineResult.signal, null); +assert.strictEqual(longLineResult.stdout, ''); +assert.ok(longLineResult.stderr.includes(`${longLineEntry}:1`)); +assert.match(longLineResult.stderr, /\.\.\.x+"; require\("this-package-does-not-exist"\);/); +assert.match(longLineResult.stderr, / {43}\^{27}/); +assert.match(longLineResult.stderr, /MODULE_NOT_FOUND/); From 457eab72adf99d75146754021a536425f10b2f04 Mon Sep 17 00:00:00 2001 From: Xuguang Mei Date: Thu, 23 Apr 2026 13:41:27 +0800 Subject: [PATCH 2/2] module: add more test cases --- ...ynamic-import-not-found-error-location.mjs | 30 +++++++++++++++++-- ...st-esm-module-not-found-error-location.mjs | 30 ++++++++++++++----- .../test-internal-errors-error-source.js | 26 ++++++++++++++++ .../test-module-not-found-error-location.js | 18 +++++++++++ 4 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-internal-errors-error-source.js diff --git a/test/es-module/test-esm-dynamic-import-not-found-error-location.mjs b/test/es-module/test-esm-dynamic-import-not-found-error-location.mjs index faadef3aee34aa..154d05cb85d059 100644 --- a/test/es-module/test-esm-dynamic-import-not-found-error-location.mjs +++ b/test/es-module/test-esm-dynamic-import-not-found-error-location.mjs @@ -9,13 +9,13 @@ tmpdir.refresh(); const missingPackage = 'this-package-does-not-exist'; -async function assertDynamicImportNotFoundLocation(entry, sourceLine, underline) { +async function assertDynamicImportNotFoundLocation(entry, sourceLine, underline, line = 2) { const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [entry]); assert.strictEqual(code, 1); assert.strictEqual(signal, null); assert.strictEqual(stdout, ''); - assert.ok(stderr.includes(`${entry}:2`)); + assert.ok(stderr.includes(`${entry}:${line}`)); assert.match(stderr, new RegExp(sourceLine)); assert.match(stderr, underline); assert.match(stderr, /ERR_MODULE_NOT_FOUND/); @@ -46,3 +46,29 @@ await assertDynamicImportNotFoundLocation( `import\\(${JSON.stringify(missingPackage)}\\);`, / {8}\^{27}/, ); + +const variableEntry = tmpdir.resolve('dynamic-variable-entry.mjs'); +await writeFile(variableEntry, [ + 'Error.stackTraceLimit = 200;', + `const pkg = ${JSON.stringify(missingPackage)};`, + 'await import(pkg);', +].join('\n')); + +await assertDynamicImportNotFoundLocation( + variableEntry, + 'await import\\(pkg\\);', + / {13}\^{27}/, + 3, +); + +const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--input-type=module', + '--eval', + `await import(${JSON.stringify(missingPackage)})`, +]); + +assert.strictEqual(code, 1); +assert.strictEqual(signal, null); +assert.strictEqual(stdout, ''); +assert.match(stderr, /ERR_MODULE_NOT_FOUND/); +assert.doesNotMatch(stderr, /await import/); diff --git a/test/es-module/test-esm-module-not-found-error-location.mjs b/test/es-module/test-esm-module-not-found-error-location.mjs index 5ee5929c0e9706..25e76d2e9f06e9 100644 --- a/test/es-module/test-esm-module-not-found-error-location.mjs +++ b/test/es-module/test-esm-module-not-found-error-location.mjs @@ -14,12 +14,26 @@ await writeFile(entry, [ 'console.log(ok, missing);', ].join('\n')); -const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [entry]); +async function assertModuleNotFoundLocation(args) { + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, args); -assert.strictEqual(code, 1); -assert.strictEqual(signal, null); -assert.strictEqual(stdout, ''); -assert.ok(stderr.includes(`${entry}:2`)); -assert.match(stderr, /import missing from "this-package-does-not-exist";/); -assert.match(stderr, / {21}\^{27}/); -assert.match(stderr, /ERR_MODULE_NOT_FOUND/); + assert.strictEqual(code, 1); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, ''); + assert.ok(stderr.includes(`${entry}:2`)); + assert.match(stderr, /import missing from "this-package-does-not-exist";/); + assert.match(stderr, / {21}\^{27}/); + assert.match(stderr, /ERR_MODULE_NOT_FOUND/); +} + +await assertModuleNotFoundLocation([entry]); + +await assertModuleNotFoundLocation([ + '--no-warnings', + '--import', + 'data:text/javascript,' + + 'import{register}from"node:module";' + + 'register("data:text/javascript,' + + 'export async function resolve(s,c,n){return n(s,c)}")', + entry, +]); diff --git a/test/parallel/test-internal-errors-error-source.js b/test/parallel/test-internal-errors-error-source.js new file mode 100644 index 00000000000000..f958dbf55fb97a --- /dev/null +++ b/test/parallel/test-internal-errors-error-source.js @@ -0,0 +1,26 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); + +const assert = require('assert'); +const { getErrorSourceMessage } = require('internal/errors/error_source'); + +assert.strictEqual( + getErrorSourceMessage('file.js', 1, undefined, 0, 1), + undefined, +); +assert.strictEqual( + getErrorSourceMessage('file.js', Number.NaN, 'const value = 1;', 0, 1), + undefined, +); + +assert.strictEqual( + getErrorSourceMessage('file.js', 1, '\tconst value = 1;', -5, 0), + 'file.js:1\n\tconst value = 1;\n^\n', +); + +assert.strictEqual( + getErrorSourceMessage('file.js', 7, `${'x'.repeat(20)}target${'y'.repeat(140)}`, 20, 200), + `file.js:7\n${'x'.repeat(20)}target${'y'.repeat(94)}...\n${' '.repeat(20)}${'^'.repeat(100)}\n`, +); diff --git a/test/parallel/test-module-not-found-error-location.js b/test/parallel/test-module-not-found-error-location.js index 139ea3a2b2fd74..268027e37cba09 100644 --- a/test/parallel/test-module-not-found-error-location.js +++ b/test/parallel/test-module-not-found-error-location.js @@ -45,3 +45,21 @@ assert.ok(longLineResult.stderr.includes(`${longLineEntry}:1`)); assert.match(longLineResult.stderr, /\.\.\.x+"; require\("this-package-does-not-exist"\);/); assert.match(longLineResult.stderr, / {43}\^{27}/); assert.match(longLineResult.stderr, /MODULE_NOT_FOUND/); + +const variableEntry = tmpdir.resolve('variable-entry.cjs'); +writeFileSync(variableEntry, [ + 'const pkg = "this-package-does-not-exist";', + 'require(pkg);', +].join('\n')); + +const variableResult = spawnSync(process.execPath, [variableEntry], { + encoding: 'utf8', +}); + +assert.strictEqual(variableResult.status, 1); +assert.strictEqual(variableResult.signal, null); +assert.strictEqual(variableResult.stdout, ''); +assert.ok(variableResult.stderr.includes(`${variableEntry}:2`)); +assert.match(variableResult.stderr, /require\(pkg\);/); +assert.match(variableResult.stderr, /^\^{27}$/m); +assert.match(variableResult.stderr, /MODULE_NOT_FOUND/);