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

Split the client reference manifest file to be generated per-entry #52450

Merged
merged 11 commits into from
Jul 10, 2023
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
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 = []
}
}
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