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:
Shu Ding 2022-07-12 11:42:46 +02:00 committed by GitHub
parent 9342a6ccc7
commit c2b40d0773
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 175 additions and 78 deletions

View file

@ -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),
}),
],
})
)
}
}

View file

@ -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') +
`

View file

@ -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(

View file

@ -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')

View file

@ -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())
)
}

View 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}
</>
)
}

View file

@ -0,0 +1,3 @@
export default function Page() {
return null
}

View file

@ -0,0 +1,3 @@
.client-css {
color: green;
}

View file

@ -0,0 +1,3 @@
.client-css {
color: green;
}

View 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}
</>
)
}

View file

@ -0,0 +1,3 @@
.server-css {
color: green;
}

View file

@ -0,0 +1,3 @@
.server-css {
color: green;
}

View file

@ -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)')
})
})
})
})