Skip to content

Commit

Permalink
Split the client reference manifest file to be generated per-entry (#…
Browse files Browse the repository at this point in the history
…52450)

This PR changes client manifest generation process. Instead of one big
manifest file that contains client references for the entire app, we're
now generating one manifest file per entry which only covers client
components that can be reached in the module graph.
  • Loading branch information
shuding committed Jul 10, 2023
1 parent c68c4bd commit 990c58c
Show file tree
Hide file tree
Showing 20 changed files with 275 additions and 126 deletions.
10 changes: 0 additions & 10 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import {
PAGES_MANIFEST,
PHASE_PRODUCTION_BUILD,
PRERENDER_MANIFEST,
CLIENT_REFERENCE_MANIFEST,
REACT_LOADABLE_MANIFEST,
ROUTES_MANIFEST,
SERVER_DIRECTORY,
Expand Down Expand Up @@ -903,14 +902,6 @@ export default async function build(
: []),
path.join(SERVER_DIRECTORY, APP_PATHS_MANIFEST),
APP_BUILD_MANIFEST,
path.join(
SERVER_DIRECTORY,
CLIENT_REFERENCE_MANIFEST + '.js'
),
path.join(
SERVER_DIRECTORY,
CLIENT_REFERENCE_MANIFEST + '.json'
),
path.join(
SERVER_DIRECTORY,
SERVER_REFERENCE_MANIFEST + '.js'
Expand Down Expand Up @@ -1487,7 +1478,6 @@ export default async function build(
pageRuntime,
edgeInfo,
pageType,
hasServerComponents: !!appDir,
incrementalCacheHandlerPath:
config.experimental.incrementalCacheHandlerPath,
isrFlushToDisk: config.experimental.isrFlushToDisk,
Expand Down
5 changes: 0 additions & 5 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,6 @@ export async function isPageStatic({
pageRuntime,
edgeInfo,
pageType,
hasServerComponents,
originalAppPath,
isrFlushToDisk,
maxMemoryCacheSize,
Expand All @@ -1356,7 +1355,6 @@ export async function isPageStatic({
edgeInfo?: any
pageType?: 'pages' | 'app'
pageRuntime?: ServerRuntime
hasServerComponents?: boolean
originalAppPath?: string
isrFlushToDisk?: boolean
maxMemoryCacheSize?: number
Expand Down Expand Up @@ -1425,7 +1423,6 @@ export async function isPageStatic({
componentsResult = await loadComponents({
distDir,
pathname: originalAppPath || page,
hasServerComponents: !!hasServerComponents,
isAppPath: pageType === 'app',
})
}
Expand Down Expand Up @@ -1669,7 +1666,6 @@ export async function hasCustomGetInitialProps(
const components = await loadComponents({
distDir,
pathname: page,
hasServerComponents: false,
isAppPath: false,
})
let mod = components.ComponentMod
Expand All @@ -1692,7 +1688,6 @@ export async function getDefinedNamedExports(
const components = await loadComponents({
distDir,
pathname: page,
hasServerComponents: false,
isAppPath: false,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
const buildManifest = self.__BUILD_MANIFEST
const prerenderManifest = maybeJSONParse(self.__PRERENDER_MANIFEST)
const reactLoadableManifest = maybeJSONParse(self.__REACT_LOADABLE_MANIFEST)
const rscManifest = maybeJSONParse(self.__RSC_MANIFEST)
const rscManifest = maybeJSONParse(self.__RSC_MANIFEST?.[${JSON.stringify(
page
)}])
const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST)
const subresourceIntegrityManifest = ${
sriEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function getRender({
nextFontManifest,
Document,
App: appMod?.default as AppType,
clientReferenceManifest,
}

const server = new WebServer({
Expand All @@ -86,7 +87,6 @@ export function getRender({
runtime: SERVER_RUNTIME.experimentalEdge,
supportsDynamicHTML: true,
disableOptimizedLoading: true,
clientReferenceManifest,
serverActionsManifest,
serverActionsBodySizeLimit,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
import {
APP_BUILD_MANIFEST,
CLIENT_REFERENCE_MANIFEST,
CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
SYSTEM_ENTRYPOINTS,
} from '../../../shared/lib/constants'
Expand Down Expand Up @@ -76,8 +77,22 @@ export class AppBuildManifestPlugin {
}

const filesForPage = getEntrypointFiles(entrypoint)

manifest.pages[pagePath] = [...new Set([...mainFiles, ...filesForPage])]
const manifestsForPage =
pagePath.endsWith('/page') ||
pagePath === '/not-found' ||
pagePath === '/_not-found'
? [
'server/app' +
pagePath.replace(/%5F/g, '_') +
'_' +
CLIENT_REFERENCE_MANIFEST +
'.js',
]
: []

manifest.pages[pagePath] = [
...new Set([...mainFiles, ...manifestsForPage, ...filesForPage]),
]
}

const json = JSON.stringify(manifest, null, 2)
Expand Down
179 changes: 135 additions & 44 deletions packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,43 @@ export type ClientReferenceManifest = {
}
}

function getAppPathRequiredChunks(chunkGroup: webpack.ChunkGroup) {
return chunkGroup.chunks
.map((requiredChunk: webpack.Chunk) => {
if (SYSTEM_ENTRYPOINTS.has(requiredChunk.name || '')) {
return null
}

// Get the actual chunk file names from the chunk file list.
// It's possible that the chunk is generated via `import()`, in
// that case the chunk file name will be '[name].[contenthash]'
// instead of '[name]-[chunkhash]'.
return [...requiredChunk.files].map((file) => {
// It's possible that a chunk also emits CSS files, that will
// be handled separatedly.
if (!file.endsWith('.js')) return null
if (file.endsWith('.hot-update.js')) return null

return requiredChunk.id + ':' + file
})
})
.flat()
.filter(nonNullable)
}

function mergeManifest(
manifest: ClientReferenceManifest,
manifestToMerge: ClientReferenceManifest
) {
Object.assign(manifest.clientModules, manifestToMerge.clientModules)
Object.assign(manifest.ssrModuleMapping, manifestToMerge.ssrModuleMapping)
Object.assign(
manifest.edgeSSRModuleMapping,
manifestToMerge.edgeSSRModuleMapping
)
Object.assign(manifest.entryCSSFiles, manifestToMerge.entryCSSFiles)
}

const PLUGIN_NAME = 'ClientReferenceManifestPlugin'

export class ClientReferenceManifestPlugin {
Expand Down Expand Up @@ -116,43 +153,21 @@ export class ClientReferenceManifestPlugin {
compilation: webpack.Compilation,
context: string
) {
const manifest: ClientReferenceManifest = {
ssrModuleMapping: {},
edgeSSRModuleMapping: {},
clientModules: {},
entryCSSFiles: {},
}
const manifestsPerGroup = new Map<string, ClientReferenceManifest[]>()

compilation.chunkGroups.forEach((chunkGroup) => {
function getAppPathRequiredChunks() {
return chunkGroup.chunks
.map((requiredChunk: webpack.Chunk) => {
if (SYSTEM_ENTRYPOINTS.has(requiredChunk.name || '')) {
return null
}

// Get the actual chunk file names from the chunk file list.
// It's possible that the chunk is generated via `import()`, in
// that case the chunk file name will be '[name].[contenthash]'
// instead of '[name]-[chunkhash]'.
return [...requiredChunk.files].map((file) => {
// It's possible that a chunk also emits CSS files, that will
// be handled separatedly.
if (!file.endsWith('.js')) return null
if (file.endsWith('.hot-update.js')) return null

return requiredChunk.id + ':' + file
})
})
.flat()
.filter(nonNullable)
// By default it's the shared chunkGroup (main-app) for every page.
let entryName = ''
const manifest: ClientReferenceManifest = {
ssrModuleMapping: {},
edgeSSRModuleMapping: {},
clientModules: {},
entryCSSFiles: {},
}
const requiredChunks = getAppPathRequiredChunks()

let chunkEntryName: string | null = null
if (chunkGroup.name && /^app[\\/]/.test(chunkGroup.name)) {
// Absolute path without the extension
chunkEntryName = (this.appDirBase + chunkGroup.name).replace(
const chunkEntryName = (this.appDirBase + chunkGroup.name).replace(
/[\\/]/g,
path.sep
)
Expand All @@ -161,8 +176,11 @@ export class ClientReferenceManifestPlugin {
.filter(
(f) => !f.startsWith('static/css/pages/') && f.endsWith('.css')
)

entryName = chunkGroup.name
}

const requiredChunks = getAppPathRequiredChunks(chunkGroup)
const recordModule = (id: ModuleId, mod: webpack.NormalModule) => {
// Skip all modules from the pages folder.
if (mod.layer !== WEBPACK_LAYERS.appClient) {
Expand Down Expand Up @@ -311,22 +329,95 @@ export class ClientReferenceManifestPlugin {
}
}
})

// A page's entry name can have extensions. For example, these are both valid:
// - app/foo/page
// - app/foo/page.page
// Let's normalize the entry name to remove the extra extension
const groupName = /\/page(\.[^/]+)?$/.test(entryName)
? entryName.replace(/\/page(\.[^/]+)?$/, '/page')
: entryName.slice(0, entryName.lastIndexOf('/'))

if (!manifestsPerGroup.has(groupName)) {
manifestsPerGroup.set(groupName, [])
}
manifestsPerGroup.get(groupName)!.push(manifest)

if (entryName.includes('/@')) {
// Remove parallel route labels:
// - app/foo/@bar/page -> app/foo
// - app/foo/@bar/layout -> app/foo/layout -> app/foo
const entryNameWithoutNamedSegments = entryName.replace(/\/@[^/]+/g, '')
const groupNameWithoutNamedSegments =
entryNameWithoutNamedSegments.slice(
0,
entryNameWithoutNamedSegments.lastIndexOf('/')
)
if (!manifestsPerGroup.has(groupNameWithoutNamedSegments)) {
manifestsPerGroup.set(groupNameWithoutNamedSegments, [])
}
manifestsPerGroup.get(groupNameWithoutNamedSegments)!.push(manifest)
}

// Special case for the root not-found page.
if (/^app\/not-found(\.[^.]+)?$/.test(entryName)) {
if (!manifestsPerGroup.has('app/not-found')) {
manifestsPerGroup.set('app/not-found', [])
}
manifestsPerGroup.get('app/not-found')!.push(manifest)
}
})

const file = 'server/' + CLIENT_REFERENCE_MANIFEST
const json = JSON.stringify(manifest, null, this.dev ? 2 : undefined)
// Generate per-page manifests.
for (const [groupName] of manifestsPerGroup) {
if (groupName.endsWith('/page') || groupName === 'app/not-found') {
const mergedManifest: ClientReferenceManifest = {
ssrModuleMapping: {},
edgeSSRModuleMapping: {},
clientModules: {},
entryCSSFiles: {},
}

pluginState.ASYNC_CLIENT_MODULES = []
const segments = groupName.split('/')
let group = ''
for (const segment of segments) {
if (segment.startsWith('@')) continue
for (const manifest of manifestsPerGroup.get(group) || []) {
mergeManifest(mergedManifest, manifest)
}
group += (group ? '/' : '') + segment
}
for (const manifest of manifestsPerGroup.get(groupName) || []) {
mergeManifest(mergedManifest, manifest)
}

assets[file + '.js'] = new sources.RawSource(
`self.__RSC_MANIFEST=${JSON.stringify(json)}`
// Work around webpack 4 type of RawSource being used
// TODO: use webpack 5 type by default
) as unknown as webpack.sources.RawSource
assets[file + '.json'] = new sources.RawSource(
json
// Work around webpack 4 type of RawSource being used
// TODO: use webpack 5 type by default
) as unknown as webpack.sources.RawSource
const json = JSON.stringify(mergedManifest)

const pagePath = groupName.replace(/%5F/g, '_')
assets['server/' + pagePath + '_' + CLIENT_REFERENCE_MANIFEST + '.js'] =
new sources.RawSource(
`globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify(
pagePath.slice('app'.length)
)}]=${JSON.stringify(json)}`
) as unknown as webpack.sources.RawSource

if (pagePath === 'app/not-found') {
// Create a separate special manifest for the root not-found page.
assets[
'server/' +
'app/_not-found' +
'_' +
CLIENT_REFERENCE_MANIFEST +
'.js'
] = new sources.RawSource(
`globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify(
'/_not-found'
)}]=${JSON.stringify(json)}`
) as unknown as webpack.sources.RawSource
}
}
}

pluginState.ASYNC_CLIENT_MODULES = []
}
}
8 changes: 3 additions & 5 deletions packages/next/src/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
CLIENT_REFERENCE_MANIFEST,
MIDDLEWARE_MANIFEST,
MIDDLEWARE_REACT_LOADABLE_MANIFEST,
NEXT_CLIENT_SSR_ENTRY_SUFFIX,
SUBRESOURCE_INTEGRITY_MANIFEST,
NEXT_FONT_MANIFEST,
SERVER_REFERENCE_MANIFEST,
Expand Down Expand Up @@ -99,21 +98,19 @@ function getEntryFiles(
if (meta.edgeSSR) {
if (meta.edgeSSR.isServerComponent) {
files.push(`server/${SERVER_REFERENCE_MANIFEST}.js`)
files.push(`server/${CLIENT_REFERENCE_MANIFEST}.js`)
if (opts.sriEnabled) {
files.push(`server/${SUBRESOURCE_INTEGRITY_MANIFEST}.js`)
}
files.push(
...entryFiles
.filter(
(file) =>
file.startsWith('pages/') && !file.endsWith('.hot-update.js')
file.startsWith('app/') && !file.endsWith('.hot-update.js')
)
.map(
(file) =>
'server/' +
// TODO-APP: seems this should be removed.
file.replace('.js', NEXT_CLIENT_SSR_ENTRY_SUFFIX + '.js')
file.replace('.js', '_' + CLIENT_REFERENCE_MANIFEST + '.js')
)
)
}
Expand All @@ -135,6 +132,7 @@ function getEntryFiles(
.filter((file) => !file.endsWith('.hot-update.js'))
.map((file) => 'server/' + file)
)

return files
}

Expand Down

0 comments on commit 990c58c

Please sign in to comment.