Repository: nodejs/node
Pull request: util: fix error's namespaced node_modules highlighting using inspect

When inspecting errors, node_modules are highlighted with an underscore. So far namespaced modules only highlighted the namespace but not the rest of the module name. This is fixed by matching the full name.
As drive-by it improves the performance slightly by removing the regular expression in favor of indexOf to identify the right spot.

Changed files:
filename: lib/internal/util/inspect.js
patch: @@ -250,7 +250,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
@@ -1423,16 +1422,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;
 }

filename: test/parallel/test-util-inspect.js
patch: @@ -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.<anonymous> (/test/node_modules/foo/node_modules/bar/baz.js:2:7)',
+    '    at A.<anonymous> (/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`;