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 all 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
108 changes: 91 additions & 17 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,67 @@ export class FlightManifestPlugin {
if (!ssrNamedModuleId.startsWith('.'))
ssrNamedModuleId = `./${ssrNamedModuleId}`

if (
mod.request &&
/(?<!\.module)\.css$/.test(mod.request) &&
(dev
? mod.loaders.some((item: any) =>
item.loader.includes('next-style-loader/index.js')
)
: mod.loaders.some((item: any) =>
item.loader.includes('mini-css-extract-plugin/loader.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 +186,36 @@ export class FlightManifestPlugin {
)
.filter((name) => name !== null)

// Get all CSS files imported in that chunk.
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)
}
// Get all CSS files imported from the module's dependencies.
const visitedModule = new Set()
const cssChunks: Set<string> = new Set()

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

const modRequest = module.userRequest
if (visitedModule.has(modRequest)) return
visitedModule.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.add(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 All @@ -167,7 +241,7 @@ export class FlightManifestPlugin {
moduleExports[name] = {
id,
name,
chunks: requiredChunks.concat(cssChunks),
chunks: requiredChunks.concat([...cssChunks]),
}
}
if (!moduleIdMapping[id][name]) {
Expand Down
50 changes: 47 additions & 3 deletions packages/next/client/app-index.tsx
Expand Up @@ -30,8 +30,7 @@ __webpack_require__.u = (chunkId: any) => {
self.__next_require__ = __webpack_require__

// eslint-disable-next-line no-undef
// @ts-expect-error TODO: fix type
self.__next_chunk_load__ = (chunk) => {
;(self as any).__next_chunk_load__ = (chunk: string) => {
if (chunk.endsWith('.css')) {
const link = document.createElement('link')
link.rel = 'stylesheet'
Expand Down Expand Up @@ -153,7 +152,52 @@ 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)
})
)
// In development mode, import css in dev when it's wrapped by style loader.
// In production mode, css are standalone chunk that doesn't need to be imported.
if (data.id) {
;(self as any).__next_require__(data.id)
}
}

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)
}
if (!data.startsWith('CSS:')) {
controller.enqueue(chunk)
}
},
flush() {
loadCssFromStreamData(buffer)
},
})

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

rscCache.set(cacheKey, newResponse)
return newResponse
Expand Down