Skip to content

fix: resolve tsconfig per file & map package imports from outDir to rootDir - #847

Open
sahiljadhav7 wants to merge 4 commits into
privatenumber:masterfrom
sahiljadhav7:fix/per-file-tsconfig
Open

sahiljadhav7 wants to merge 4 commits into
privatenumber:masterfrom
sahiljadhav7:fix/per-file-tsconfig

Conversation

@sahiljadhav7

Copy link
Copy Markdown

Fixes #696, fixes #714, fixes #482

What was wrong

Monorepos (#696, #482). tsx loaded a single tsconfig, found by walking up from the current directory. Files outside that tsconfig got no tsconfig at all, so a workspace package lost its own settings (e.g. jsx: react-jsx → "React is not defined"). Running from a directory without a tsconfig also meant the entry file's own tsconfig was ignored. Worse, the app's paths were applied to other packages' imports. A dependency's ~/lib/label could quietly load the app's ~/lib/label file with no error.

Package imports (#714). With "imports": { "#~/*": "./dist/*" }, tsx looked in dist/ while tsc maps back to src/. With no build you got ERR_MODULE_NOT_FOUND. With an old build lying around, tsx silently ran the stale output.

What changed

  • Each file now uses the tsconfig that owns it, the same way the TypeScript language server picks one (getTsconfig(file, { includes: true }), already in get-tsconfig v5).
  • Path aliases and allowJs come from the tsconfig nearest the importing file. Scripts outside include, --eval, stdin and the REPL keep resolving aliases just as before.
  • --tsconfig / TSX_TSCONFIG_PATH behave exactly as before: one config for the whole run.
  • # imports that land in outDir/declarationDir resolve to the rootDir source first, so stale builds are never picked up. Like TypeScript, this only happens when the tsconfig belongs to the importing package and the target isn't in node_modules. Without rootDir, the tsconfig's directory is the source root.
  • Both the ESM and CJS loaders are covered.

Tests

Added tests in tests/specs/tsconfig.ts, run for both ESM and CJS packages. They cover:

  • workspace dependencies compiled with their own tsconfig;
  • running from a directory with no tsconfig;
  • the alias hijack case;
  • # imports mapped to source, including the stale-build case;
  • the new package-scope and node_modules guards;
  • behaviour that has to stay the same: --eval, stdin, scripts outside include, explicit --tsconfig, and no rootDir.

Each test failed before its fix. For the compatibility tests, I reintroduced each regression on purpose and checked that the right tests failed.

Full suite locally on Node 24.2.0: the same 8 tests fail on master and on this branch, so nothing new breaks. The failures seem to come from running 24.2 instead of the pinned 24.15.

Notes for review

  • Some files outside a tsconfig's include used to get no tsconfig. They now get the tsconfig that actually owns them. This is the intended fix, but it is a visible behaviour change.
  • This goes the other way from fix(esm/hook/resolve): tsconfig when in node_modules #768, which drops the node_modules guard on aliases. Per-file lookup covers that use case without applying the root config to every dependency.

sahiljadhav7 and others added 4 commits September 27, 2026 08:38
## Problem
tsx loaded a single tsconfig found from the current working directory.
Files not included by it were compiled with no tsconfig, so workspace
packages lost their own compiler options (e.g. `jsx`), and running from
a directory without a tsconfig ignored the entry's tsconfig. Path
aliases of that one tsconfig also applied to every non-dependency
importer, so a workspace package's alias could silently resolve to a
file in another package.

## Changes
- `createTsconfigLookup()` replaces the single parsed tsconfig with two
  lookups:
  - `compile(file)`: nearest tsconfig that includes the file (TypeScript
    language server behavior)
  - `resolve(importer)`: nearest tsconfig to the importer, for `paths` &
    `allowJs`. Doesn't require inclusion, so scripts outside `include`,
    `--eval`, stdin and REPL keep resolving aliases as before
- An explicit `--tsconfig` / `TSX_TSCONFIG_PATH` keeps the single-config
  behavior
- ESM & CJS loaders use the lookup

Fixes privatenumber#696
Fixes privatenumber#482

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## Problem
Package imports (`#`) that target the tsconfig outDir, e.g.
`"imports": { "#~/*": "./dist/*" }`, resolved to the compiled output.
Without a build it failed with ERR_MODULE_NOT_FOUND, and with a stale
build it silently loaded outdated code. TypeScript maps these targets
back to rootDir.

## Changes
- Map `#` import targets inside outDir/declarationDir to rootDir
  (tsconfig directory when rootDir isn't set), mirroring TypeScript's
  `tryLoadInputFileForPath`
- The source is tried before the compiled target, so a stale build
  isn't loaded
- Uses the importer's tsconfig, in both ESM & CJS loaders

Fixes privatenumber#714

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## Problem
Package imports targeting outDir were mapped to rootDir with whichever
tsconfig was nearest the importer, even one outside the package (e.g. a
parent tsconfig whose outDir contains it). With an explicit tsconfig,
imports from dependencies could also be mapped. Both could load an
unrelated source file instead of the package's own file.

## Changes
Match TypeScript's tryLoadInputFileForPath guards:
- Only map when the tsconfig is inside the importing package
- Never map targets inside node_modules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guard behavior that per-file tsconfig resolution must keep:
- `--eval` & stdin resolve path aliases from the cwd tsconfig
- scripts outside `include` still resolve path aliases
- an explicit `--tsconfig` applies over a nearer tsconfig
- package imports map from outDir to the tsconfig directory when
  rootDir isn't set

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant