mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-25 01:49:06 -05:00
feat(proxy): allow manual injection of bootstrap script to prevent hydration mismatches (#33295)
* fix(proxy): allow manual injection of bootstrap script to prevent hydration mismatches Injects Cypress bootstrap code into a developer-provided <script data-cy-bootstrap> tag if present. This allows developers to add attributes like suppressHydrationWarning to the script tag to prevent React hydration mismatches. * docs: add changelog entry for manual bootstrap injection * docs: move changelog entry to Bugfixes section * Update CHANGELOG.md * Update CHANGELOG.md --------- Co-authored-by: Jennifer Shehane <jennifer@cypress.io> Co-authored-by: Cacie Prins <cacieprins@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ _Released 02/17/2026 (PENDING)_
|
||||
|
||||
**Features:**
|
||||
|
||||
- Introduces manual bootstrap script injection via a `<script data-cy-bootstrap>` tag. This is a workaround to fix React SSR hydration mismatches, and enables React apps to use `suppressHydrationWarning` to ignore the mismatch. Addresses [#27204](https://github.com/cypress-io/cypress/issues/27204). Addressed in [#33295](https://github.com/cypress-io/cypress/pull/33295).
|
||||
- Added Brotli compression support to the proxy. Addresses [#6197](https://github.com/cypress-io/cypress/issues/6197).
|
||||
|
||||
**Bugfixes:**
|
||||
|
||||
@@ -26,6 +26,7 @@ const doctypeRe = /<\!doctype.*?>/i
|
||||
const headRe = /<head(?!er).*?>/i
|
||||
const bodyRe = /<body.*?>/i
|
||||
const htmlRe = /<html.*?>/i
|
||||
const bootstrapScriptRe = /(<script[^>]*\bdata-cy-bootstrap\b[^>]*>)([\s\S]*?)(<\/script>)/i
|
||||
|
||||
function getRewriter (useAstSourceRewriting: boolean) {
|
||||
return useAstSourceRewriting ? astRewriter : regexRewriter
|
||||
@@ -91,6 +92,20 @@ export async function html (html: string, opts: SecurityOpts & InjectionOpts) {
|
||||
return html
|
||||
}
|
||||
|
||||
const bootstrapMatch = html.match(bootstrapScriptRe)
|
||||
|
||||
if (bootstrapMatch) {
|
||||
const contentToInject = htmlToInject.replace(/^<script[^>]*>|<\/script>$/g, '')
|
||||
let openTag = bootstrapMatch[1]
|
||||
|
||||
// Ensure nonce is present if provided in options
|
||||
if (opts.cspNonce && !openTag.includes('nonce=')) {
|
||||
openTag = openTag.replace(/>$/, ` nonce="${opts.cspNonce}">`)
|
||||
}
|
||||
|
||||
return html.replace(bootstrapScriptRe, `${openTag}${contentToInject}${bootstrapMatch[3]}`)
|
||||
}
|
||||
|
||||
// TODO: move this into regex-rewriting and have ast-rewriting handle this in its own way
|
||||
|
||||
const headMatch = html.match(headRe)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import * as rewriter from '../../../../lib/http/util/rewriter'
|
||||
|
||||
describe('http/util/rewriter', () => {
|
||||
describe('html', () => {
|
||||
it('injects into head by default', async () => {
|
||||
const html = '<html><head></head><body></body></html>'
|
||||
const opts = {
|
||||
domainName: 'localhost',
|
||||
wantsInjection: 'full',
|
||||
shouldInjectDocumentDomain: true,
|
||||
} as any
|
||||
|
||||
const result = await rewriter.html(html, opts)
|
||||
|
||||
expect(result).toContain('document.domain')
|
||||
expect(result).toContain('<script')
|
||||
expect(result).toContain('<head> <script')
|
||||
})
|
||||
|
||||
it('injects into developer-provided script tag and adds nonce if missing', async () => {
|
||||
const html = '<html><head><script data-cy-bootstrap></script></head><body></body></html>'
|
||||
const opts = {
|
||||
domainName: 'localhost',
|
||||
wantsInjection: 'full',
|
||||
shouldInjectDocumentDomain: true,
|
||||
cspNonce: 'test-nonce-123',
|
||||
} as any
|
||||
|
||||
const result = await rewriter.html(html, opts)
|
||||
|
||||
// Should NOT inject a new head script
|
||||
expect(result).not.toContain('<head> <script')
|
||||
|
||||
// Should preserve the marker
|
||||
expect(result).toContain('data-cy-bootstrap')
|
||||
|
||||
// Should automatically add the nonce
|
||||
expect(result).toContain('nonce="test-nonce-123"')
|
||||
|
||||
// Should contain the code
|
||||
expect(result).toContain('document.domain')
|
||||
})
|
||||
|
||||
it('preserves existing attributes on developer-provided script tag', async () => {
|
||||
const html = '<html><head><script data-cy-bootstrap id="cy-bootstrap" nonce="existing"></script></head><body></body></html>'
|
||||
const opts = {
|
||||
domainName: 'localhost',
|
||||
wantsInjection: 'full',
|
||||
shouldInjectDocumentDomain: true,
|
||||
cspNonce: 'new-nonce',
|
||||
} as any
|
||||
|
||||
const result = await rewriter.html(html, opts)
|
||||
|
||||
// Should check that it uses the existing nonce and doesn't double up
|
||||
// Current implementation checks !includes('nonce='), so it respects existing
|
||||
expect(result).toContain('nonce="existing"')
|
||||
expect(result).not.toContain('nonce="new-nonce"')
|
||||
|
||||
expect(result).toContain('id="cy-bootstrap"')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user