diff --git a/lib/events.js b/lib/events.js index dafcdea7b57..b79c8a36cdc 100644 --- a/lib/events.js +++ b/lib/events.js @@ -22,7 +22,6 @@ 'use strict'; const { - ArrayPrototypeIndexOf, ArrayPrototypeJoin, ArrayPrototypeShift, ArrayPrototypeSlice, @@ -33,7 +32,6 @@ const { ErrorCaptureStackTrace, FunctionPrototypeBind, FunctionPrototypeCall, - MathMin, NumberIsNaN, ObjectCreate, ObjectDefineProperty, @@ -55,7 +53,10 @@ const kRejection = SymbolFor('nodejs.rejection'); const { kEmptyObject } = require('internal/util'); -const { inspect } = require('internal/util/inspect'); +const { + inspect, + identicalSequenceRange, +} = require('internal/util/inspect'); let spliceOne; @@ -424,31 +425,6 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return _getMaxListeners(this); }; -// Returns the length and line number of the first sequence of `a` that fully -// appears in `b` with a length of at least 4. -function identicalSequenceRange(a, b) { - for (let i = 0; i < a.length - 3; i++) { - // Find the first entry of b that matches the current entry of a. - const pos = ArrayPrototypeIndexOf(b, a[i]); - if (pos !== -1) { - const rest = b.length - pos; - if (rest > 3) { - let len = 1; - const maxLen = MathMin(a.length - i, rest); - // Count the number of consecutive entries. - while (maxLen > len && a[i + len] === b[pos + len]) { - len++; - } - if (len > 3) { - return [len, i]; - } - } - } - } - - return [0, 0]; -} - function enhanceStackTrace(err, own) { let ctorInfo = ''; try { @@ -465,9 +441,9 @@ function enhanceStackTrace(err, own) { const ownStack = ArrayPrototypeSlice( StringPrototypeSplit(own.stack, '\n'), 1); - const { 0: len, 1: off } = identicalSequenceRange(ownStack, errStack); + const { len, offset } = identicalSequenceRange(ownStack, errStack); if (len > 0) { - ArrayPrototypeSplice(ownStack, off + 1, len - 2, + ArrayPrototypeSplice(ownStack, offset + 1, len - 2, ' [... lines matching original stack trace ...]'); } diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 6a5d7b15cd2..bf22769f228 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -146,6 +146,12 @@ const { } = require('internal/validators'); let hexSlice; +let internalUrl; + +function pathToFileUrlHref(filepath) { + internalUrl ??= require('internal/url'); + return internalUrl.pathToFileURL(filepath).href; +} const builtInObjects = new SafeSet( ArrayPrototypeFilter( @@ -1283,6 +1289,58 @@ function removeDuplicateErrorKeys(ctx, keys, err, stack) { } } +function markNodeModules(ctx, line) { + let tempLine = ''; + let nodeModule; + let pos = 0; + while ((nodeModule = nodeModulesRegExp.exec(line)) !== null) { + // '/node_modules/'.length === 14 + tempLine += line.slice(pos, nodeModule.index + 14); + tempLine += ctx.stylize(nodeModule[1], 'module'); + pos = nodeModule.index + nodeModule[0].length; + } + if (pos !== 0) { + line = tempLine + line.slice(pos); + } + return line; +} + +function markCwd(ctx, line, workingDirectory) { + let cwdStartPos = line.indexOf(workingDirectory); + let tempLine = ''; + let cwdLength = workingDirectory.length; + if (cwdStartPos !== -1) { + if (line.slice(cwdStartPos - 7, cwdStartPos) === 'file://') { + cwdLength += 7; + cwdStartPos -= 7; + } + const start = line[cwdStartPos - 1] === '(' ? cwdStartPos - 1 : cwdStartPos; + const end = start !== cwdStartPos && line.endsWith(')') ? -1 : line.length; + const workingDirectoryEndPos = cwdStartPos + cwdLength + 1; + const cwdSlice = line.slice(start, workingDirectoryEndPos); + + tempLine += line.slice(0, start); + tempLine += ctx.stylize(cwdSlice, 'undefined'); + tempLine += line.slice(workingDirectoryEndPos, end); + if (end === -1) { + tempLine += ctx.stylize(')', 'undefined'); + } + } else { + tempLine += line; + } + return tempLine; +} + +function safeGetCWD() { + let workingDirectory; + try { + workingDirectory = process.cwd(); + } catch { + return; + } + return workingDirectory; +} + function formatError(err, constructor, tag, ctx, keys) { const name = err.name != null ? String(err.name) : 'Error'; let stack = getStackString(err); @@ -1306,25 +1364,30 @@ function formatError(err, constructor, tag, ctx, keys) { stack = `[${stack}]`; } else { let newStack = stack.slice(0, stackStart); - const lines = getStackFrames(ctx, err, stack.slice(stackStart + 1)); + const stackFramePart = stack.slice(stackStart + 1); + const lines = getStackFrames(ctx, err, stackFramePart); if (ctx.colors) { // Highlight userland code and node modules. - for (const line of lines) { + const workingDirectory = safeGetCWD(); + let esmWorkingDirectory; + for (let line of lines) { const core = line.match(coreModuleRegExp); if (core !== null && NativeModule.exists(core[1])) { newStack += `\n${ctx.stylize(line, 'undefined')}`; } else { - // This adds underscores to all node_modules to quickly identify them. - let nodeModule; newStack += '\n'; - let pos = 0; - while ((nodeModule = nodeModulesRegExp.exec(line)) !== null) { - // '/node_modules/'.length === 14 - newStack += line.slice(pos, nodeModule.index + 14); - newStack += ctx.stylize(nodeModule[1], 'module'); - pos = nodeModule.index + nodeModule[0].length; + + line = markNodeModules(ctx, line); + if (workingDirectory !== undefined) { + let newLine = markCwd(ctx, line, workingDirectory); + if (newLine === line) { + esmWorkingDirectory ??= pathToFileUrlHref(workingDirectory); + newLine = markCwd(ctx, line, esmWorkingDirectory); + } + line = newLine; } - newStack += pos === 0 ? line : line.slice(pos); + + newStack += line; } } } else { @@ -2290,10 +2353,11 @@ function stripVTControlCharacters(str) { } module.exports = { + identicalSequenceRange, inspect, + inspectDefaultOptions, format, formatWithOptions, getStringWidth, - inspectDefaultOptions, - stripVTControlCharacters + stripVTControlCharacters, }; diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index c7cb51b221c..9b57fd4a99b 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -30,6 +30,7 @@ const v8 = require('v8'); const { previewEntries } = internalBinding('util'); const { inspect } = util; const { MessageChannel } = require('worker_threads'); +const url = require('url'); assert.strictEqual(util.inspect(1), '1'); assert.strictEqual(util.inspect(false), 'false'); @@ -2811,9 +2812,15 @@ assert.strictEqual( } { + const originalCWD = process.cwd(); + + process.cwd = () => (process.platform === 'win32' ? + 'C:\\workspace\\node-test-binary-windows js-suites-%percent-encoded\\node' : + '/home/user directory/repository%encoded/node'); + // Use a fake stack to verify the expected colored outcome. const stack = [ - 'TypedError: Wonderful message!', + 'Error: CWD is grayed out, even cwd that are percent encoded!', ' at A. (/test/node_modules/foo/node_modules/bar/baz.js:2:7)', ' at Module._compile (node:internal/modules/cjs/loader:827:30)', ' at Fancy (node:vm:697:32)', @@ -2823,23 +2830,78 @@ assert.strictEqual( // This file is not an actual Node.js core file. ' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)', ' at require (node:internal/modules/cjs/helpers:14:16)', + ' at Array.forEach ()', + ` at ${process.cwd()}/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (${process.cwd()}/node_modules/hyper_module/folder/file.js:2753:10)`, ' at /test/test-util-inspect.js:2239:9', ' at getActual (node:assert:592:5)', ]; - const isNodeCoreFile = [ - false, false, true, true, false, true, false, true, false, true, - ]; - const err = new TypeError('Wonderful message!'); + const err = new Error('CWD is grayed out, even cwd that are percent encoded!'); err.stack = stack.join('\n'); + if (process.platform === 'win32') { + err.stack = stack.map((frame) => (frame.includes('node:') ? + frame : + frame.replaceAll('/', '\\')) + ).join('\n'); + } + const escapedCWD = util.inspect(process.cwd()).slice(1, -1); util.inspect(err, { colors: true }).split('\n').forEach((line, i) => { - let actual = stack[i].replace(/node_modules\/([a-z]+)/g, (a, m) => { + let expected = stack[i].replace(/node_modules\/([^/]+)/gi, (_, m) => { return `node_modules/\u001b[4m${m}\u001b[24m`; + }).replaceAll(new RegExp(`(\\(?${escapedCWD}(\\\\|/))`, 'gi'), (_, m) => { + return `\x1B[90m${m}\x1B[39m`; }); - if (isNodeCoreFile[i]) { - actual = `\u001b[90m${actual}\u001b[39m`; + if (expected.includes(process.cwd()) && expected.endsWith(')')) { + expected = `${expected.slice(0, -1)}\x1B[90m)\x1B[39m`; } - assert.strictEqual(actual, line); + if (line.includes('node:')) { + if (!line.includes('foo') && !line.includes('aaa')) { + expected = `\u001b[90m${expected}\u001b[39m`; + } + } else if (process.platform === 'win32') { + expected = expected.replaceAll('/', '\\'); + } + assert.strictEqual(line, expected); }); + + // Check ESM + const encodedCwd = url.pathToFileURL(process.cwd()); + const sl = process.platform === 'win32' ? '\\' : '/'; + + // Use a fake stack to verify the expected colored outcome. + err.stack = 'Error: ESM and CJS mixed are both grayed out!\n' + + ` at ${encodedCwd}/test/parallel/test-esm.mjs:2760:12\n` + + ` at Object. (${encodedCwd}/node_modules/esm_module/folder/file.js:2753:10)\n` + + ` at ${process.cwd()}${sl}test${sl}parallel${sl}test-cjs.js:2760:12\n` + + ` at Object. (${process.cwd()}${sl}node_modules${sl}cjs_module${sl}folder${sl}file.js:2753:10)`; + + let actual = util.inspect(err, { colors: true }); + let expected = 'Error: ESM and CJS mixed are both grayed out!\n' + + ` at \x1B[90m${encodedCwd}/\x1B[39mtest/parallel/test-esm.mjs:2760:12\n` + + ` at Object. \x1B[90m(${encodedCwd}/\x1B[39mnode_modules/\x1B[4mesm_module\x1B[24m/folder/file.js:2753:10\x1B[90m)\x1B[39m\n` + + ` at \x1B[90m${process.cwd()}${sl}\x1B[39mtest${sl}parallel${sl}test-cjs.js:2760:12\n` + + ` at Object. \x1B[90m(${process.cwd()}${sl}\x1B[39mnode_modules${sl}\x1B[4mcjs_module\x1B[24m${sl}folder${sl}file.js:2753:10\x1B[90m)\x1B[39m`; + + assert.strictEqual(actual, expected); + + // ESM without need for encoding + process.cwd = () => (process.platform === 'win32' ? + 'C:\\workspace\\node-test-binary-windows-js-suites\\node' : + '/home/user/repository/node'); + let expectedCwd = process.cwd(); + if (process.platform === 'win32') { + expectedCwd = `/${expectedCwd.replaceAll('\\', '/')}`; + } + // Use a fake stack to verify the expected colored outcome. + err.stack = 'Error: ESM without need for encoding!\n' + + ` at file://${expectedCwd}/file.js:15:15`; + + actual = util.inspect(err, { colors: true }); + expected = 'Error: ESM without need for encoding!\n' + + ` at \x1B[90mfile://${expectedCwd}/\x1B[39mfile.js:15:15`; + assert.strictEqual(actual, expected); + + process.cwd = originalCWD; } { diff --git a/test/pseudo-tty/console_colors.out b/test/pseudo-tty/console_colors.out index 8766302ffd7..006eb9edfe6 100644 --- a/test/pseudo-tty/console_colors.out +++ b/test/pseudo-tty/console_colors.out @@ -1,38 +1,38 @@ -{ foo: *[32m'bar'*[39m } +{ foo: [32m'bar'[39m } string q -{ foo: *[32m'bar'*[39m } with object format param +{ foo: [32m'bar'[39m } with object format param Error: test at abc (../fixtures/node_modules/bar.js:4:4) foobar - at * (*console_colors.js:*:*) -*[90m at * (node:internal*:*:*)*[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m + at Object. [90m(*[39m*console_colors.js:*:*[90m)[39m +[90m at * (node:internal*:*:*)[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m Error: Should not ever get here. - at * (*node_modules*[4m*node_modules*[24m*bar.js:*:*) -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m - at * (*console_colors.js:*:*) -*[90m at *[39m -*[90m at *[39m + at Object. [90m(*node_modules*[4m*node_modules*[24m*bar.js:*:*[90m)[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m + at Object. [90m(*console_colors.js:*:*[90m)[39m +[90m at *[39m +[90m at *[39m Error at evalmachine.:*:* -*[90m at Script.runInThisContext (node:vm:*:*)*[39m -*[90m at Object.runInThisContext (node:vm:*:*)*[39m - at * (*console_colors.js:*:*) -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m -*[90m at *[39m +[90m at Script.runInThisContext (node:vm:*:*)[39m +[90m at Object.runInThisContext (node:vm:*:*)[39m + at Object. [90m(*[39m*console_colors.js:*:*[90m)[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m +[90m at *[39m diff --git a/test/pseudo-tty/test-fatal-error.out b/test/pseudo-tty/test-fatal-error.out index b6eeca17f55..c6c93adfb6a 100644 --- a/test/pseudo-tty/test-fatal-error.out +++ b/test/pseudo-tty/test-fatal-error.out @@ -3,12 +3,12 @@ throw err; ^ TypeError: foobar - at Object. (*test-fatal-error.js:*) -*[90m at *(node:internal*loader:*:*)*[39m -*[90m at *(node:internal*loader:*:*)*[39m -*[90m at *(node:internal*loader:*:*)*[39m -*[90m at *(node:internal*loader:*:*)*[39m -*[90m at *[39m -*[90m at *[39m { - bla: *[33mtrue*[39m + at Object. [90m(*test-fatal-error.js:*:*[90m)[39m +[90m at *(node:internal*loader:*:*)[39m +[90m at *(node:internal*loader:*:*)[39m +[90m at *(node:internal*loader:*:*)[39m +[90m at *(node:internal*loader:*:*)[39m +[90m at *[39m +[90m at *[39m { + bla: [33mtrue[39m }