Vite · advanced
How to test Vite alias resolution in dev and production builds
Verify that Vite alias configuration resolves identically in dev (esbuild pre-bundling + on-demand transform) and production (Rollup bundling) output. The guide frames alias resolution as a contract: every import the resolver accepts in dev must survive bundling without rewriting, dead-code elimination, or externalization.
The symptoms
- •An import that resolves in `vite dev` fails at `vite build` with `Rollup failed to resolve import "<alias>/..."` or `Could not resolve`.
- •The same alias renders correctly in the browser during HMR but is undefined in the production bundle, producing `ReferenceError` or a 404 for a hashed asset.
- •Resolved paths differ between dev and build: dev returns source files under `src/`, but the bundle emits pre-bundled or `node_modules` copies, changing module identity and side-effect ordering.
- •CSS or asset imports through an alias compile in dev but emit empty or missing files in production because the alias is only registered for JS resolution.
- •TypeScript types compile, but `tsc --noEmit` reports `Cannot find module` because the alias exists only in `vite.config` and not in `tsconfig.json` `paths`.
Likely causes
- •`resolve.alias` entries use single-string form (`{ "@": "/src" }`) and are not expressed as an array of `{ find, replacement }` objects, causing ambiguous prefix matching on overlapping tokens such as `@/utils` vs `@apollo/client`.
- •An alias is configured only for the JS resolver and not for `resolve.alias` entries that also need to cover CSS, JSON, or asset imports processed by Rollup plugins.
- •The alias replacement is a relative or absolute path that Vite rewrites in dev but Rollup interprets as an external module in build, due to missing or trailing slash handling.
- •Pre-bundling via esbuild deduplicates a package that the alias points at, so the alias is silently bypassed in dev and only the deduplicated copy is bundled.
- •`tsconfig.json` `compilerOptions.paths` disagrees with `vite.config` `resolve.alias`, so type checking and runtime resolution diverge silently.
First ten minutes
- 01Reproduce the failure on a clean checkout by running `vite build` and capturing the exact `Rollup failed to resolve import` line; record the import string Vite reports as unresolvable.
- 02Open `vite.config.ts` and list every entry under `resolve.alias`; flag any single-string form, any regex form, and any entry whose `replacement` ends without a trailing slash while the `find` is a prefix token.
- 03Diff the import that fails against the alias `find` tokens; if the import is a substring of a longer token, the alias will not match it in build mode.
- 04Compare `tsconfig.json` `compilerOptions.paths` with `vite.config` `resolve.alias`; if they are not a 1:1 mirror, mark this as a candidate cause before changing config.
- 05Start `vite dev --debug resolve` and re-trigger the failing import; record which file the resolver returns versus what Rollup returned at build time.
- 06Check `package.json` `dependencies` for any package that shares the same import prefix as an alias (for example `@/` vs `@scope/`); overlapping prefixes are the most common silent mismatch.
Evidence to collect
- •The literal error string from the build output, including the import path Vite/Rollup could not resolve and the file that issued the import.
- •The resolved file path returned by the dev resolver for the same import string, obtained from `vite dev --debug resolve` or the browser network panel.
- •The list of hashed output assets in `dist/assets/` whose source paths contain the alias token, used to confirm whether the alias survived bundling.
- •The `tsconfig.json` `paths` map and the `vite.config` `resolve.alias` map, captured side by side to compare each entry.
- •Whether the failing import is a JS module, a CSS file, an asset (image, font), or a JSON file, since Vite routes these through different internal resolvers.
Where to look
- •The boundary between the Vite dev resolver (esbuild-based, in-memory) and the Rollup resolver used at build time, since alias semantics differ between the two.
- •The `resolve.alias` configuration in `vite.config.ts` or `vite.config.js`, focusing on whether entries use string form, `{ find, replacement }` form, or regex.
- •The `tsconfig.json` `compilerOptions.paths` and `baseUrl` settings, which govern type resolution but not runtime resolution.
- •The `optimizeDeps` configuration, where `include` and `exclude` arrays can force pre-bundling that bypasses an alias for a given package.
- •CSS, JSON, and asset import call sites, which are routed through plugin pipelines rather than the JS resolver and may not see the alias at all.
Diagnostic steps
- 01Switch the failing alias from single-string form to `{ find, replacement }` object form with an explicit trailing slash on `replacement` when `find` is a prefix token; rebuild and observe whether the build error changes.
- 02Add `resolve.alias` entries that mirror every `tsconfig.json` `paths` entry, then run `vite build` and `tsc --noEmit` to confirm parity; a passing build with a passing type check indicates parity, not correctness, so continue to step 3.
- 03Inspect `dist/assets/*.js` for the import string Vite reported as unresolvable; if the string is absent, the alias was bypassed by pre-bundling rather than failed.
- 04Run `vite dev --debug resolve` and search the log for the failing import token; the line shows the on-disk file the dev resolver returned, which you compare against the build-time error to identify divergence.
- 05If the import is a CSS or asset, add an explicit alias entry or move the import to a path the resolver accepts without an alias; bundlers handle non-JS imports through plugin chains that ignore some alias shapes.
- 06If `optimizeDeps.include` contains the same package the alias targets, remove the entry or scope the alias so esbuild does not deduplicate the package out of the alias path.
Common mistakes
- •Treating a passing `vite dev` as proof of alias correctness, when the dev resolver and the Rollup resolver apply alias matching with different rules.
- •Using a single-string alias like `'@': '/src'` and expecting it to distinguish `@/utils` from `@apollo/client`; without an explicit delimiter, longer tokens can match unintentionally.
- •Configuring aliases only in `vite.config.ts` and not in `tsconfig.json` `paths`, causing IDE and `tsc` to disagree with the runtime.
- •Forgetting that CSS, JSON, and asset imports are resolved by Rollup plugins and may need separate alias handling or a path that does not require aliasing.
- •Adding a package to `optimizeDeps.include` that overlaps an alias target, which causes esbuild to dedupe the package and bypass the alias in dev.
Safe fixes
- •Replace single-string alias entries with `{ find: '@/', replacement: path.resolve(__dirname, 'src') + '/' }` so the trailing slash enforces a delimiter; rebuild and confirm the original error no longer appears.
- •Mirror every `resolve.alias` entry in `tsconfig.json` `compilerOptions.paths`, using `/*` suffix notation so dynamic imports resolve identically in dev, build, and type checking.
- •If a CSS or asset import fails through an alias, move the import to an absolute project path or add a dedicated alias entry targeting the asset directory explicitly; verify by rebuilding and inspecting `dist/assets/`.
- •If pre-bundling bypasses an alias, add the package to `optimizeDeps.exclude` so esbuild does not deduplicate it, then re-run `vite dev --force` to clear the dep cache and re-resolve.
- •If Rollup reports an external for an aliased path, ensure the alias `replacement` is an absolute, in-project path and not a bare module specifier; rebuild and confirm the external warning is gone.
Prove the fix
- 01`vite build` completes without `Rollup failed to resolve import` errors, and the previously failing import string no longer appears in the build log.
- 02The same import that fails in build resolves to the same on-disk file in `vite dev --debug resolve` as it does in the Rollup build output, confirming parity.
- 03`dist/assets/*.js` contains the hashed module whose source path matches the alias `replacement`, confirming the alias survived bundling rather than being bypassed.
- 04`tsc --noEmit` and `vite build` both succeed on the same import, confirming runtime and type resolution agree.
- 05A regression check that imports two distinct modules sharing only a prefix (for example `@/utils` and `@apollo/client`) still resolves to two distinct on-disk files in both dev and build, proving the delimiter is enforced.
Prevention and next steps
- •Keep `resolve.alias` and `tsconfig.json` `paths` as a single mirrored map, generated or reviewed together in code review, so they cannot drift.
- •Use the `{ find, replacement }` object form with explicit delimiters for every alias, and forbid single-string alias entries in lint or review guidelines.
- •Add a CI step that runs `vite build` and `tsc --noEmit` on the same commit, since alias drift is only visible when both resolvers run against the same import set.
- •Avoid putting packages that share a prefix with an alias into `optimizeDeps.include`; document this constraint alongside the alias configuration.
- •Treat CSS, JSON, and asset imports as a separate alias domain from JS imports, and audit them whenever an alias is added or renamed.
Safe commands and checks
vite build vite dev --debug resolve vite dev --force tsc --noEmit grep -RIn "@/" src grep -RIn "resolve.alias" vite.config.*