Adding viteConfig to Cypress's component.devServer broke your aliases. Why?
answer
- The option replaces, it does not add
- Detection stops the moment you supply one
- Only Cypress's own settings merge on top
- Plugins and aliases vanish together
- Import the real config and spread it
basics
~20 sPassing viteConfig or webpackConfig replaces Cypress's automatic detection of your project's bundler config. Cypress then compiles specs with only what you passed, so plugins and aliases declared in the real config file disappear. Import that file and spread it instead.
solid answer
~40 sThe option looks additive and is a replacement. With `viteConfig` omitted, Cypress searches upward from the project root for your `vite.config` and uses it; pass a `viteConfig` and Cypress resolves it with `configFile: false`, so that file is never loaded. Webpack is the same in effect — the upward search only runs when no `webpackConfig` and no framework preset supplied one. Cypress still merges its own settings on top (public path, spec entries, its plugins, file-system allow rules), but nothing of yours. Everything declared in the real config — `resolve.alias`, the Tailwind or SFC plugins — is gone, so specs fail to resolve imports or render unstyled. The fix is to import the real config and spread it, layering only your test-only overrides on top.
code
javascript · 22 lines// cypress.config.js
import { defineConfig } from 'cypress'
export default defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'vite',
viteConfig: async () => {
const real = (await import('./vite.config')).default
return {
...real,
resolve: {
...real.resolve,
alias: { ...real.resolve?.alias, '@ds': '/src/design-system' },
},
}
},
},
},
})go deeper
Know that Cypress can find your project's bundler config on its own, and that the viteConfig and webpackConfig options exist for the cases where it should not.
Explain that supplying one of those options turns detection off, and that only Cypress's own settings are merged on top of what you passed.
Diagnose from the shape of the failure: a config edit that breaks every spec at once while the application still builds is environmental, not a component defect.
Set the expectation that a harness override is a named diff against the real config, never a second copy of it, and say where that rule is enforced.
## The block has two modes, and they are exclusive `component.devServer` reuses your project's build in one of two ways: - **Detect** — omit `viteConfig` / `webpackConfig`, and Cypress searches upward from the project root for a `vite.config` or `webpack.config` file and uses what it finds. - **Supply** — pass `viteConfig` or `webpackConfig`, and Cypress uses what you passed. The search does not run. The second mode is the trap, because the option reads like an addition and behaves like a **replacement**. ## What each bundler does with the override | | With the option omitted | With the option passed | |---|---|---| | Vite | Searches upward for `vite.config.{ts,js,mjs,cjs,mts,cts}` and loads it | The override is resolved with `configFile: false`, so the project's `vite.config` is never loaded | | Webpack | Searches upward for `webpack.config.{ts,js,mjs,cjs,mts,cts}` and loads it | The search is skipped; only the supplied config, plus any framework preset, is used | In both modes Cypress still merges **its own** settings on top: the public path, the spec entry points, its own plugins, and for Vite the file-system allow rules. Those exist for Cypress's benefit, not your application's. Nothing else is inherited. There is one wrinkle on the webpack side. For frameworks that ship a preset — Angular and Next.js — the preset itself counts as a supplied config, so the upward search never runs for those projects even when you pass nothing. Their overrides layer on top of the preset rather than on top of a detected file, which is why the same edit can behave differently across two repositories that both use webpack. ## What the failure looks like - `failed to resolve import "@/components/RatingWidget"`, or webpack's `Module not found`, on specs that compiled fine the day before. - Every component suddenly renders unstyled, because the plugin that compiled the Tailwind or single-file-component styles was declared in the config file you just stopped loading. - A Vue or Svelte spec that does not compile at all, since the SFC plugin lives in that same file. - The application itself still builds and runs perfectly — which is what sends people hunting in the component instead of the config. Note the tell: the breakage arrives with a **config edit**, not a code edit, and it hits every spec at once. A failure that is that uniform is almost always environmental. ## Merge instead of replacing 1. Import the real config and spread it, then layer test-only changes on top. Both options accept an object or an async function returning one, so the shape you want is `viteConfig: async () => ({ ...(await import('./vite.config')).default, /* overrides */ })`. 2. Spread nested objects explicitly. `resolve.alias` is replaced wholesale, so write `alias: { ...real.resolve?.alias, '@ds': '/src/design-system' }` rather than a bare object that silently discards every alias the project already had. 3. Keep the diff short enough to read at a glance in the config file. An override growing into a second build is a signal to fix the real config instead. 4. If all you need is a config file that does not sit at the project root, export it from that file and pass the export, rather than restating its contents. Both options accept either a plain object or an async function returning one, and the function form is what makes the merge possible, since it can `await` the import of the real config before building the object Cypress receives. Reaching for the object form is what pushes people into restating settings by hand. ## The alias trap that is not this one A related failure looks identical and has a different cause: aliases declared only under `compilerOptions.paths` in `tsconfig.json`. Those are a TypeScript type-checking feature. Neither Vite nor webpack reads them when bundling, so Cypress does not resolve them either — the editor is happy and the spec will not compile. The fix is the same plugin a production build needs: `vite-tsconfig-paths` (an npm plugin) in the Vite config, or `tsconfig-paths-webpack-plugin` (an npm plugin) under webpack's `resolve.plugins`. Meta-frameworks that generate their bundler config at runtime rather than exposing a discoverable `vite.config` — Nuxt is the usual example — produce the same symptom for a third reason. Cypress reads a discoverable config file; it does not execute a framework's own config to extract the settings that framework generates. Aliases those frameworks provide must therefore be declared explicitly under `viteConfig`.
- How do you add one alias without losing everything else in the real Vite config?Spread the real config and spread the nested object too. `resolve.alias` is replaced wholesale, so `resolve: { ...real.resolve, alias: { ...real.resolve?.alias, '@ds': '/src/design-system' } }` keeps the existing map and adds yours. A bare `resolve: { alias: { ... } }` silently discards every alias the project already had, which is the same failure one level down.
- Why do a meta-framework's built-in aliases fail in a Cypress component spec?Because some meta-frameworks configure the bundler internally rather than through a discoverable `vite.config` or `webpack.config`, and Cypress does not execute a framework's own config file to extract what it generates at run time. Those aliases are therefore invisible to the dev server Cypress starts, and the ones your components rely on have to be declared explicitly under `viteConfig`.
saying these in an interview costs you the question
- Assumes viteConfig is merged into the detected vite.config
- Starts rewriting component imports to relative paths instead
- Copies the whole bundler config into the Cypress config file
- Blames the spec because the application still builds fine
- Expects tsconfig compilerOptions paths to resolve at bundle time