Skip to content

tsconfig "~/*" and "@/*" path aliases silently destroy compilerOptions.paths (JSONC stripper is not string-aware) #620

Description

@zorgy28

Summary

parseTsConfigText() in skills/understand/extract-import-map.mjs strips JSONC comments with a regex that is not string-aware. A tsconfig path alias "~/*" (or "@/*") contains the literal /*, which the block-comment regex treats as a comment opener. It scans forward to the next */ — typically inside "**/*.ts" in include — and deletes everything between, destroying compilerOptions.paths.

No comments are required to trigger this. A path alias plus an include glob is enough, and both ship in the stock Next.js tsconfig.json.

Version: 2.9.4

The code

const stripped = raw
  .replace(/\/\*[\s\S]*?\*\//g, '')     // <-- not string-aware
  .replace(/(^|[^:])\/\/.*$/gm, '$1');

JSON.parse(stripped) then throws, the raw-text fallback also fails (real JSONC comments), and parseTsConfigText returns null — so the config contributes zero aliases.

Reproducer

const raw = `{
  "compilerOptions": { "baseUrl": ".", "paths": { "~/*": ["./src/*"] } },
  "include": ["**/*.ts"]
}`;

const stripped = raw
  .replace(/\/\*[\s\S]*?\*\//g, '')
  .replace(/(^|[^:])\/\/.*$/gm, '$1');

console.log(stripped);
// {
//   "compilerOptions": { "baseUrl": ".", "paths": { "~*.ts"]
// }

JSON.parse(stripped);
// SyntaxError: Expected ':' after property name in JSON at position 59

Impact

The only signal is one stderr line:

Warning: extract-import-map: tsconfig.json at <path> failed to parse —
path aliases from this config will not be applied — relative imports unaffected

After that, every aliased import silently fails to resolve. Nothing downstream errors — the knowledge graph is just quietly missing most of its import relationships.

Measured on a real Next.js project (386 ~/… imports across 140 files, vs 209 relative imports):

before after fix
totalEdges 190 528
filesWithImports 99 209

Roughly two thirds of the import graph was missing, with no error surfaced.

Suggested fix

Replace the regex pair with a single-pass stripper that tracks in-string state and backslash escapes:

function stripJsonCommentsOutsideStrings(text) {
  let out = '';
  let inString = false;
  let escaped = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    if (inString) {
      out += c;
      if (escaped) escaped = false;
      else if (c === '\\') escaped = true;
      else if (c === '"') inString = false;
      continue;
    }
    if (c === '"') { inString = true; out += c; continue; }
    if (c === '/' && text[i + 1] === '*') {
      const end = text.indexOf('*/', i + 2);
      i = end === -1 ? text.length : end + 1;
      continue;
    }
    if (c === '/' && text[i + 1] === '/') {
      const nl = text.indexOf('\n', i);
      if (nl === -1) break;
      i = nl - 1;
      continue;
    }
    out += c;
  }
  return out;
}

Then const stripped = stripJsonCommentsOutsideStrings(raw);.

Verified against: a real Next.js tsconfig with ~/*, an @/* alias, a URL containing //, a line comment, and an escaped quote inside a string — 5/5 parse correctly and retain paths.

Related

A sibling copy of the same naive regex exists in stripJsLikeComments() (used for require() extraction). Its docstring acknowledges the limitation and the blast radius is much smaller, so it may not be worth changing — flagging it only for completeness.


Found while analysing a production TypeScript + Python repo with /understand. Happy to open a PR with the fix and a regression test if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions