JavaScript: honour the project's tsconfig when resolving modules - #8733
Merged
knutwannheden merged 3 commits intoSep 1, 2026
Conversation
JavaScriptParser built a fixed set of ts.CompilerOptions and never read the project's tsconfig.json, so a `types` entry naming a declaration package outside node_modules/@types — how UI5 ships its API surface — and every `paths` alias resolved to nothing. Resolution failure is TS2307, outside the range checkSyntaxErrors gates on, so type-aware recipes reported zero findings and exited successfully. Each input file is matched to the nearest tsconfig.json or jsconfig.json at or below relativeTo, inputs are grouped by it, and one ts.Program is built per group. Grouping forces two related fixes: JavaScriptTypeMapping deduplicates by ts.Type.id, which is only unique within the TypeChecker that issued it, so it becomes per-program rather than per-batch; and the old program kept for incremental reuse becomes a map keyed by config. Only resolution-affecting options are read. `target`, `lib` and `strict` change what types look like rather than what a specifier finds, and each would only narrow attribution. `module` is excluded for a sharper reason: a project's own module kind classifies files as CJS or ESM, and an ESM-only import from a CJS file is then TS1479, which is inside the critical range and costs the whole file its LST. Where a project states only `module`, the resolution TypeScript pairs with it is adopted without the module kind itself; `Classic` is declined, searching no node_modules. A default baseUrl cannot be carried over a project's `paths`, since baseUrl takes precedence over pathsBasePath and would resolve a nested project's aliases against the repository root. Two trades this makes, both measured. A project declaring a modern moduleResolution loses deep-subpath imports into packages that wall them off behind an `exports` map, which Node10 reached — the project's own tsc refuses them too, and Node enforces the map at runtime. And `types` is restrictive as well as additive: stating it disables the implicit @types sweep, so a project narrowing `types` loses ambient declarations it gets today. Loading a large declaration package also roughly doubles to triples one-time program construction, per batch rather than per file. Discovery is gated on an explicit relativeTo. Without one the search would walk up from the process working directory and apply whatever config sits above it — under vitest, this repository's own tsconfig. With no relativeTo or no config file the options are byte-identical to the defaults. The compiler host also answers directoryExists for directories holding only in-memory inputs, which module resolution probes before the files in them.
…res-the-project-tsconfig # Conflicts: # rewrite-javascript/rewrite/src/javascript/parser.ts
A project that states `moduleResolution: node16` was sending every import through the `require` branch of every dual package: `ts.resolveModuleName` was called with no resolution mode, and TypeScript only substitutes ESNext for an absent mode under `Bundler`. An ESM consumer that tsc types against a package's `import` branch was typed against its `require` branch instead — wrong attribution rather than missing attribution, which counts cannot see. The mode now comes from the usage location, which under the pinned `preserve` module kind is ESNext for every file, so a project reads the same branch whatever resolution it declares. The shared `ts.SourceFile` cache is keyed by module format alongside path. A file's format is baked in at creation and follows from the requesting project's `moduleResolution`, so with several projects per batch the first one to load a dependency otherwise fixed its format for the rest. A config that cannot be read now leaves its files on the parser defaults, grouped with every other file that has no config. `parseJsonConfigFileContent` could throw out of the generator and lose the batch, and the error path returned options that differed from the no-config project only by retaining `baseUrl`, which bought that project a second identical program. Also: `parseJsonConfigFileContent` gets a host whose `readDirectory` answers empty, since only `options` is read and `ts.sys` would walk the whole tree under each config; root names are collected from the deduplicated inputs rather than the raw list; and a directory named `..cache` is no longer read as an escape from the project root.
knutwannheden
deleted the
javascriptparser-ignores-the-project-tsconfig
branch
September 1, 2026 08:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
JavaScriptParserbuilds a fixed set ofts.CompilerOptionsand never reads the project'stsconfig.json. Atypesentry naming a declaration package outsidenode_modules/@types— how UI5 ships its entire API surface — and everypathsalias therefore resolve to nothing. Nothing inrewrite-javascript/rewrite/src/callsts.readConfigFile,ts.parseJsonConfigFileContentorts.findConfigFile. Resolution failure is TS2307, outside the rangecheckSyntaxErrorsgates on, so a type-aware recipe reports zero findings and exits successfully — indistinguishable from a clean codebase.Measured through the parser, same files, same
node_modules:How it works
Each input is matched to the nearest
tsconfig.jsonorjsconfig.jsonat or belowrelativeTo, inputs are grouped by it, and onets.Programis built per group. Grouping forces two related fixes:JavaScriptTypeMappingdeduplicates byts.Type.id, which is only unique within theTypeCheckerthat issued it, so it becomes per-program rather than per-batch; and the retained program for incremental reuse becomes a map keyed by config.Only resolution-affecting options are read —
baseUrl,paths,pathsBasePath,rootDirs,types,typeRoots,moduleResolution,moduleSuffixes,customConditions,resolvePackageJsonExports/Imports,resolveJsonModule.target,libandstrictstay with the parser: they change the shape of types rather than where a specifier resolves, and each would only narrow attribution.moduleis excluded for a sharper reason. A project's module kind classifies files as CJS or ESM, and an ESM-only import from a CJS file is then TS1479 — inside the critical range, so the file yields aParseErrorand no LST at all. Losing every symbol in a file is worse than losing one import's types, and the diagnostic family is too large to allowlist:ts.Diagnosticshas 37 codes in the 1000–2000 band tied to module-kind classification. Where a project states onlymodule, the resolution TypeScript pairs with it is adopted without the module kind itself;Classicis declined, since it searches nonode_modules.Export conditions come from the usage location rather than from
moduleResolutionalone. Without that, a project statingnode16sends every import through therequirebranch of every dual package, because TypeScript only substitutes ESNext for an absent mode underBundler— wrong attribution rather than missing attribution.Discovery is gated on
relativeToWithout a project root there is no search to bound, and walking up from the process working directory would apply whatever config sits above it — under vitest, this repository's own
tsconfig.json, which statesmodule: Node16, to all 2262 tests. With norelativeToor no config file the options are byte-identical to the defaults, so the change is inert for every project that has no tsconfig.What this deliberately trades
One cost, measured.
typesis restrictive as well as additive: the parser defaults totypes: ["*"], and a project stating the key replaces that rather than adding to it, so a repo whose tsconfig says"types": ["node", "jest"]loses the ambient declarations of every other installed@types/*package. That is whattscdoes for them, and it is the same mechanism that makes UI5 work, but it is a narrowing. Loading a large declaration package also roughly doubles to triples one-time program construction, per batch rather than per file.A stated
moduleResolution: node/node10is honoured literally rather than floored at the parser default. Those projects keep deep-subpath imports into packages that wall them off behind anexportsmap — which theBundlerdefault refuses — and in exchange stay unable to see packages that publish their types only through anexportscondition. Flooring them would swap one loss for the other, so it is a trade rather than a fix, and honouring what the project declares is the consistent answer. It can change later without touching this design.Worth stating what is not a cost here, since it is easy to assume otherwise: no project loses deep-subpath resolution relative to
main. TheBundlerdefault already refuses those, from #8672; this PR only ever recovers them, for projects that declarenode10.Tests
Seven tests in
test/javascript/tsconfig.test.ts, each mutation-checked to confirm it is killed by removing the line it claims to defend, and by no other: thetypesentry in both directions,baseUrl, per-project grouping, a statedmoduleResolutionthat resolves differently from the default, amodulekind that sets resolution without being adopted, the absence of a Node10 fallback, andjsconfig.jsonwithtsconfig.jsonoutranking it. Two further tests were written and then dropped once mutation testing showed they were subsumed.The fixtures are hand-built packages rather than real dependencies, so they need no install: a declaration package outside
@types/using the interface-and-const shape@sapui5/typesuses forsap/ui/core/Theming, a package walling its subpaths behindexports, one publishing different typings per condition, and one whose subpath needs a directory-index lookup.Also fixed
The compiler host now answers
directoryExistsfor directories holding only in-memory inputs, which module resolution probes before the files in them. Without it, any in-memory source under a directory absent from disk was unreachable — silently losing cross-file attribution in tests that do not write sources to disk, which is every test using thenpm()helper.