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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 05Start `vite dev --debug resolve` and re-trigger the failing import; record which file the resolver returns versus what Rollup returned at build time.
  6. 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

  1. 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.
  2. 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.
  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.
  4. 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.
  5. 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.
  6. 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

  1. 01`vite build` completes without `Rollup failed to resolve import` errors, and the previously failing import string no longer appears in the build log.
  2. 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.
  3. 03`dist/assets/*.js` contains the hashed module whose source path matches the alias `replacement`, confirming the alias survived bundling rather than being bypassed.
  4. 04`tsc --noEmit` and `vite build` both succeed on the same import, confirming runtime and type resolution agree.
  5. 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.*