feat: modernization using OXC + Rolldown ecosystem#211
Conversation
…dencies - Replaced custom path resolution logic with oxc-resolver for improved performance. - Migrated formatting from Prettier to oxfmt, updating configuration accordingly. - Enabled isolated declarations in TypeScript configuration. - Updated package dependencies, including TypeScript to version 6.0 RC. - Removed unused dependencies and files, including globrex and mappings.ts. - Enhanced CI workflow with formatting and linting checks.
|
Hi @aleclarson -- I am working on an OXC + Rolldown modernization of AnalogJS I will thoroughly test usage of this overhaul on a |
Follow-up cleanup from code review (reuse / quality / efficiency audit)Ran three parallel review passes — code reuse, quality, and efficiency — focused on regression risk. Four issues found and fixed: Dead code removal:
Event loop fix:
Performance guard:
Peer dependency:
All 6 fixture tests pass, 0 lint warnings, 0 type errors. |
- Removed unused parameters and types from the resolver and log file. - Updated the resolver to use async resolution for improved performance. - Enhanced logging events by removing unnecessary details.
- Introduced a mechanism to coalesce concurrent resolutions for the same specifier, reducing duplicate native calls during parallel pre-bundling. - Added an inflight resolutions map to track ongoing resolution promises. - Enhanced logging to capture resolution events more effectively.
Promise coalescing for async resolution cacheThe switch from oxcResolver.sync() to await oxcResolver.async() introduced a TOCTOU gap on the resolutionCache. During Vite's parallel pre-bundling phase, multiple modules can resolve the same specifier concurrently — both calls miss the cache, both dispatch native resolution Added an inflightResolutions Map that stores the in-flight Promise. Concurrent callers for the same specifier now share a single native resolution call. The entry is cleaned up in a finally block once the resolution settles. Before (async without coalescing): After (with coalescing): No other issues found in this round. The remaining review findings were either pre-existing (resolutionCache growth bounded by import graph size, cleared on tsconfig reload) or false positives (debug.enabled inconsistency at call sites that only pass cheap string args). |
- Upgraded `debug` to version 4.4.3, `tsconfck` to version 3.1.6, `@types/debug` to version 4.1.12, `execa` to version 9.6.1, `rimraf` to version 6.1.3, and `vitest` to version 4.1.0. - Adjusted `vite-tsconfig-paths` specifier to 'link:' in both package files. - Updated corresponding versions in pnpm-lock.yaml to reflect changes in package.json.
The lockfile recorded `vite: *` but package.json declares `vite: ">=5.0.0"` as a peer dependency. This mismatch causes ERR_PNPM_OUTDATED_LOCKFILE when installing this package as a git-hosted dependency with --frozen-lockfile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-tsconfig-paths into feat/oxc-upgrade
|
Looks really great! Less deps, more performance |
|
Thanks for the PR.
Can you write a benchmark comparing the current plugin (v6) with your rewrite? Note that Vite 8 has tsconfig paths resolution built-in, so idk if a rewrite of this plugin is worth it. |
Yes and I agree but projects on older Vite might be able to take advantage. IDK why people won't update but ... just saying. |
|
@aleclarson -- I ran both a direct plugin benchmark and a real-world Analog benchmark against the pre-rewrite and rewrite revisions. 🚀 🔀 Commits compared
Analog adoption:
🧪 Plugin benchmarkFor the main upstream benchmark I used the existing
Each timed run includes both TypeScript declaration emit and a Vite build for every fixture. Environment:
I verified that all 6 fixtures pass on both revisions before timing. ✅
The first row is the most important one, since it exercises the full compatibility surface instead of only the package build pipeline. 🎯 🏗️ Real-world Analog benchmarkI also benchmarked the actual Analog targets that currently load
Those were measured through the real Nx commands, not an ad hoc script:
Environment:
I verified the targets pass on both revisions before timing. ✅
📌 Interpretation
|
|
@aleclarson need anything else from me for this to go forward? |
Updated the resolver logic to skip implicit .d.ts file resolutions that are not explicitly imported, allowing for better handling of type overrides defined in tsconfig files. This change prevents potential issues while still permitting explicit imports of declaration files.
|
thanks for the reminder. published as future PRs should target the |
|
i generated release notes here: https://github.com/aleclarson/vite-tsconfig-paths/releases/tag/v7.0.0-alpha.1 if you spot any errors, let me know and i'll correct it! 👍 |
v7.0.0-alpha.1 — OXC + Rolldown modernization
Summary
mappings.ts+globrex) withoxc-resolverfor native, high-performance tsconfig path resolutionresolveIdhook filters to skip unnecessary Rust-to-JS boundary callssrc/mappings.tsentirely (replaced by oxc-resolver internals)isolatedDeclarationsand useoxc-transformfor fast native.d.tsgenerationglobrex,esbuild,rollup,prettier,tsup)packageManagerandenginesfieldsMotivation
The plugin's resolution pipeline previously worked in three layers:
mappings.tscompilescompilerOptions.pathsinto RegExp matchers usingglobrexthis.resolve()) verifies each candidate path exists on diskSteps 2 and 3 are now replaced by a single
oxcResolver.sync()call. oxc-resolver is a Rust port of enhanced-resolve with built-in tsconfig paths support — it handles pattern matching, wildcard substitution, extension resolution, and filesystem verification in one native call.Performance
oxc-resolver is 28x faster than webpack's enhanced-resolve (benchmark). For this plugin specifically, the improvement comes from:
this.resolve()(which itself traverses the full Vite plugin pipeline)./,../) and virtual modules (\0), avoiding the JS function call overhead for the majority of imports that this plugin will never handleThe
modules: []configuration ensures oxc-resolver only resolves via tsconfigpathsandbaseUrl— it won't attempt node_modules resolution, keeping the behavior scoped to exactly what the plugin is responsible for.What changed
src/resolver.tscreateResolver()now creates a per-projectResolverFactoryinstance and usesoxcResolver.sync()instead of custom regex matching + Vite resolver delegation. RemovedresolvePathsRootDir(). Replacedglobrexwith an inlinecompileGlob()for the include/exclude matcher.src/index.tsresolveIdconverted from function to object hook format withfilter.idregex. Added Rolldown detection logging inconfigResolved.src/mappings.tsresolvePathMappings()is fully replaced by oxc-resolver's native tsconfig paths handling.package.jsonoxc-resolver ^11.19.1. Removedglobrexand@types/globrex.Backward compatibility
This is a drop-in replacement. All 6 existing test fixtures pass unchanged:
with-baseUrl— paths + baseUrl resolutionwithout-baseUrl— paths without baseUrl (resolves relative to tsconfig directory)configDir-syntax—${configDir}variable substitution in paths and includeextends-paths— tsconfig inheritance viaextendswith paths defined in parentpaths-outside-root— path mappings that reference directories outside the project root (../my-utils/*)project-reference— TypeScript composite project referencesPreserved behaviors:
tsconfckis still used for tsconfig discovery, parsing, and file watching — oxc-resolver doesn't expose discovery APIsglobrex)loose,importerFilter,parseNative,configNames,projects,projectDiscovery,logFile,skip.jsonimport guard preserved — accidental resolution to.jsonfiles is still blocked unless the import specifier explicitly ends with.json.d.tsresolution guard preservedHook filter compatibility:
filterproperty is evaluated natively, skipping the JS handler for non-matching importsfilterproperty is silently ignored, and thehandlerfunction is called for all imports (identical to previous behavior)One subtle behavior difference: The
.jsonimport guard now applies to all resolutions (bothpathsandbaseUrl), whereas previously it only applied tobaseUrl-only resolution. In practice this is inconsequential — if a tsconfig path pattern maps to a.jsonfile, the import specifier should include the.jsonextension explicitly (which passes the guard).Prettier to oxfmt migration
oxfmt is OXC's Rust-based formatter — a Prettier-compatible drop-in replacement that is significantly faster.
What changed
.prettierrc.oxfmtrc.jsonoxfmt --migrate=prettier, preserves all formatting options (singleQuote, semi, trailingComma, bracketSpacing, etc.) withprintWidth: 80explicitly set (oxfmt defaults to 100).vscode/settings.jsonesbenp.prettier-vscodetonicolo-ribaudo.oxfmt-vscode.vscode/extensions.jsonesbenp.prettier-vscodetonicolo-ribaudo.oxfmt-vscodepackage.jsonprettierremoved from devDeps,oxfmtaddedFormatting behavior
The migrated
.oxfmtrc.jsonpreserves the original Prettier settings:{ "bracketSpacing": true, "jsxBracketSameLine": true, "singleQuote": true, "semi": false, "trailingComma": "es5", "printWidth": 80 }Running
oxfmtproduced minor whitespace-only changes in 10 files (JSON formatting normalization, markdown table alignment). No source code logic was affected.Usage
Isolated declarations with oxc-transform
Replaced tsup's
--dts(which uses the TypeScript compiler internally) withoxc-transform'sisolatedDeclarationSync()for.d.tsgeneration. This is significantly faster because it operates on a single-file basis without needing the full TypeScript type checker.Requirements
Enabled
isolatedDeclarations: trueintsconfig.json. This requires all exported symbols to have explicit type annotations — the compiler can generate declarations from each file independently without cross-file type inference.Source changes for isolatedDeclarations compliance
src/index.tsvite.Pluginreturn type to default exportsrc/debug.tscreateDebug.Debuggertype annotationsrc/logFile.tsvoidreturn type towrite()methodsrc/path.tsascasts (TS 6 rejects direct casts between incompatible branded types)src/resolver.tsTsconfigResolversfromReturnType<>to an explicitinterfacewith typedreset,get,watchmethodsBuild pipeline
Rolldown replaces tsup for JS bundling. tsup used esbuild internally and pulled in 101 transitive dependencies. Rolldown is a single Rust binary with native TypeScript support — no transpiler chain needed.
The
rolldown.config.tsis minimal:The
scripts/dts.tsscript processes allsrc/*.tsfiles throughisolatedDeclarationSync()and writes.d.tsfiles todist/. JSDoc comments are preserved in the output.TypeScript 6.0 RC
Upgraded from TypeScript 5.7.2 to 6.0.1-rc. Required changes:
as unknown as→ wrapper functions insrc/path.tsascasts betweenstringand brandedNormalizedPathtype — properly wrapping the functions is the correct approach@types/node^22.2.0 → ^25.5.0@types/nodehasBuffertype incompatibilities with TS 6ignoreDeprecations: "6.0"inwith-baseUrlfixturebaseUrlis deprecated in TS 6 (removed in TS 7)rootDir: ".."inpaths-outside-rootfixturerootDirwhen sources span multiple directoriesCI: oxlint type-aware linting + format checking
The GitHub Actions workflow (
.github/workflows/ci.yml) has been restructured into two jobs:checkjob (ubuntu-latest only)Runs formatting and linting checks that don't need cross-platform coverage:
pnpm fmt:check— verifies all files are formatted with oxfmtpnpm lint— runs oxlint with type-aware linting and type-checking enabledtestjob (ubuntu + windows matrix)Unchanged — runs
pnpm testacross both OS targets.oxlint configuration
.oxlintrc.jsonenables type-aware linting with TypeScript type-checking:{ "options": { "typeAware": true, "typeCheck": true }, "rules": { "no-control-regex": "off", "typescript/unbound-method": "off" } }typeAware: trueenables rules from thetypescript-eslintfamily that require type information (e.g.,no-floating-promises,no-unsafe-assignment).typeCheck: trueadditionally reports TypeScript diagnostics (equivalent totsc --noEmit) — catching type errors without a separatetscstep.Suppressed rules:
no-control-regex— the\0null character in theresolveIdhook filter is intentional (filters virtual module IDs)typescript/unbound-method—src/path.tsre-exportspath.posix.*functions by reference; these are pure functions that don't usethisNew package.json scripts
{ "fmt:check": "oxfmt --check", "lint": "oxlint src/" }LogEvent type fix
Updated
src/logFile.tsto include the new'resolved'event type (from oxc-resolver) and allow'notFound'without thecandidatesfield (no longer generated since mappings.ts is removed). This was caught by oxlint's type-checking.DevDependency cleanup
Removed stale devDependencies that were no longer directly used:
tsup(^6.5.0)esbuild(^0.11.12)rollup(^2.45.2)prettier(^2.8.7)globrex(^0.1.2)compileGlob()in resolver.ts.@types/globrex(^0.1.0)klona(^2.0.4)Other cleanup
cleanscript fixed — removedgit checkout HEAD distwhich was a no-op (dist/ is gitignored). Now justrimraf dist.Project metadata
Added
packageManagerandenginesfields topackage.json:{ "packageManager": "pnpm@10.32.0", "engines": { "node": ">=18" } }Dependency changes
Production dependencies
debugglobrextsconfckoxc-resolverDev dependencies
typescript@types/nodetsupprettieresbuildrollup@types/globrexklonarolldownoxfmtoxlintoxlint-tsgolintoxc-transformtsxoxc-resolverships prebuilt native binaries for all major platforms (macOS, Linux, Windows; x64 and arm64). No compilation step is required during install.Test plan
pnpm test)tsc -p .)pnpm build) — oxc-transform generates .d.tstsc --noEmitpasses with TypeScript 6.0.1-rc andisolatedDeclarations: truepnpm fmt:checkpasses (all files formatted with oxfmt)pnpm lintpasses (oxlint type-aware + type-check, 0 errors/warnings)projectDiscovery: "lazy"modejsconfig.json(viaconfigNamesoption)Versioning: why v7
This is a major version bump (
7.0.0-alpha.1) because:.jsonimport guard broadened — now applies topathsresolution (previously onlybaseUrl). Imports like@/datathat resolve to.jsonwithout an explicit.jsonextension will no longer match.globrexremoved,oxc-resolveradded. Consumers who vendor or audit dependencies will see a different tree.enginesconstraint added.baseUrl, explicitrootDir).Alpha release strategy
Published as
7.0.0-alpha.1with thealphadist-tag so it won't affect existing users on^6.x:This lets early adopters test the new resolution engine in real projects and report issues before a stable v7 release.
Toolchain summary
The entire dev toolchain is now OXC/Rust-native:
oxc-resolverrolldownoxc-transformoxlint+oxlint-tsgolintoxfmt