CSS modules support improvements for Server Components (#38536)
Improve CSS modules support in server components. ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
This commit is contained in:
parent
9342a6ccc7
commit
c2b40d0773
13 changed files with 175 additions and 78 deletions
|
@ -249,22 +249,24 @@ export const css = curry(async function css(
|
|||
})
|
||||
)
|
||||
|
||||
// Throw an error for CSS Modules used outside their supported scope
|
||||
fns.push(
|
||||
loader({
|
||||
oneOf: [
|
||||
markRemovable({
|
||||
test: [regexCssModules, regexSassModules],
|
||||
use: {
|
||||
loader: 'error-loader',
|
||||
options: {
|
||||
reason: getLocalModuleImportError(),
|
||||
if (!ctx.experimental.appDir) {
|
||||
// Throw an error for CSS Modules used outside their supported scope
|
||||
fns.push(
|
||||
loader({
|
||||
oneOf: [
|
||||
markRemovable({
|
||||
test: [regexCssModules, regexSassModules],
|
||||
use: {
|
||||
loader: 'error-loader',
|
||||
options: {
|
||||
reason: getLocalModuleImportError(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (ctx.isServer) {
|
||||
fns.push(
|
||||
|
@ -371,6 +373,24 @@ export const css = curry(async function css(
|
|||
],
|
||||
})
|
||||
)
|
||||
fns.push(
|
||||
loader({
|
||||
oneOf: [
|
||||
markRemovable({
|
||||
sideEffects: false,
|
||||
test: regexCssModules,
|
||||
issuer: {
|
||||
or: [
|
||||
{ and: [ctx.rootDirectory, /\.(js|mjs|jsx|ts|tsx)$/] },
|
||||
// Also match the virtual client entry which doesn't have file path
|
||||
(filePath) => !filePath,
|
||||
],
|
||||
},
|
||||
use: getCssModuleLoader(ctx, lazyPostCSSInitializer),
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SERVER_RUNTIME } from '../../../lib/constants'
|
||||
|
||||
export default async function transformSource(this: any): Promise<string> {
|
||||
let { modules, runtime, ssr } = this.getOptions()
|
||||
let { modules, runtime, ssr, server } = this.getOptions()
|
||||
if (!Array.isArray(modules)) {
|
||||
modules = modules ? [modules] : []
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export default async function transformSource(this: any): Promise<string> {
|
|||
const requests = modules as string[]
|
||||
const code =
|
||||
requests
|
||||
.filter((request) => (server ? !request.endsWith('.css') : true))
|
||||
.map((request) => `import(/* webpackMode: "eager" */ '${request}')`)
|
||||
.join(';\n') +
|
||||
`
|
||||
|
|
|
@ -22,7 +22,7 @@ type Options = {
|
|||
const PLUGIN_NAME = 'ClientEntryPlugin'
|
||||
|
||||
export const injectedClientEntries = new Map()
|
||||
const regexCssGlobal = /(?<!\.module)\.css$/
|
||||
const regexCSS = /\.css$/
|
||||
|
||||
export class ClientEntryPlugin {
|
||||
dev: boolean = false
|
||||
|
@ -84,7 +84,7 @@ export class ClientEntryPlugin {
|
|||
|
||||
if (
|
||||
clientComponentRegex.test(modRequest) ||
|
||||
regexCssGlobal.test(modRequest)
|
||||
regexCSS.test(modRequest)
|
||||
) {
|
||||
clientComponentImports.push(modRequest)
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export class ClientEntryPlugin {
|
|||
isDev: this.dev,
|
||||
})
|
||||
|
||||
const clientLoader = `next-flight-client-entry-loader?${stringify({
|
||||
const loaderOptions = {
|
||||
modules: clientComponentImports,
|
||||
runtime: this.isEdgeServer
|
||||
? SERVER_RUNTIME.edge
|
||||
|
@ -123,6 +123,13 @@ export class ClientEntryPlugin {
|
|||
ssr: pageStaticInfo.ssr,
|
||||
// Adding name here to make the entry key unique.
|
||||
name,
|
||||
}
|
||||
const clientLoader = `next-flight-client-entry-loader?${stringify(
|
||||
loaderOptions
|
||||
)}!`
|
||||
const clientSSRLoader = `next-flight-client-entry-loader?${stringify({
|
||||
...loaderOptions,
|
||||
server: true,
|
||||
})}!`
|
||||
|
||||
const bundlePath = 'app' + normalizePagePath(routeInfo.page)
|
||||
|
@ -157,7 +164,7 @@ export class ClientEntryPlugin {
|
|||
// Inject the entry to the server compiler (__sc_client__).
|
||||
const clientComponentEntryDep = (
|
||||
webpack as any
|
||||
).EntryPlugin.createDependency(clientLoader, {
|
||||
).EntryPlugin.createDependency(clientSSRLoader, {
|
||||
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
||||
})
|
||||
promises.push(
|
||||
|
|
|
@ -93,7 +93,7 @@ export class FlightManifestPlugin {
|
|||
|
||||
if (
|
||||
mod.request &&
|
||||
/(?<!\.module)\.css$/.test(mod.request) &&
|
||||
/\.css$/.test(mod.request) &&
|
||||
(dev
|
||||
? mod.loaders.some((item: any) =>
|
||||
item.loader.includes('next-style-loader/index.js')
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
renderToInitialStream,
|
||||
createBufferedTransformStream,
|
||||
continueFromInitialStream,
|
||||
createSuffixStream,
|
||||
createPrefixStream,
|
||||
} from './node-web-streams-helper'
|
||||
import { isDynamicRoute } from '../shared/lib/router/utils'
|
||||
import { tryGetPreviewData } from './api-utils/node'
|
||||
|
@ -113,7 +113,8 @@ function useFlightResponse(
|
|||
writable: WritableStream<Uint8Array>,
|
||||
cachePrefix: string,
|
||||
req: ReadableStream<Uint8Array>,
|
||||
serverComponentManifest: any
|
||||
serverComponentManifest: any,
|
||||
cssFlightData: string
|
||||
) {
|
||||
const id = cachePrefix + ',' + (React as any).useId()
|
||||
let entry = rscCache.get(id)
|
||||
|
@ -125,7 +126,10 @@ function useFlightResponse(
|
|||
rscCache.set(id, entry)
|
||||
|
||||
let bootstrapped = false
|
||||
const forwardReader = forwardStream.getReader()
|
||||
// We only attach CSS chunks to the inlined data.
|
||||
const forwardReader = forwardStream
|
||||
.pipeThrough(createPrefixStream(cssFlightData))
|
||||
.getReader()
|
||||
const writer = writable.getWriter()
|
||||
function process() {
|
||||
forwardReader.read().then(({ done, value }) => {
|
||||
|
@ -188,7 +192,7 @@ function createServerComponentRenderer(
|
|||
globalThis.__next_chunk_load__ = () => Promise.resolve()
|
||||
}
|
||||
|
||||
const cssFlight = getCssFlight(ComponentMod, serverComponentManifest)
|
||||
const cssFlightData = getCssFlightData(ComponentMod, serverComponentManifest)
|
||||
|
||||
let RSCStream: ReadableStream<Uint8Array>
|
||||
const createRSCStream = () => {
|
||||
|
@ -199,7 +203,7 @@ function createServerComponentRenderer(
|
|||
{
|
||||
context: serverContexts,
|
||||
}
|
||||
).pipeThrough(createSuffixStream(cssFlight))
|
||||
)
|
||||
}
|
||||
return RSCStream
|
||||
}
|
||||
|
@ -211,7 +215,8 @@ function createServerComponentRenderer(
|
|||
writable,
|
||||
cachePrefix,
|
||||
reqStream,
|
||||
serverComponentManifest
|
||||
serverComponentManifest,
|
||||
cssFlightData
|
||||
)
|
||||
const root = response.readRoot()
|
||||
return root
|
||||
|
@ -322,7 +327,7 @@ function getSegmentParam(segment: string): {
|
|||
return null
|
||||
}
|
||||
|
||||
function getCssFlight(ComponentMod: any, serverComponentManifest: any) {
|
||||
function getCssFlightData(ComponentMod: any, serverComponentManifest: any) {
|
||||
const importedServerCSSFiles: string[] =
|
||||
ComponentMod.__client__?.__next_rsc_css__ || []
|
||||
|
||||
|
@ -760,7 +765,10 @@ export async function renderToHTML(
|
|||
return [actualSegment]
|
||||
}
|
||||
|
||||
const cssFlight = getCssFlight(ComponentMod, serverComponentManifest)
|
||||
const cssFlightData = getCssFlightData(
|
||||
ComponentMod,
|
||||
serverComponentManifest
|
||||
)
|
||||
const flightData: FlightData = [
|
||||
// TODO: change walk to output without ''
|
||||
walkTreeWithFlightRouterState(tree, {}, providedFlightRouterState).slice(
|
||||
|
@ -770,7 +778,7 @@ export async function renderToHTML(
|
|||
|
||||
return new RenderResult(
|
||||
renderToReadableStream(flightData, serverComponentManifest)
|
||||
.pipeThrough(createSuffixStream(cssFlight))
|
||||
.pipeThrough(createPrefixStream(cssFlightData))
|
||||
.pipeThrough(createBufferedTransformStream())
|
||||
)
|
||||
}
|
||||
|
|
12
test/e2e/app-dir/app/app/css/css-nested/layout.client.js
Normal file
12
test/e2e/app-dir/app/app/css/css-nested/layout.client.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import './style.css'
|
||||
import styles from './style.module.css'
|
||||
|
||||
export default function ClientLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles['client-css']}>Client Layout: CSS Modules</div>
|
||||
<div className="client-css">Client Layout: Global CSS</div>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
3
test/e2e/app-dir/app/app/css/css-nested/page.client.js
Normal file
3
test/e2e/app-dir/app/app/css/css-nested/page.client.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return null
|
||||
}
|
3
test/e2e/app-dir/app/app/css/css-nested/style.css
Normal file
3
test/e2e/app-dir/app/app/css/css-nested/style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.client-css {
|
||||
color: green;
|
||||
}
|
3
test/e2e/app-dir/app/app/css/css-nested/style.module.css
Normal file
3
test/e2e/app-dir/app/app/css/css-nested/style.module.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.client-css {
|
||||
color: green;
|
||||
}
|
14
test/e2e/app-dir/app/app/css/layout.server.js
Normal file
14
test/e2e/app-dir/app/app/css/layout.server.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import './style.css'
|
||||
import styles from './style.module.css'
|
||||
|
||||
export default function ServerLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<div id="server-cssm" className={styles['server-css']}>
|
||||
Server Layout: CSS Modules
|
||||
</div>
|
||||
<div className="server-css">Server Layout: Global CSS</div>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
3
test/e2e/app-dir/app/app/css/style.css
Normal file
3
test/e2e/app-dir/app/app/css/style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.server-css {
|
||||
color: green;
|
||||
}
|
3
test/e2e/app-dir/app/app/css/style.module.css
Normal file
3
test/e2e/app-dir/app/app/css/style.module.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.server-css {
|
||||
color: green;
|
||||
}
|
|
@ -393,66 +393,86 @@ describe('app dir', () => {
|
|||
})
|
||||
|
||||
describe('css support', () => {
|
||||
it('should support global css inside server component layouts', async () => {
|
||||
const browser = await webdriver(next.url, '/dashboard')
|
||||
describe('server layouts', () => {
|
||||
it('should support global css inside server layouts', async () => {
|
||||
const browser = await webdriver(next.url, '/dashboard')
|
||||
|
||||
// Should body text in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('.p')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
// Should body text in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('.p')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
|
||||
// Should inject global css for .green selectors
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('.green')).color`
|
||||
)
|
||||
).toBe('rgb(0, 128, 0)')
|
||||
// Should inject global css for .green selectors
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('.green')).color`
|
||||
)
|
||||
).toBe('rgb(0, 128, 0)')
|
||||
})
|
||||
|
||||
it('should support css modules inside server layouts', async () => {
|
||||
const browser = await webdriver(next.url, '/css/css-nested')
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('#server-cssm')).color`
|
||||
)
|
||||
).toBe('rgb(0, 128, 0)')
|
||||
})
|
||||
})
|
||||
|
||||
it('should support css modules inside client layouts', async () => {
|
||||
const browser = await webdriver(next.url, '/client-nested')
|
||||
|
||||
// Should render h1 in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('h1')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
describe.skip('server pages', () => {
|
||||
it('should support global css inside server pages', async () => {})
|
||||
it('should support css modules inside server pages', async () => {})
|
||||
})
|
||||
|
||||
it('should support css modules inside client pages', async () => {
|
||||
const browser = await webdriver(next.url, '/client-component-route')
|
||||
describe('client layouts', () => {
|
||||
it('should support css modules inside client layouts', async () => {
|
||||
const browser = await webdriver(next.url, '/client-nested')
|
||||
|
||||
// Should render p in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('p')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
// Should render h1 in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('h1')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
})
|
||||
|
||||
it('should support global css inside client layouts', async () => {
|
||||
const browser = await webdriver(next.url, '/client-nested')
|
||||
|
||||
// Should render button in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('button')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
})
|
||||
})
|
||||
|
||||
it('should support global css inside client layouts', async () => {
|
||||
const browser = await webdriver(next.url, '/client-nested')
|
||||
describe('client pages', () => {
|
||||
it('should support css modules inside client pages', async () => {
|
||||
const browser = await webdriver(next.url, '/client-component-route')
|
||||
|
||||
// Should render button in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('button')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
})
|
||||
// Should render p in red
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('p')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
})
|
||||
|
||||
it('should support global css inside client pages', async () => {
|
||||
const browser = await webdriver(next.url, '/client-component-route')
|
||||
it('should support global css inside client pages', async () => {
|
||||
const browser = await webdriver(next.url, '/client-component-route')
|
||||
|
||||
// Should render `b` in blue
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('b')).color`
|
||||
)
|
||||
).toBe('rgb(0, 0, 255)')
|
||||
// Should render `b` in blue
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('b')).color`
|
||||
)
|
||||
).toBe('rgb(0, 0, 255)')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue