diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 78318243e65eca..e3e6f5ef967593 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -244,7 +244,6 @@ const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; const numberRegExp = /^(0|[1-9][0-9]*)$/; const coreModuleRegExp = /^ {4}at (?:[^/\\(]+ \(|)node:(.+):\d+:\d+\)?$/; -const nodeModulesRegExp = /[/\\]node_modules[/\\](.+?)(?=[/\\])/g; const classRegExp = /^(\s+[^(]*?)\s*{/; // eslint-disable-next-line node-core/no-unescaped-regexp-dot @@ -1412,16 +1411,45 @@ 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 += StringPrototypeSlice(line, pos, nodeModule.index + 14); - tempLine += ctx.stylize(nodeModule[1], 'module'); - pos = nodeModule.index + nodeModule[0].length; - } - if (pos !== 0) { - line = tempLine + StringPrototypeSlice(line, pos); + let lastPos = 0; + let searchFrom = 0; + + while (true) { + const nodeModulePosition = StringPrototypeIndexOf(line, 'node_modules', searchFrom); + if (nodeModulePosition === -1) { + break; + } + + // Ensure it's a path segment: must have a path separator before and after + const separator = line[nodeModulePosition - 1]; + const after = line[nodeModulePosition + 12]; // 'node_modules'.length === 12 + + if ((after !== '/' && after !== '\\') || (separator !== '/' && separator !== '\\')) { + // Not a proper segment; continue searching + searchFrom = nodeModulePosition + 1; + continue; + } + + const moduleStart = nodeModulePosition + 13; // Include trailing separator + + // Append up to and including '/node_modules/' + tempLine += StringPrototypeSlice(line, lastPos, moduleStart); + + let moduleEnd = StringPrototypeIndexOf(line, separator, moduleStart); + if (line[moduleStart] === '@') { + // Namespaced modules have an extra slash: @namespace/package + moduleEnd = StringPrototypeIndexOf(line, separator, moduleEnd + 1); + } + + const nodeModule = StringPrototypeSlice(line, moduleStart, moduleEnd); + tempLine += ctx.stylize(nodeModule, 'module'); + + lastPos = moduleEnd; + searchFrom = moduleEnd; + } + + if (lastPos !== 0) { + line = tempLine + StringPrototypeSlice(line, lastPos); } return line; } diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index b06f6814e4985a..5aafb4378c18e1 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -2837,7 +2837,7 @@ assert.strictEqual( // Use a fake stack to verify the expected colored outcome. const stack = [ '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 A. (/test/node_modules/foo/node_modules/@namespace/bar/baz.js:2:7)', ' at Module._compile (node:internal/modules/cjs/loader:827:30)', ' at Fancy (node:vm:697:32)', // This file is not an actual Node.js core file. @@ -2862,7 +2862,7 @@ assert.strictEqual( } const escapedCWD = util.inspect(process.cwd()).slice(1, -1); util.inspect(err, { colors: true }).split('\n').forEach((line, i) => { - let expected = stack[i].replace(/node_modules\/([^/]+)/gi, (_, 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`;