Skip to content

tsconfig with glob include + path aliases → all aliases silently dropped #502

Description

@shivss26

Ran /understand on a TanStack Start project and noticed the graph was missing a ton of import edges — turned out only relative imports (./foo) were showing up, and everything using our #/… path alias resolved to nothing. About half our internal imports just weren't in the graph, and a quarter of the nodes ended up orphaned.

Tracked it down to the comment stripping in parseTsConfigText() (skills/understand/extract-import-map.mjs). It strips JSONC comments with /\/\*[\s\S]*?\*\//g, which isn't string-aware — so the /* inside an include glob like "**/*.ts" gets read as the start of a comment, and it happily deletes everything up to the next */ (which is sitting in "**/*.tsx"). That mangles the JSON, JSON.parse throws, the raw fallback throws too (real /* */ comments this time), and the function just returns null — so every compilerOptions.paths alias gets dropped on the floor. You see it as a failed to parse — path aliases will not be applied warning.

The annoying part is **/*.ts includes are basically in every tsconfig, so I think this hits any project that uses path aliases + a glob include, not just ours.

Minimal repro — a tsconfig with:

{
  "include": ["**/*.ts", "**/*.tsx"],
  "compilerOptions": {
    "paths": { "#/*": ["./src/*"] }
    /* any block comment */
  }
}

→ aliases dropped, #/… imports resolve to [].

Fix is just making the strip string-aware (skip /* */ and // when you're inside a string), or swapping in a real JSONC parser like jsonc-parser. I tried a ~15-line string-aware version locally and the same project jumped from 105 → 222 internal import edges with nothing else changed. Patch:

@@ -148,11 +148,28 @@
  */
 function parseTsConfigText(raw) {
   // tsconfig.json often contains JSONC-style comments; strip line and block
-  // comments before parsing. The strip is naive (it doesn't honor string
-  // contents), so we fall back to the raw text on failure.
-  const stripped = raw
-    .replace(/\/\*[\s\S]*?\*\//g, '')
-    .replace(/(^|[^:])\/\/.*$/gm, '$1');
+  // comments before parsing. String-aware strip (does NOT misfire on `/*` that
+  // appears inside string literals like the `**/*.ts` globs in `include`), then
+  // drop trailing commas.
+  let stripped = '';
+  {
+    let inStr = false, esc = false;
+    for (let i = 0; i < raw.length; i++) {
+      const c = raw[i], n = raw[i + 1];
+      if (inStr) {
+        stripped += c;
+        if (esc) esc = false;
+        else if (c === '\\') esc = true;
+        else if (c === '"') inStr = false;
+        continue;
+      }
+      if (c === '"') { inStr = true; stripped += c; continue; }
+      if (c === '/' && n === '*') { i += 2; while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/')) i++; i++; continue; }
+      if (c === '/' && n === '/') { while (i < raw.length && raw[i] !== '\n') i++; stripped += '\n'; continue; }
+      stripped += c;
+    }
+    stripped = stripped.replace(/,(\s*[}\]])/g, '$1');
+  }
   let parsed;
   try {
     parsed = JSON.parse(stripped);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions