Skip to content

Commit

Permalink
Refactor client CSS imports (#39758)
Browse files Browse the repository at this point in the history
Removed the hack of client-side CSS injection via `chunk`. Instead collect them with the manifest plugin and SSR them as link tags.

Next step is to adjust HMR to not relying on mini-css-extract plugin and webpack.
  • Loading branch information
shuding committed Aug 22, 2022
1 parent 3d0d090 commit cb1038c
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 54 deletions.
19 changes: 10 additions & 9 deletions packages/next/build/webpack/plugins/flight-client-entry-plugin.ts
Expand Up @@ -71,11 +71,6 @@ export class FlightClientEntryPlugin {
// For each SC server compilation entry, we need to create its corresponding
// client component entry.
for (const [name, entry] of compilation.entries.entries()) {
// If the request is not for `app` directory entry skip it.
// if (!entry.request || !entry.request.startsWith('next-app-loader')) {
// continue
// }

// Check if the page entry is a server component or not.
const entryDependency = entry.dependencies?.[0]
// Ensure only next-app-loader entries are handled.
Expand Down Expand Up @@ -193,7 +188,10 @@ export class FlightClientEntryPlugin {
const clientComponentImports: ClientComponentImports = []
const serverCSSImports: CssImports = {}

const filterClientComponents = (dependencyToFilter: any): void => {
const filterClientComponents = (
dependencyToFilter: any,
inClientComponentBoundary: boolean
): void => {
const mod: webpack.NormalModule =
compilation.moduleGraph.getResolvedModule(dependencyToFilter)
if (!mod) return
Expand Down Expand Up @@ -236,20 +234,23 @@ export class FlightClientEntryPlugin {
}

// Check if request is for css file.
if (isClientComponent || isCSS) {
if ((!inClientComponentBoundary && isClientComponent) || isCSS) {
clientComponentImports.push(modRequest)
return
}

compilation.moduleGraph
.getOutgoingConnections(mod)
.forEach((connection: any) => {
filterClientComponents(connection.dependency)
filterClientComponents(
connection.dependency,
inClientComponentBoundary || isClientComponent
)
})
}

// Traverse the module graph to find all client components.
filterClientComponents(dependency)
filterClientComponents(dependency, false)

return [clientComponentImports, serverCSSImports]
}
Expand Down
51 changes: 19 additions & 32 deletions packages/next/build/webpack/plugins/flight-manifest-plugin.ts
Expand Up @@ -114,6 +114,9 @@ export class FlightManifestPlugin {
const dev = this.dev

compilation.chunkGroups.forEach((chunkGroup) => {
const cssResourcesInChunkGroup: string[] = []
let entryFilepath: string = ''

function recordModule(
chunk: webpack.Chunk,
id: ModuleId,
Expand Down Expand Up @@ -176,6 +179,11 @@ export class FlightManifestPlugin {
}
manifest.__ssr_module_mapping__ = moduleIdMapping
}

if (chunkGroup.name) {
cssResourcesInChunkGroup.push(resource)
}

return
}

Expand All @@ -186,6 +194,10 @@ export class FlightManifestPlugin {
return
}

if (/\/(page|layout)\.client\.(ts|js)x?$/.test(resource)) {
entryFilepath = resource
}

const exportsInfo = compilation.moduleGraph.getExportsInfo(mod)
const cjsExports = [
...new Set([
Expand Down Expand Up @@ -218,37 +230,6 @@ export class FlightManifestPlugin {
)
.filter((name) => name !== null)

// 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: ManifestChunks = []
if (!moduleExports[name]) {
Expand All @@ -273,7 +254,7 @@ export class FlightManifestPlugin {
moduleExports[name] = {
id,
name,
chunks: requiredChunks.concat([...cssChunks]),
chunks: requiredChunks,
}
}

Expand Down Expand Up @@ -310,6 +291,12 @@ export class FlightManifestPlugin {
}
}
})

const clientCSSManifest: any = manifest.__client_css_manifest__ || {}
if (entryFilepath) {
clientCSSManifest[entryFilepath] = cssResourcesInChunkGroup
}
manifest.__client_css_manifest__ = clientCSSManifest
})

const file = 'server/' + FLIGHT_MANIFEST
Expand Down
12 changes: 0 additions & 12 deletions packages/next/client/app-index.tsx
Expand Up @@ -32,18 +32,6 @@ self.__next_require__ = __webpack_require__
// eslint-disable-next-line no-undef
;(self as any).__next_chunk_load__ = (chunk: string) => {
if (!chunk) return Promise.resolve()
if (chunk.endsWith('.css')) {
// @ts-expect-error __webpack_public_path__ is inlined by webpack
const chunkPath = `${__webpack_public_path__ || ''}${chunk}`
const existingTag = document.querySelector(`link[href="${chunkPath}"]`)
if (!existingTag) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = chunkPath
document.head.appendChild(link)
}
return Promise.resolve()
}
const [chunkId, chunkFileName] = chunk.split(':')
chunkFilenameMap[chunkId] = `static/chunks/${chunkFileName}.js`

Expand Down
4 changes: 3 additions & 1 deletion packages/next/server/app-render.tsx
Expand Up @@ -383,7 +383,9 @@ function getCssInlinedLinkTags(
serverCSSManifest: FlightCSSManifest,
filePath: string
): string[] {
const layoutOrPageCss = serverCSSManifest[filePath]
const layoutOrPageCss =
serverCSSManifest[filePath] ||
serverComponentManifest.__client_css_manifest__?.[filePath]

if (!layoutOrPageCss) {
return []
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/css/css-client/client-foo.css
@@ -0,0 +1,3 @@
.foo {
color: blue;
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/css/css-client/client-layout.css
@@ -0,0 +1,3 @@
body {
background: cyan;
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/app/app/css/css-client/client-page.css
@@ -0,0 +1,6 @@
h1 {
color: red !important;
}
h1::after {
content: ' (from css-client!!)';
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/app/app/css/css-client/foo.js
@@ -0,0 +1,5 @@
import './client-foo.css'

export default function Foo() {
return <b className="foo">foo</b>
}
12 changes: 12 additions & 0 deletions test/e2e/app-dir/app/app/css/css-client/layout.client.js
@@ -0,0 +1,12 @@
import './client-layout.css'

import Foo from './foo'

export default function ServerLayout({ children }) {
return (
<>
{children}
<Foo />
</>
)
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/app/app/css/css-client/page.client.js
@@ -0,0 +1,5 @@
import './client-page.css'

export default function Page() {
return <h1>Page!!!</h1>
}

0 comments on commit cb1038c

Please sign in to comment.