fix(vite-dev-server): exclude CT specs from Vite 8 JSX refresh (#33751)

* fix(vite-dev-server): exclude CT specs from Vite 8 JSX refresh

Prevents duplicate describe/it registration in headed mode when specs define
local React components (HMR self-accept). Sets oxc.jsxRefreshExclude for Vite 8.

Fixes #33750

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(vite-dev-server): scope jsxRefreshInclude so CSS is not transformed by Oxc

Pair jsxRefreshExclude with a script-only jsxRefreshInclude pattern; Vite's
createFilter(undefined, exclude) otherwise matches all non-spec assets.

Add unit tests including CSS path regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Bill Glesias
2026-05-06 12:55:45 -04:00
committed by GitHub
parent d96f158f14
commit d9b2be464b
3 changed files with 62 additions and 1 deletions
+1
View File
@@ -7,6 +7,7 @@
**Bugfixes:**
- Fixed an issue where component specs that defined local React components could register every `describe` / `it` block twice in `cypress open` when using Vite 8, because React refresh treated those specs as HMR self-accepting modules. `@cypress/vite-dev-server` now excludes component spec files from JSX refresh while leaving Fast Refresh enabled for application source. Fixes [#33750](https://github.com/cypress-io/cypress/issues/33750).
- Fixed an issue where multi-origin tests using [`cy.origin`](https://docs.cypress.io/api/commands/origin) could fail to talk to a secondary origin after test isolation, when the spec-bridge iframe was already present, or when more than one secondary origin became ready around the same time. Cached spec-bridge window targets are now cleared at the correct lifecycle points, improving performance of specs with cy.origin calls. Addressed in [#33704](https://github.com/cypress-io/cypress/pull/33704).
- Fixed an issue where a CSS selector built internally from element attributes could throw an uncaught `Syntax error, unrecognized expression` and crash the runner when an attribute value contained CSS-special characters (for example, an `<input>` with a `pattern` attribute containing regex metacharacters). Fixes [#26967](https://github.com/cypress-io/cypress/issues/26967) and [#29345](https://github.com/cypress-io/cypress/issues/29345).
- Fixed an issue where transient HTTP 500 responses from Cypress Cloud were not retried for idempotent requests. Fixed in [#33718](https://github.com/cypress-io/cypress/pull/33718).
+15
View File
@@ -18,6 +18,12 @@ import type { Vite_7, Vite_8 } from './getVite.js'
const debug = debugFn('cypress:vite-dev-server:resolve-config')
// Limit jsxRefreshInclude/exclude matching to scripts. With only jsxRefreshExclude set, Vite builds
// createFilter(undefined, exclude) which matches every non-excluded path — CSS would hit transformWithOxc and fail.
// @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/oxc.ts (transform + jsxRefreshFilter)
/** Passed as `oxc.jsxRefreshInclude` so JSX refresh excludes do not match CSS or other assets. */
export const JSX_REFRESH_SCRIPT_RE = /\.(?:[cm]?js|[cm]?ts|[jt]sx)$/
export const isVite8 = (vite: Vite_7 | Vite_8): boolean => {
const isVite8 = vite.version && semverGte(vite.version, '8.0.0') || false
@@ -138,6 +144,15 @@ function makeCypressViteConfig (config: ViteDevServerConfig, vite: Vite_7 | Vite
const viteConfig: InlineConfig_7 | InlineConfig_8 = {
root: projectRoot,
base: `${devServerPublicPathRoute}/`,
// Vite 8 Rolldown/react-plugin can wrap JSX specs with `import.meta.hot.accept`, re-evaluating
// the module in headed mode and registering describe/it twice. Excluding CT specs from JSX refresh fixes it.
// @see https://github.com/cypress-io/cypress/issues/33750
...(isVite8(vite) ? {
oxc: {
jsxRefreshInclude: JSX_REFRESH_SCRIPT_RE,
jsxRefreshExclude: specs.map((s) => s.absolute),
},
} : {}),
optimizeDeps: {
...options,
entries: [
+46 -1
View File
@@ -1,3 +1,5 @@
import path from 'path'
import { fileURLToPath } from 'node:url'
import { vi, describe, it, beforeEach, expect } from 'vitest'
import { EventEmitter } from 'events'
import * as vite5 from 'vite-5'
@@ -5,7 +7,7 @@ import * as vite6 from 'vite-6'
import * as vite7 from 'vite-7'
import * as vite8 from 'vite-8'
import { scaffoldSystemTestProject } from './test-helpers/scaffoldProject'
import { createViteDevServerConfig } from '../src/resolveConfig'
import { createViteDevServerConfig, JSX_REFRESH_SCRIPT_RE } from '../src/resolveConfig'
import type { ViteDevServerConfig } from '../src/devServer'
const getViteDevServerConfig = (projectRoot: string) => {
@@ -115,4 +117,47 @@ describe('resolveConfig', function () {
})
})
})
describe('Vite 8 JSX refresh excludes component specs', () => {
// Real package root so createRequire can resolve `vite` like a consumer project; inline viteConfig skips fixture scaffolding.
const viteDevServerPackageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
it('sets oxc.jsxRefreshInclude and jsxRefreshExclude from Cypress specs (Vite 8)', async () => {
const specAbsolutes = [
path.join(viteDevServerPackageRoot, 'src', 'Hello.cy.tsx'),
path.join(viteDevServerPackageRoot, 'src', 'Other.cy.tsx'),
]
const viteDevServerConfig = {
...getViteDevServerConfig(viteDevServerPackageRoot),
viteConfig: {},
specs: [
{ absolute: specAbsolutes[0], relative: 'src/Hello.cy.tsx' },
{ absolute: specAbsolutes[1], relative: 'src/Other.cy.tsx' },
],
} as unknown as ViteDevServerConfig
const viteConfig = await createViteDevServerConfig(viteDevServerConfig, vite8)
expect(viteConfig.oxc?.jsxRefreshInclude).to.equal(JSX_REFRESH_SCRIPT_RE)
expect(viteConfig.oxc?.jsxRefreshExclude).to.eql(specAbsolutes)
})
it('does not set oxc overrides for Vite 7', async () => {
const specAbsolute = path.join(viteDevServerPackageRoot, 'components', 'Card.cy.tsx')
const viteDevServerConfig = {
...getViteDevServerConfig(viteDevServerPackageRoot),
viteConfig: {},
specs: [{ absolute: specAbsolute, relative: 'components/Card.cy.tsx' }],
} as unknown as ViteDevServerConfig
const viteConfig = await createViteDevServerConfig(viteDevServerConfig, vite7)
expect(viteConfig.oxc).to.be.undefined
})
it('matches only script-like paths so imported CSS (e.g. support files) is not run through transformWithOxc', () => {
expect(JSX_REFRESH_SCRIPT_RE.test('/project/cypress/support/backgroundColor.css')).to.be.false
expect(JSX_REFRESH_SCRIPT_RE.test('/project/src/App.cy.tsx')).to.be.true
})
})
}, 1000 * 60)