feat: add DTS resolver matching TypeScript's bundler mode#997
feat: add DTS resolver matching TypeScript's bundler mode#997
Conversation
…ler"` Add `resolve_dts()` method to `ResolverGeneric` that implements TypeScript's `ts.resolveModuleName` algorithm for declaration file resolution. This replaces the need for JS workarounds that configure enhanced-resolve with DTS-friendly options but can't match TypeScript's actual resolution behavior. Key features: - Two-pass node_modules walk: TS/DTS + @types before JS - @types scoped name mangling (@babel/core -> @types/babel__core) - TypeScript extension substitution (.js -> .ts, .d.ts) - typesVersions package.json field support - exports field absolute priority (blocks types/typings/main) - NAPI bindings: resolveDtsSync / resolveDtsAsync Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #997 +/- ##
==========================================
- Coverage 94.02% 91.62% -2.41%
==========================================
Files 17 18 +1
Lines 3348 3880 +532
==========================================
+ Hits 3148 3555 +407
- Misses 200 325 +125 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR adds a DTS resolver that implements TypeScript's ts.resolveModuleName with moduleResolution: "bundler" algorithm. The implementation addresses a critical gap for tools like rolldown-plugin-dts that need to inline and bundle type declarations, and solves numerous edge cases that cannot be handled by workarounds using the existing JavaScript resolver.
Changes:
- Implements a new
resolve_dts()method with TypeScript-specific resolution logic including two-pass node_modules walk, @types scoped name mangling, TypeScript extension substitution, typesVersions support, and proper exports field handling - Adds package.json field accessors for
types,typings, andtypesVersionsfields in both simd and serde implementations - Exposes
resolveDtsSyncandresolveDtsAsyncmethods through NAPI bindings with comprehensive documentation
Reviewed changes
Copilot reviewed 18 out of 41 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/dts_resolver.rs | Core DTS resolution algorithm implementation with TypeScript-specific logic |
| src/tests/dts_resolver.rs | Comprehensive test suite with 20 test cases covering relative resolution, extension priority, @types, exports, typesVersions, and name mangling |
| src/package_json/simd.rs | Added types(), typings(), and types_versions() accessors for SIMD JSON parser |
| src/package_json/serde.rs | Added types(), typings(), and types_versions() accessors for serde JSON parser |
| napi/src/lib.rs | NAPI bindings for resolve_dts_sync and resolve_dts_async |
| napi/index.d.ts | TypeScript type definitions for DTS resolution methods |
| src/tests/mod.rs | Added dts_resolver test module |
| src/lib.rs | Added dts_resolver module |
| fixtures/dts_resolver/* | Test fixtures for DTS resolution scenarios |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// `specifier` is the module specifier string. | ||
| /// | ||
| /// The resolver uses the existing `ResolveOptions` for: | ||
| /// - `condition_names` (used for `exports` resolution; `"types"` is always added) |
There was a problem hiding this comment.
The documentation states that "types" is always added to condition_names, but this is not implemented in the code. Users must manually include "types" in their ResolveOptions.condition_names (as shown in the test at src/tests/dts_resolver.rs:13). Either update the documentation to reflect the actual behavior, or implement the automatic addition of "types" to condition_names when calling resolve_dts.
| let mut entry = if extensions.contains(Extensions::DECLARATION) { | ||
| pkg.typings().or_else(|| pkg.types()) | ||
| } else { | ||
| None | ||
| }; | ||
| if entry.is_none() | ||
| && extensions.intersects( | ||
| Extensions::TYPESCRIPT | ||
| .union(Extensions::JAVASCRIPT) | ||
| .union(Extensions::DECLARATION), | ||
| ) | ||
| { | ||
| entry = pkg.main_fields(&main_fields).next(); | ||
| } | ||
|
|
||
| let vp_specifier = entry.unwrap_or("index"); | ||
| if let Some(path) = self.dts_resolve_via_version_paths( | ||
| extensions, | ||
| vp_specifier, | ||
| candidate, | ||
| &version_paths, | ||
| ctx, | ||
| )? { | ||
| return Ok(Some(path)); | ||
| } | ||
| } | ||
|
|
||
| // Determine entry file (types/typings/main) | ||
| if let Some(ref pkg) = pkg { | ||
| let mut entry = if extensions.contains(Extensions::DECLARATION) { | ||
| pkg.typings().or_else(|| pkg.types()) | ||
| } else { | ||
| None | ||
| }; | ||
| if entry.is_none() | ||
| && extensions.intersects( | ||
| Extensions::TYPESCRIPT | ||
| .union(Extensions::JAVASCRIPT) | ||
| .union(Extensions::DECLARATION), | ||
| ) | ||
| { | ||
| entry = pkg.main_fields(&main_fields).next(); | ||
| } |
There was a problem hiding this comment.
There is duplicate logic for determining the entry field (types/typings/main). Lines 390-403 and 419-432 contain nearly identical code. Consider extracting this into a helper method to reduce duplication and improve maintainability.
| } | ||
| _ => { | ||
| // Unknown extensions like .vue, .svelte - return them for d.{ext}.ts handling | ||
| if !ext.is_empty() && ext.len() < 10 { Some(ext) } else { None } |
There was a problem hiding this comment.
Consider extracting the magic number 10 to a named constant like MAX_UNKNOWN_EXTENSION_LENGTH to improve code readability and make the intent clearer.
Summary
resolve_dts()method toResolverGenericimplementing TypeScript'sts.resolveModuleNamewithmoduleResolution: "bundler"algorithmnode_moduleswalk: all ancestors for TS/DTS +@typesbefore trying JS — fixes the fundamental algorithmic difference that JS workarounds can't address@typesscoped name mangling (@babel/core→@types/babel__core).js→.ts,.d.ts) following TS priority ordertypesVersionspackage.json field supportexportsfield absolute priority (blockstypes/typings/mainwhen present)resolveDtsSync/resolveDtsAsync@types, exports,typesVersions,typingsfield, and name manglingCloses #549
🤖 Generated with Claude Code