Skip to content

TypeScript: expand ${configDir} and compute rootDir the way tsc does - #14

Merged
0xfauzi merged 8 commits into
mainfrom
claude/new-contributor-triage-141j1e
Sep 26, 2026
Merged

0xfauzi merged 8 commits into
mainfrom
claude/new-contributor-triage-141j1e

Conversation

@0xfauzi

@0xfauzi 0xfauzi commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Fixes #13, and removes the guess at rootDir that #13's first commit left in place.

Two commits. The first expands ${configDir}. The second replaces the guessed source root with the one the project's own tsc would use. Both rules were measured with the compiler before they were written, and each has a test whose docstring states the acceptance first.

Commit 1: expand ${configDir} in inherited tsconfig paths

${configDir} stands for the folder of the top-level tsconfig.json, the one the project owns, so a shared base config can point at the project that extends it. Before, the text was kept as written, so @sindresorhus/tsconfig left ky's outDir at node_modules/@sindresorhus/tsconfig/${configDir}/distribution, a folder that does not exist.

The rule, from tsc --showConfig on TypeScript 7.0.2 with the variable at three levels of an extends chain:

value written in tsc resolved it to
${configDir}/source (rootDir) npm package ./source under the top-level folder
${configDir}/typings (typeRoots) nested config ./typings under the top-level folder
./local-typings (typeRoots) nested config ./config/nested/local-typings, the declaring folder
${configDir}/src/* (paths) npm package the absolute top-level src/*
lib/* (paths) npm package lib/*, unchanged, relative to baseUrl
cache/${configDir}/x (tsBuildInfoFile) nested config left literal, resolved from the declaring folder

So: in every file of the chain, a baseUrl, paths target, rootDir or outDir value that starts with the variable becomes the top-level folder; anywhere else it stays literal; a plain relative value still means the declaring file's folder. The same holds for files, include and exclude, which commit 2 reads.

Commit 2: compute rootDir the way the project's tsc does

With rootDir unset, the reader used to guess src/ or the repository root. That is a guess, and on a TypeScript 5 project with include: ["source"] it maps out/source/cli.js to a file that tsc would never have produced. The new test shows that case failing on the old code.

What tsc does, measured by where it emits with rootDir unset, on ten layouts:

layout 5.9.3 6.0.3 7.0.2
all sources under source/ root is source/ root is the tsconfig folder, and error TS5011 asks for an explicit rootDir root is the tsconfig folder
source/ and scripts/ the common folder, the repository the tsconfig folder the tsconfig folder
include: ["source"] with scripts/ present source/ the tsconfig folder, TS5011 the tsconfig folder
files: ["source/index.ts"] source/
exclude: ["scripts"] source/
a .d.ts outside source/ source/ (declarations do not count)
a test file under test/ the repository (tests count) the tsconfig folder the tsconfig folder
composite: true the tsconfig folder the tsconfig folder the tsconfig folder
include: ["${configDir}/source"] in a base config source/

Blank cells were not run; the 6 and 7 columns did not vary with the input set in any run.

The rule the reader now follows, in root_candidates:

  1. An explicit rootDir is the answer.
  2. composite means the tsconfig folder, on every version.
  3. Otherwise the project's TypeScript major decides. 5 takes the longest common folder of the non-declaration files that files, include and exclude select, test files included. 6 and later take the tsconfig folder.
  4. The major is read from node_modules/typescript/package.json, then from the typescript range in package.json (^5.9.3 names 5). When neither exists, both roots are tried, and a compiled target maps to a module only when exactly one root names one. Two matches leave the target unmapped, because which root tsc used is not knowable, and a wrong answer costs more than none.

The input-file selection is checked against tsc

files, include and exclude follow tsconfig glob rules: **/ crosses folders, * and ? stay inside one, a folder entry means everything under it, and outDir is always left out. To make sure this matches the compiler rather than my reading of the docs, I ran tsc --listFilesOnly (5.9.3) on five of the pinned repositories and compared the non-declaration source files it lists with input_files:

repository tsc input_files only in tsc only in input_files
ky 31 31 0 0
taxonomy 123 123 0 0
zod 467 467 0 0
nest 1073 1073 0 0
excalidraw 637 637 0 0

hono and date-fns are solution-style configs (files: [] or include: [] with references), so both tsc and input_files see no inputs there; systemap does not follow references, and on those two the major (6 and 7) picks the tsconfig folder anyway.

Effect on the pinned repositories

bench/typescript_adapter.py gives the same counts as the run recorded on #12 for all seven, every column. ky is the one whose path changed: its include: ["source"] and TypeScript ^5.9.3 give root source/, and with the src//source/ fallback disabled its seven package entries still map, through outDir and rootDir alone. Before this commit they only mapped through the fallback.

The extra walk input_files makes costs 0.05 s on zod and 0.21 s on nest, measured in the container.

Verification

  • uv run pytest -q: 400 passed, including the two new tests.
  • uv run pre-commit run with each commit staged against its parent: every hook passes, including the complexity and file-length ratchets.
  • uv run mypy src/systemap: clean.
  • uv run systemap refresh, extract --check, check, judgement --strict, render --check: pass; docs/map/ is updated.

Branch

This branch sits on #12's current head 084122e, because typescript_config.py only exists there. Until #12 merges, this PR shows #12's six commits plus two. After #12 merges, I will rebase so only these two remain.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CzLdCRXKdAP2swLysEfacu

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

What this change does to the map

83aabb818069b62c3cce16dea34dcb3d4a0ebda7 to 91e63d5d26a09fcb856d217aca5ad1056bbb16b4: 8 modules changed, 5 added, 0 removed, 0 moved; 2 of 16 cards named.

Changed, nothing to do (5)

  • added: systemap.language, claimed by FactsExtractor
  • added: systemap.language_config, claimed by Config
  • added: systemap.typescript, claimed by FactsExtractor
  • added: systemap.typescript_config, claimed by Config
  • added: systemap.typescript_surface, claimed by FactsExtractor

Next to the change

FactsExtractor holds most of what changed, and a flow joins it to 9 of the 16 cards, so naming them would say nothing.

the map at 91e63d5

The committed map at 91e63d5; the change map itself is systemap figure --mode change --base 83aabb8 --out change.svg.
Nothing to decide: the map already covers this change. systemap refresh brings the facts up to date.

@0xfauzi 0xfauzi changed the title TypeScript: expand ${configDir} in inherited tsconfig paths TypeScript: expand ${configDir} and compute rootDir the way tsc does Sep 26, 2026
pdd27673 and others added 3 commits September 26, 2026 20:44
A shared base tsconfig cannot know where the project that extends it
lives, so TypeScript lets a path option start with ${configDir}, the
folder of the top-level tsconfig.json. The reader kept the text as
written, so @sindresorhus/tsconfig left ky's outDir pointing inside
node_modules at a folder that does not exist, and a compiled package
entry could only be mapped back to its source by guessing src/ or
source/.

The rule comes from tsc --showConfig (7.0.2), not from memory: the
variable counts only at the start of a value, in every file of the
extends chain, and always means the top-level folder. A value such as
cache/${configDir}/x is left literal, and a plain relative value still
means the folder of the file that declares it.

The test states its acceptance before the fix and fails on the previous
head. bench/typescript_adapter.py gives the same counts as the run
recorded on #12 for all seven repositories.

Fixes #13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzLdCRXKdAP2swLysEfacu
With rootDir unset, the reader guessed src/ or the repository root. On
a TypeScript 5 project with include: ["source"] that maps a compiled
entry to a file tsc never produced, and a wrong answer costs more than
none.

The rule was measured before it was written, by where tsc emits with
rootDir unset on ten layouts. 5.9.3 takes the longest common folder of
the non-declaration files that files, include and exclude select, test
files included. 6.0.3 and 7.0.2 take the tsconfig folder, and 6 reports
TS5011 asking for an explicit rootDir. composite means the tsconfig
folder on every version.

So root_candidates reads the project's TypeScript major, from
node_modules/typescript first and the package.json range second, and
applies that version's rule. With neither, both roots are tried and a
compiled target maps only when exactly one fits; two matches leave it
unmapped rather than pick one.

input_files follows the tsconfig glob rules, and on ky, taxonomy, zod,
nest and excalidraw it returns exactly the files tsc --listFilesOnly
lists: 2331 files, none missing, none extra. Counts on all seven pinned
repositories are unchanged; ky's seven package entries now map through
outDir and rootDir alone, where before only the src/ and source/
fallback found them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzLdCRXKdAP2swLysEfacu
@0xfauzi
0xfauzi force-pushed the claude/new-contributor-triage-141j1e branch from 6665a6f to 91e63d5 Compare September 26, 2026 19:50
@0xfauzi
0xfauzi merged commit 56716e9 into main Sep 26, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeScript: expand ${configDir} in inherited tsconfig paths

3 participants