ADR-0040: A TypeScript 7 tsconfig resolves as TypeScript resolves it where dependency-cruiser's tsconfig-paths departs; the tsconfig's module decides what is compiled for acorn
- Status: Accepted (2026-10-08, by the owner)
- Date: 2026-09-26
- Derives from: the project's promise, "superset, precisely; nothing is dropped"; ADR-0009 (the upstream suites are the specification), ADR-0012 (oxc parses and
oxc_resolverresolves), ADR-0036 (the precedent for a behaviour that departs from upstream) - Constrains:
crates/rb-extract-ts(lib.rsload_tsconfigandcheck_module,pipeline.rsTsCompilerOptionsandcommonjs_output), conformance/divergences.md - Implemented by: PR #36:
crates/rb-extract-ts, with the fixturestests/options/ts7-configandtests/options/ts-config-module - Requirements: FR-EXT-TS-02, FR-EXT-TS-03
Context
TypeScript 7.0, the native port of the 6.0 compiler, is the latest tag on npm. Its source syntax is 6.0's; what changes for an extractor is the tsconfig. baseUrl is gone, so paths resolve against the folder of the config that declares them, ${configDir} names the folder of the config that is being compiled, and moduleResolution is bundler or one of the node16 to nodenext family. The amd, umd, system and none module kinds, deprecated in 6.0, are removed.
Rulebearing never loads the typescript package. Two things decide whether a TypeScript 7 repository extracts correctly: how oxc_resolver reads the tsconfig, and how the extractor models what dependency-cruiser's transpileModule step hands acorn when tsPreCompilationDeps is off. Both were measured against dependency-cruiser 18.2.0 with TypeScript 6.0.3 (the pin gate 1 records), with the fixtures tests/options/ts7-config and tests/options/ts-config-module. dependency-cruiser cannot load TypeScript 7, whose package does not carry the JavaScript API its transpileModule and tsc parser call; its manifest declares typescript >=2.0.0 <7.0.0.
The compiled module kind. dependency-cruiser spreads the tsconfig's own compilerOptions over its defaults (src/extract/transpile/typescript-wrap.mjs). So module: commonjs, node16, node18, node20 and nodenext, and an unset module with an ES3 or ES5 target, turn every static import and re-export into a require for acorn. commonjs turns import() into one too. export * as ns is lowered to an import only under ES2015 modules, and a tsconfig module overrides the nodenext default of the .mts flavour. The extractor read none of this: it lowered .mts only, whatever the tsconfig said. This was a parity gap, not a decision, and it is closed. TypeScript 7 makes it common, because nodenext is the setting its documentation gives Node projects.
Two resolutions where TypeScript and dependency-cruiser disagree. dependency-cruiser resolves paths through tsconfig-paths-webpack-plugin 4.2.0 and tsconfig-paths 4.2.0:
| Case | TypeScript 7 (and 6) | dependency-cruiser 18.2.0 | Cause upstream |
|---|---|---|---|
paths inherited through extends, no baseUrl (the shape of a TypeScript 7 monorepo's shared base config) |
against the declaring config's folder; ${configDir} expanded |
unresolved | tsconfig-loader.js rebases an inherited baseUrl but never inherited paths, and does not expand ${configDir} |
a bare specifier (src/util.js) that no paths key matches, no baseUrl |
unresolved (a package lookup) | resolved against the tsconfig's folder, type undetermined |
the plugin calls createMatchPathAsync without addMatchAll, which defaults to true and adds a match-all * whether or not baseUrl is set |
oxc_resolver gives TypeScript's answer in both cases. Before TypeScript 7 a repository that used paths almost always set baseUrl, which tsconfig-paths handles correctly, so neither difference was reachable from gate 1's suites or from the layer 5 oracles.
Decision
- The tsconfig's
moduleandtargetdecide what acorn reads, astranspileModuledecides it upstream:TsCompilerOptions::emitmaps them toEs2015,Es2020,NodeorCommonJs, and the forms are lowered to match. This is parity and needs no divergence row. amd,umd,systemandnonestop the run (exit 2, the tsconfig named, the fix given) when a TypeScript file will be compiled for acorn, that is unlesstsPreCompilationDepsistrueor the parser istscorswc. Their output wraps imports in a loader that the lowering does not model, and upstream's treatment ofnonediffers between.tsand.mts. Answering them as ES modules would be the silent wrong answer the working agreement forbids. TypeScript 7 does not accept them, so no TypeScript 7 repository reaches this error.- Where
tsconfig-pathsdeparts from TypeScript, the extractor follows TypeScript, under both configuration formats. The two cases in the table are recorded as permanent rows in conformance/divergences.md for any layer 5 oracle that reaches them, each citing its upstream cause, and the fixtures assert TypeScript's answer with the upstream answer named in their doc comments.
Consequences
- A TypeScript 7 repository extracts correctly with
tsPreCompilationDepson or off. A repository withmodule: nodenextandtsPreCompilationDepsoff gets the samerequireedges dependency-cruiser reports, where it previously gotimportedges. - A dependency-cruiser configuration over a TypeScript 7 monorepo resolves inherited
pathswhere dependency-cruiser leaves them unresolved, so anot-to-unresolvablerule that dependency-cruiser reports there is not reported by Rulebearing. This departs from the drop-in promise in the direction of the compiler, which is the one the repository's own build already agrees with. - A bare specifier that resolved in dependency-cruiser only through the implicit match-all stays unresolved. Code that compiles under TypeScript cannot contain one, except where the same name also exists as a package, in which case both TypeScript and Rulebearing resolve the package.
- A repository still on
amd,umd,systemornonethat ran before this change now exits 2 unless it setstsPreCompilationDeps: true. Its edges were wrong before (importwhere dependency-cruiser reportsamd-defineorrequire).
Alternatives considered
- Reproduce
tsconfig-pathsfor a dependency-cruiser configuration and follow TypeScript for a native one, as ADR-0036 splits Markdown fences. Rejected: ADR-0036 kept a documented, tested upstream behaviour, but these two are resolver defects with no upstream test that asserts them. Reproducing them would mean reimplementing a second resolver besideoxc_resolverto produce unresolved edges for code that compiles, and only for a TypeScript that dependency-cruiser itself cannot load. - Treat the removed module kinds as ES modules. Rejected: a silent wrong answer.
- Model
amdandumdoutput (definedependencies plus therequireandexportspseudo-modules that dependency-cruiser then reports as unresolved). Rejected for now: TypeScript has removed them, and no oracle uses them. A later ADR can add them if a user needs them.