Files
6c105133e7 Fix: CSS asset URLs broken with experimental.inlineCss (#85140)
This PR fixes a critical issue with the `experimental.inlineCss` feature
where CSS `url()` references to fonts and other assets would break when
CSS was inlined into `<style>` tags instead of being loaded via external
`<link>` tags. The problem occurred because relative URLs in CSS are
resolved relative to the CSS file's location when loaded externally, but
when the CSS is inlined directly into the HTML document, those same
relative URLs resolve relative to the HTML document instead, causing 404
errors for fonts and images. We fixed this by detecting when CSS is
being prepared for inlining and switching from relative URLs (like
`../_next/static/media/font.woff2`) to absolute URLs (like
`/_next/static/media/font.woff2`) in both the Webpack and Turbopack
bundlers. This ensures that asset references work correctly regardless
of whether the CSS is inlined or external, while maintaining the
existing optimization of using relative URLs for the default case where
CSS is loaded via external stylesheets. The fix is transparent to users
and requires no changes to application code—simply enabling
`experimental.inlineCss: true` will now work correctly with all CSS
asset references.


Fixes https://github.com/vercel/next.js/issues/83674

---------

Co-authored-by: Niklas Mischkulnig <[email protected]>
2025-10-21 10:13:36 -07:00

98 lines
3.2 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'
describe('app dir - css - experimental inline css', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
;(isNextDev ? describe.skip : describe)('Production only', () => {
it('should render page with correct styles', async () => {
const browser = await next.browser('/')
const inlineStyleTag = await browser.elementByCss('style')
expect(await inlineStyleTag.text()).toContain('color')
const p = await browser.elementByCss('p')
expect(await p.getComputedCss('color')).toBe('rgb(255, 255, 0)') // yellow
})
it('should not return rsc payload with inlined style as a dynamic client nav', async () => {
const rscPayload = await (
await next.fetch(`/a?${NEXT_RSC_UNION_QUERY}`, {
method: 'GET',
headers: {
rsc: '1',
},
})
).text()
const style = 'font-size'
expect(rscPayload).toContain('__PAGE__') // sanity check
expect(rscPayload).not.toContain(style)
expect(
await (
await next.fetch(`/a?${NEXT_RSC_UNION_QUERY}`, {
method: 'GET',
})
).text()
).toContain(style) // sanity check that HTML has the style
})
it('should have only one style tag when navigating from page with inlining to page without inlining', async () => {
const browser = await next.browser('/')
await browser.waitForElementByCss('#link-b').click()
await browser.waitForElementByCss('#page-b')
const styleTags = await browser.elementsByCss('style')
const linkTags = await browser.elementsByCss('link[rel="stylesheet"]')
expect(styleTags).toHaveLength(1)
expect(linkTags).toHaveLength(0)
})
it('should apply font styles correctly via className', async () => {
const browser = await next.browser('/')
const fontElement = await browser.elementByCss('#with-font')
const computedFontFamily = await fontElement.getComputedCss('fontFamily')
expect(computedFontFamily).toBeTruthy()
expect(computedFontFamily).not.toBe('Times')
})
it('should apply font styles correctly via CSS variable', async () => {
const browser = await next.browser('/')
const bodyElement = await browser.elementByCss('body')
const computedFontFamily = await bodyElement.getComputedCss('fontFamily')
expect(computedFontFamily).toBeTruthy()
expect(computedFontFamily).not.toBe('Times')
})
it('should inline font-face with absolute src URL', async () => {
const $ = await next.render$('/')
const styleTag = $('style')
expect(styleTag.length).toBeGreaterThan(0)
const styleContent = styleTag.html()
expect(styleContent).toMatch(/@font-face/)
expect(styleContent).toMatch(/font-family/)
const srcMatch = styleContent.match(/src:\s*url\(([^)]+)\)/)
expect(srcMatch).toBeTruthy()
const fontUrl = srcMatch[1].replace(/['"]/g, '')
expect(fontUrl).toMatch(/^\//)
const res = await next.fetch(fontUrl)
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('font')
})
})
})