Fix CSS resources being duplicated in app dir (#44168)

Currently, to get all the chunk files that contain a specific module in a build, we use `chunk.files`. However a module itself can be included by multiple chunks, or even chunks from different entries. Theoretically that's correct but in our architecture, we only need these chunks that are from the entry that is currently rendering.

One solution is to add a 2-level key (the entry name) to modules in flight manifest, but that introduces too much size overhead to the manifest. So instead we leverage the `__entry_css_files__` field to generate a list of all files for a specific entry, and then find the intersection set of `{CSSFilesForEntry, CSSFilesForModule}` to get the corresponding CSS files for a specific Next.js entry.

Also renamed `__entry_css__` to be more specific, and did some performance optimizations.

NEXT-297

## Bug

- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/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`
- [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
This commit is contained in:
Shu Ding 2022-12-20 19:43:03 +01:00 committed by GitHub
parent 04daf7eb06
commit 966d2b16c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 111 additions and 46 deletions

View file

@ -2105,6 +2105,7 @@ export default async function getBaseWebpackConfig(
(isClient
? new FlightManifestPlugin({
dev,
appDir,
})
: new FlightClientEntryPlugin({
appDir,

View file

@ -284,11 +284,11 @@ export class FlightClientEntryPlugin {
}
const entryCSSInfo: Record<string, string[]> =
cssManifest.__entry_css__ || {}
cssManifest.__entry_css_mods__ || {}
entryCSSInfo[entryName] = cssImportsForChunk[entryName]
Object.assign(cssManifest, {
__entry_css__: entryCSSInfo,
__entry_css_mods__: entryCSSInfo,
})
})
})
@ -349,9 +349,9 @@ export class FlightClientEntryPlugin {
{
...serverCSSManifest,
...edgeServerCSSManifest,
__entry_css__: {
...serverCSSManifest.__entry_css__,
...edgeServerCSSManifest.__entry_css__,
__entry_css_mods__: {
...serverCSSManifest.__entry_css_mods__,
...edgeServerCSSManifest.__entry_css_mods__,
},
},
null,

View file

@ -26,6 +26,7 @@ import { traverseModules } from '../utils'
interface Options {
dev: boolean
appDir: string
}
/**
@ -65,6 +66,9 @@ export type FlightManifest = {
__edge_ssr_module_mapping__: {
[moduleId: string]: ManifestNode
}
__entry_css_files__: {
[entryPathWithoutExtension: string]: string[]
}
} & {
[modulePath: string]: ManifestNode
}
@ -72,7 +76,7 @@ export type FlightManifest = {
export type FlightCSSManifest = {
[modulePath: string]: string[]
} & {
__entry_css__?: {
__entry_css_mods__?: {
[entry: string]: string[]
}
}
@ -86,9 +90,11 @@ export const ASYNC_CLIENT_MODULES = new Set<string>()
export class FlightManifestPlugin {
dev: Options['dev'] = false
appDir: Options['appDir']
constructor(options: Options) {
this.dev = options.dev
this.appDir = options.appDir
}
apply(compiler: webpack.Compiler) {
@ -127,6 +133,7 @@ export class FlightManifestPlugin {
const manifest: FlightManifest = {
__ssr_module_mapping__: {},
__edge_ssr_module_mapping__: {},
__entry_css_files__: {},
}
const dev = this.dev
@ -145,13 +152,10 @@ export class FlightManifestPlugin {
traverseModules(compilation, (mod) => collectClientRequest(mod))
compilation.chunkGroups.forEach((chunkGroup) => {
const cssResourcesInChunkGroup = new Set<string>()
let entryFilepath: string = ''
function recordModule(
chunk: webpack.Chunk,
id: ModuleId,
mod: webpack.NormalModule
mod: webpack.NormalModule,
chunkCSS: string[]
) {
const isCSSModule =
regexCSS.test(mod.resource) ||
@ -191,15 +195,12 @@ export class FlightManifestPlugin {
ssrNamedModuleId = `./${ssrNamedModuleId.replace(/\\/g, '/')}`
if (isCSSModule) {
const chunks = [...chunk.files].filter(
(f) => !f.startsWith('static/css/pages/') && f.endsWith('.css')
)
if (!manifest[resource]) {
manifest[resource] = {
default: {
id,
name: 'default',
chunks,
chunks: chunkCSS,
},
}
} else {
@ -207,14 +208,10 @@ export class FlightManifestPlugin {
// e.g. extracted by mini-css-extract-plugin. In that case we need to
// merge the chunks.
manifest[resource].default.chunks = [
...new Set([...manifest[resource].default.chunks, ...chunks]),
...new Set([...manifest[resource].default.chunks, ...chunkCSS]),
]
}
if (chunkGroup.name) {
cssResourcesInChunkGroup.add(resource)
}
return
}
@ -224,10 +221,6 @@ export class FlightManifestPlugin {
return
}
if (/[\\/](page|layout)\.(ts|js)x?$/.test(resource)) {
entryFilepath = resource
}
const exportsInfo = compilation.moduleGraph.getExportsInfo(mod)
const isAsyncModule = ASYNC_CLIENT_MODULES.has(mod.resource)
@ -328,27 +321,50 @@ export class FlightManifestPlugin {
chunk
// TODO: Update type so that it doesn't have to be cast.
) as Iterable<webpack.NormalModule>
const chunkCSS = [...chunk.files].filter(
(f) => !f.startsWith('static/css/pages/') && f.endsWith('.css')
)
for (const mod of chunkModules) {
const modId: string = compilation.chunkGraph.getModuleId(mod) + ''
recordModule(chunk, modId, mod)
recordModule(modId, mod, chunkCSS)
// If this is a concatenation, register each child to the parent ID.
// TODO: remove any
const anyModule = mod as any
if (anyModule.modules) {
anyModule.modules.forEach((concatenatedMod: any) => {
recordModule(chunk, modId, concatenatedMod)
recordModule(modId, concatenatedMod, chunkCSS)
})
}
}
})
const clientCSSManifest: any = manifest.__client_css_manifest__ || {}
if (entryFilepath) {
clientCSSManifest[entryFilepath] = Array.from(cssResourcesInChunkGroup)
const entryCSSFiles: any = manifest.__entry_css_files__ || {}
const addCSSFilesToEntry = (
files: string[],
entryName: string | undefined | null
) => {
if (entryName?.startsWith('app/')) {
const key = this.appDir + entryName.slice(3)
entryCSSFiles[key] = files.concat(entryCSSFiles[key] || [])
}
}
manifest.__client_css_manifest__ = clientCSSManifest
const cssFiles = chunkGroup.getFiles().filter((f) => f.endsWith('.css'))
if (cssFiles.length) {
// Add to chunk entry and parent chunk groups too.
addCSSFilesToEntry(cssFiles, chunkGroup.name)
chunkGroup.getParents().forEach((parent) => {
addCSSFilesToEntry(cssFiles, parent.options.name)
})
}
manifest.__entry_css_files__ = entryCSSFiles
})
const file = 'server/' + FLIGHT_MANIFEST

View file

@ -6,10 +6,10 @@ import type {
import type {
FlightRouterState,
FlightSegmentPath,
ChildProp,
} from '../../server/app-render'
import type { ErrorComponent } from './error-boundary'
import type { FocusAndScrollRef } from './reducer'
import type { ChildProp } from '../../server/app-render'
import React, { useContext, useEffect, use } from 'react'
import ReactDOM from 'react-dom'

View file

@ -684,24 +684,28 @@ function getCssInlinedLinkTags(
filePath: string,
serverCSSForEntries: string[]
): string[] {
const layoutOrPageCss =
serverCSSManifest[filePath] ||
serverComponentManifest.__client_css_manifest__?.[filePath]
const layoutOrPageCssModules = serverCSSManifest[filePath]
if (!layoutOrPageCss) {
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, '')
const cssFilesForEntry = new Set(
serverComponentManifest.__entry_css_files__?.[filePathWithoutExt] || []
)
if (!layoutOrPageCssModules || !cssFilesForEntry.size) {
return []
}
const chunks = new Set<string>()
for (const css of layoutOrPageCss) {
for (const mod of layoutOrPageCssModules) {
// We only include the CSS if it's a global CSS, or it is used by this
// entrypoint.
if (serverCSSForEntries.includes(css) || !/\.module\.css/.test(css)) {
const mod = serverComponentManifest[css]
if (mod) {
for (const chunk of mod.default.chunks) {
chunks.add(chunk)
if (serverCSSForEntries.includes(mod) || !/\.module\.css/.test(mod)) {
const modData = serverComponentManifest[mod]
if (modData) {
for (const chunk of modData.default.chunks) {
if (cssFilesForEntry.has(chunk)) {
chunks.add(chunk)
}
}
}
}
@ -718,10 +722,10 @@ function getServerCSSForEntries(
for (const entry of entries) {
const entryName = entry.replace(/\.[^.]+$/, '')
if (
serverCSSManifest.__entry_css__ &&
serverCSSManifest.__entry_css__[entryName]
serverCSSManifest.__entry_css_mods__ &&
serverCSSManifest.__entry_css_mods__[entryName]
) {
css.push(...serverCSSManifest.__entry_css__[entryName])
css.push(...serverCSSManifest.__entry_css_mods__[entryName])
}
}
return css

View file

@ -2,6 +2,8 @@ import type ws from 'ws'
import origDebug from 'next/dist/compiled/debug'
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import type { NextConfigComplete } from '../config-shared'
import type { DynamicParamTypesShort, FlightRouterState } from '../app-render'
import { EventEmitter } from 'events'
import { findPageFile } from '../lib/find-page-file'
import { runDependingOnPageType } from '../../build/entries'
@ -15,7 +17,6 @@ import getRouteFromEntrypoint from '../get-route-from-entrypoint'
import { getPageStaticInfo } from '../../build/analysis/get-page-static-info'
import { isMiddlewareFile, isMiddlewareFilename } from '../../build/utils'
import { PageNotFoundError } from '../../shared/lib/utils'
import { DynamicParamTypesShort, FlightRouterState } from '../app-render'
import {
CompilerNameValues,
COMPILER_INDEXES,

View file

@ -0,0 +1,5 @@
import Comp from '../comp'
export default function Page() {
return <Comp />
}

View file

@ -0,0 +1,5 @@
import Comp from '../comp'
export default function Page() {
return <Comp />
}

View file

@ -0,0 +1,7 @@
'use client'
import './style.css'
export default function Comp() {
return <h1 className="red">Hello</h1>
}

View file

@ -0,0 +1,3 @@
.red {
color: red;
}

View file

@ -1472,6 +1472,29 @@ createNextDescribe(
).toBe('50px')
})
})
if (isDev) {
describe('multiple entries', () => {
it('should only load chunks for the css module that is used by the specific entrypoint', async () => {
// Visit /b first
await next.render('/css/css-duplicate/b')
const browser = await next.browser('/css/css-duplicate/a')
expect(
await browser.eval(
`[...document.styleSheets].some(({ href }) => href.endsWith('/a/page.css'))`
)
).toBe(true)
// Should not load the chunk from /b
expect(
await browser.eval(
`[...document.styleSheets].some(({ href }) => href.endsWith('/b/page.css'))`
)
).toBe(false)
})
})
}
})
if (isDev) {