Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable css import in rsc server side #38418

Merged
merged 10 commits into from Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/next/build/webpack/config/blocks/css/index.ts
Expand Up @@ -360,7 +360,11 @@ export const css = curry(async function css(
sideEffects: true,
test: regexCssGlobal,
issuer: {
and: [ctx.rootDirectory, /\.(js|mjs|jsx|ts|tsx)$/],
or: [
{ and: [ctx.rootDirectory, /\.(js|mjs|jsx|ts|tsx)$/] },
// Also match the virtual client entry which doesn't have file path
(filePath) => !filePath,
],
},
use: getGlobalCssLoader(ctx, lazyPostCSSInitializer),
}),
Expand Down
Expand Up @@ -6,13 +6,15 @@ export default async function transformSource(this: any): Promise<string> {
modules = modules ? [modules] : []
}

return (
modules
.map(
(request: string) => `import(/* webpackMode: "eager" */ '${request}')`
)
.join(';') +
const requests = modules as string[]
const code =
requests
.map((request) => `import(/* webpackMode: "eager" */ '${request}')`)
.join(';\n') +
`
export const __next_rsc_css__ = ${JSON.stringify(
requests.filter((request) => request.endsWith('.css'))
)};
export const __next_rsc__ = {
server: false,
__webpack_require__
Expand All @@ -25,5 +27,6 @@ export default async function transformSource(this: any): Promise<string> {
: ssr
? `export const __N_SSP = true;`
: `export const __N_SSG = true;`)
)

return code
}
22 changes: 13 additions & 9 deletions packages/next/build/webpack/plugins/client-entry-plugin.ts
Expand Up @@ -22,6 +22,7 @@ type Options = {
const PLUGIN_NAME = 'ClientEntryPlugin'

export const injectedClientEntries = new Map()
const regexCssGlobal = /(?<!\.module)\.css$/

export class ClientEntryPlugin {
dev: boolean = false
Expand Down Expand Up @@ -77,11 +78,15 @@ export class ClientEntryPlugin {
const module = compilation.moduleGraph.getResolvedModule(dependency)
if (!module) return

if (visited.has(module.userRequest)) return
visited.add(module.userRequest)
const modRequest = module.userRequest
if (visited.has(modRequest)) return
visited.add(modRequest)

if (clientComponentRegex.test(module.userRequest)) {
clientComponentImports.push(module.userRequest)
if (
clientComponentRegex.test(modRequest) ||
regexCssGlobal.test(modRequest)
) {
clientComponentImports.push(modRequest)
}

compilation.moduleGraph
Expand Down Expand Up @@ -149,13 +154,12 @@ export class ClientEntryPlugin {
)
}

// Inject the entry to the server compiler.
// Inject the entry to the server compiler (__sc_client__).
const clientComponentEntryDep = (
webpack as any
).EntryPlugin.createDependency(
clientLoader,
name + NEXT_CLIENT_SSR_ENTRY_SUFFIX
)
).EntryPlugin.createDependency(clientLoader, {
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
})
promises.push(
new Promise<void>((res, rej) => {
compilation.addEntry(
Expand Down
103 changes: 88 additions & 15 deletions packages/next/build/webpack/plugins/flight-manifest-plugin.ts
Expand Up @@ -9,7 +9,6 @@ import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
import { FLIGHT_MANIFEST } from '../../../shared/lib/constants'
import { clientComponentRegex } from '../loaders/utils'
import { relative } from 'path'
import { getEntrypointFiles } from './build-manifest-plugin'
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'

// This is the module that will be used to anchor all client references to.
Expand Down Expand Up @@ -79,13 +78,7 @@ export class FlightManifestPlugin {
mod: any
) {
const resource: string = mod.resource

// TODO: Hook into deps instead of the target module.
// That way we know by the type of dep whether to include.
// It also resolves conflicts when the same module is in multiple chunks.
if (!resource || !clientComponentRegex.test(resource)) {
return
}
if (!resource) return

const moduleExports: any = manifest[resource] || {}
const moduleIdMapping: any = manifest.__ssr_module_mapping__ || {}
Expand All @@ -98,6 +91,66 @@ export class FlightManifestPlugin {
if (!ssrNamedModuleId.startsWith('.'))
ssrNamedModuleId = `./${ssrNamedModuleId}`

if (
mod.request &&
/(?<!\.module)\.css$/.test(mod.request) &&
((dev &&
mod.request.includes(
'webpack/loaders/next-style-loader/index.js'
)) ||
(!dev &&
mod.request.includes('webpack/loaders/css-loader/src/index.js')))
) {
if (!manifest[resource]) {
if (dev) {
const chunkIdNameMapping = (chunk.ids || []).map((chunkId) => {
return (
chunkId +
':' +
(chunk.name || chunk.id) +
(dev ? '' : '-' + chunk.hash)
)
})
manifest[resource] = {
default: {
id,
name: 'default',
chunks: chunkIdNameMapping,
},
}
moduleIdMapping[id]['default'] = {
id: ssrNamedModuleId,
name: 'default',
chunks: chunkIdNameMapping,
}
manifest.__ssr_module_mapping__ = moduleIdMapping
} else {
const chunks = [...chunk.files].filter((f) => f.endsWith('.css'))
manifest[resource] = {
default: {
id,
name: 'default',
chunks,
},
}
moduleIdMapping[id]['default'] = {
id: ssrNamedModuleId,
name: 'default',
chunks,
}
manifest.__ssr_module_mapping__ = moduleIdMapping
}
}
return
}

// TODO: Hook into deps instead of the target module.
// That way we know by the type of dep whether to include.
// It also resolves conflicts when the same module is in multiple chunks.
if (!clientComponentRegex.test(resource)) {
return
}

const exportsInfo = compilation.moduleGraph.getExportsInfo(mod)
const cjsExports = [
...new Set(
Expand Down Expand Up @@ -132,16 +185,36 @@ export class FlightManifestPlugin {
)
.filter((name) => name !== null)

// Get all CSS files imported in that chunk.
// Get all CSS files imported from the module's dependencies.
const visitedCss = new Set()
huozhi marked this conversation as resolved.
Show resolved Hide resolved
const cssChunks: string[] = []
for (const entrypoint of chunk.groupsIterable) {
const files = getEntrypointFiles(entrypoint)
for (const file of files) {
if (file.endsWith('.css')) {
cssChunks.push(file)
}

function collectClientImportedCss(module: any) {
if (!module) return

const modRequest = module.userRequest
if (visitedCss.has(modRequest)) return
visitedCss.add(modRequest)

if (/\.css$/.test(modRequest)) {
// collect relative imported css chunks
compilation.chunkGraph.getModuleChunks(module).forEach((c) => {
;[...c.files]
.filter((file) => file.endsWith('.css'))
.forEach((file) => cssChunks.push(file))
})
}

const connections = Array.from(
compilation.moduleGraph.getOutgoingConnections(module)
)
connections.forEach((connection) => {
collectClientImportedCss(
compilation.moduleGraph.getResolvedModule(connection.dependency!)
)
})
}
collectClientImportedCss(mod)

moduleExportedKeys.forEach((name) => {
let requiredChunks = []
Expand Down
44 changes: 43 additions & 1 deletion packages/next/client/app-index.tsx
Expand Up @@ -113,7 +113,49 @@ function useInitialServerResponse(cacheKey: string) {
nextServerDataRegisterWriter(controller)
},
})
const newResponse = createFromReadableStream(readable)

async function loadCss(cssChunkInfoJson: string) {
const data = JSON.parse(cssChunkInfoJson)
await Promise.all(
data.chunks.map((chunkId: string) => {
// load css related chunks
return (self as any).__next_chunk_load__(chunkId)
})
)
// import css in dev when it's wrapped by style loader
if (process.env.NODE_ENV === 'development') {
;(self as any).__next_require__(data.id)
}
huozhi marked this conversation as resolved.
Show resolved Hide resolved
}

const loadCssFromStreamData = (data: string) => {
const seg = data.split(':')
if (seg[0] === 'CSS') {
loadCss(seg.slice(1).join(':'))
}
}

let buffer = ''
const loadCssFromFlight = new TransformStream({
transform(chunk, controller) {
const data = new TextDecoder().decode(chunk)
buffer += data
let index
while ((index = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, index)
buffer = buffer.slice(index + 1)
loadCssFromStreamData(line)
}
controller.enqueue(chunk)
},
flush() {
loadCssFromStreamData(buffer)
},
})

const newResponse = createFromReadableStream(
readable.pipeThrough(loadCssFromFlight)
)

rscCache.set(cacheKey, newResponse)
return newResponse
Expand Down
37 changes: 32 additions & 5 deletions packages/next/server/app-render.tsx
Expand Up @@ -16,6 +16,7 @@ import {
renderToInitialStream,
createBufferedTransformStream,
continueFromInitialStream,
createSuffixStream,
} from './node-web-streams-helper'
import { isDynamicRoute } from '../shared/lib/router/utils'
import { tryGetPreviewData } from './api-utils/node'
Expand Down Expand Up @@ -217,8 +218,9 @@ function createServerComponentRenderer(
globalThis.__next_chunk_load__ = () => Promise.resolve()
}

let RSCStream: ReadableStream<Uint8Array>
const cssFlight = getCssFlight(ComponentMod, serverComponentManifest)

let RSCStream: ReadableStream<Uint8Array>
const createRSCStream = () => {
if (!RSCStream) {
RSCStream = renderToReadableStream(
Expand All @@ -227,7 +229,7 @@ function createServerComponentRenderer(
{
context: serverContexts,
}
)
).pipeThrough(createSuffixStream(cssFlight))
}
return RSCStream
}
Expand Down Expand Up @@ -327,6 +329,30 @@ function getSegmentParam(segment: string): {
return null
}

function getCssFlight(ComponentMod: any, serverComponentManifest: any) {
const importedServerCSSFiles: string[] =
ComponentMod.__client__?.__next_rsc_css__ || []

const cssFiles = importedServerCSSFiles.map(
(css) => serverComponentManifest[css].default
)
if (process.env.NODE_ENV === 'development') {
return cssFiles.map((css) => `CSS:${JSON.stringify(css)}`).join('\n')
}

const cssSet = cssFiles.reduce((res, css) => {
res.add(...css.chunks)
return res
}, new Set())

const cssFlight = Array.from(cssSet)
.map(
(css) => `CSS:${JSON.stringify({ id: 'css', name: '', chunks: [css] })}`
huozhi marked this conversation as resolved.
Show resolved Hide resolved
)
.join('\n')
return cssFlight
}

export async function renderToHTML(
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -730,6 +756,7 @@ export async function renderToHTML(
return [actualSegment]
}

const cssFlight = getCssFlight(ComponentMod, serverComponentManifest)
const flightData: FlightData = [
// TODO: change walk to output without ''
walkTreeWithFlightRouterState(tree, {}, providedFlightRouterState).slice(
Expand All @@ -738,9 +765,9 @@ export async function renderToHTML(
]

return new RenderResult(
renderToReadableStream(flightData, serverComponentManifest).pipeThrough(
createBufferedTransformStream()
)
renderToReadableStream(flightData, serverComponentManifest)
.pipeThrough(createSuffixStream(cssFlight))
.pipeThrough(createBufferedTransformStream())
)
}

Expand Down
3 changes: 2 additions & 1 deletion packages/next/server/load-components.ts
Expand Up @@ -125,12 +125,13 @@ export async function loadComponents(
if (hasServerComponents) {
try {
// Make sure to also load the client entry in cache.
await requirePage(
const __client__ = await requirePage(
normalizePagePath(pathname) + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
distDir,
serverless,
appDirEnabled
)
ComponentMod.__client__ = __client__
} catch (_) {
// This page might not be a server component page, so there is no
// client entry to load.
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/dashboard/global.css
@@ -0,0 +1,3 @@
.dangerous-text {
color: red;
}
9 changes: 6 additions & 3 deletions test/e2e/app-dir/app/app/dashboard/layout.server.js
@@ -1,8 +1,11 @@
import './style.css'
import './global.css'

export default function DashboardLayout(props) {
return (
<>
<h1>Dashboard</h1>
<div className="dangerous-text">
<h1 className="green">Dashboard</h1>
{props.children}
</>
</div>
)
}
1 change: 1 addition & 0 deletions test/e2e/app-dir/app/app/dashboard/page.server.js
Expand Up @@ -2,6 +2,7 @@ export default function DashboardPage(props) {
return (
<>
<p>hello from app/dashboard</p>
<p className='green'>this is green</p>
</>
)
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/dashboard/style.css
@@ -0,0 +1,3 @@
.green {
color: green;
}