Skip to content

Commit b957f52

Browse files
authored
Fix per-entry client reference manifest for grouped and named segments (#52664)
References for Client Components need to be aggregated to the page entry level, and emitted as files in the correct directory for the SSR server to read from. For normal routes (e.g. `app/foo/bar/page`), we can go through and collect all entries (layout, loading, error, ...) from the current and parent segments, to aggregate all necessary client references. However, for routes with special conventions like `app/(group)/@named/foo/bar/page`, it needs to be normalized (remove the named slot and group segments) so it can be grouped together with `app/(group)/@named2/foo/bar/loading`.
1 parent 79227ee commit b957f52

File tree

5 files changed

+70
-71
lines changed

5 files changed

+70
-71
lines changed

packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts

Lines changed: 52 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export class ClientReferenceManifestPlugin {
155155
context: string
156156
) {
157157
const manifestsPerGroup = new Map<string, ClientReferenceManifest[]>()
158+
const manifestEntryFiles: string[] = []
158159

159160
compilation.chunkGroups.forEach((chunkGroup) => {
160161
// By default it's the shared chunkGroup (main-app) for every page.
@@ -334,94 +335,74 @@ export class ClientReferenceManifestPlugin {
334335
// A page's entry name can have extensions. For example, these are both valid:
335336
// - app/foo/page
336337
// - app/foo/page.page
337-
// Let's normalize the entry name to remove the extra extension
338-
const groupName = /\/page(\.[^/]+)?$/.test(entryName)
339-
? entryName.replace(/\/page(\.[^/]+)?$/, '/page')
340-
: entryName.slice(0, entryName.lastIndexOf('/'))
338+
if (/\/page(\.[^/]+)?$/.test(entryName)) {
339+
manifestEntryFiles.push(entryName.replace(/\/page(\.[^/]+)?$/, '/page'))
340+
}
341+
342+
// Special case for the root not-found page.
343+
if (/^app\/not-found(\.[^.]+)?$/.test(entryName)) {
344+
manifestEntryFiles.push('app/not-found')
345+
}
346+
347+
// Group the entry by their route path, so the page has all manifest items
348+
// it needs:
349+
// - app/foo/loading -> app/foo
350+
// - app/foo/page -> app/foo
351+
// - app/(group)/@named/foo/page -> app/foo
352+
const groupName = entryName
353+
.slice(0, entryName.lastIndexOf('/'))
354+
.replace(/\/@[^/]+/g, '')
355+
.replace(/\/\([^/]+\)/g, '')
341356

342357
if (!manifestsPerGroup.has(groupName)) {
343358
manifestsPerGroup.set(groupName, [])
344359
}
345360
manifestsPerGroup.get(groupName)!.push(manifest)
361+
})
346362

347-
if (entryName.includes('/@')) {
348-
// Remove parallel route labels:
349-
// - app/foo/@bar/page -> app/foo
350-
// - app/foo/@bar/layout -> app/foo/layout -> app/foo
351-
const entryNameWithoutNamedSegments = entryName.replace(/\/@[^/]+/g, '')
352-
const groupNameWithoutNamedSegments =
353-
entryNameWithoutNamedSegments.slice(
354-
0,
355-
entryNameWithoutNamedSegments.lastIndexOf('/')
356-
)
357-
if (!manifestsPerGroup.has(groupNameWithoutNamedSegments)) {
358-
manifestsPerGroup.set(groupNameWithoutNamedSegments, [])
359-
}
360-
manifestsPerGroup.get(groupNameWithoutNamedSegments)!.push(manifest)
361-
}
363+
// console.log(manifestEntryFiles, manifestsPerGroup)
362364

363-
// Special case for the root not-found page.
364-
if (/^app\/not-found(\.[^.]+)?$/.test(entryName)) {
365-
if (!manifestsPerGroup.has('app/not-found')) {
366-
manifestsPerGroup.set('app/not-found', [])
367-
}
368-
manifestsPerGroup.get('app/not-found')!.push(manifest)
365+
// Generate per-page manifests.
366+
for (const pageName of manifestEntryFiles) {
367+
const mergedManifest: ClientReferenceManifest = {
368+
ssrModuleMapping: {},
369+
edgeSSRModuleMapping: {},
370+
clientModules: {},
371+
entryCSSFiles: {},
369372
}
370-
})
371373

372-
// Generate per-page manifests.
373-
for (const [groupName] of manifestsPerGroup) {
374-
if (groupName.endsWith('/page') || groupName === 'app/not-found') {
375-
const mergedManifest: ClientReferenceManifest = {
376-
ssrModuleMapping: {},
377-
edgeSSRModuleMapping: {},
378-
clientModules: {},
379-
entryCSSFiles: {},
380-
}
374+
const segments = pageName.split('/')
375+
let group = ''
376+
for (const segment of segments) {
377+
if (segment.startsWith('@')) continue
378+
if (segment.startsWith('(') && segment.endsWith(')')) continue
381379

382-
const segments = groupName.split('/')
383-
let group = ''
384-
for (const segment of segments) {
385-
if (segment.startsWith('@')) continue
386-
for (const manifest of manifestsPerGroup.get(group) || []) {
387-
mergeManifest(mergedManifest, manifest)
388-
}
389-
group += (group ? '/' : '') + segment
390-
}
391-
for (const manifest of manifestsPerGroup.get(groupName) || []) {
380+
for (const manifest of manifestsPerGroup.get(group) || []) {
392381
mergeManifest(mergedManifest, manifest)
393382
}
383+
group += (group ? '/' : '') + segment
384+
}
394385

395-
const json = JSON.stringify(mergedManifest)
396-
397-
const pagePath = groupName.replace(/%5F/g, '_')
398-
const pageBundlePath = normalizePagePath(pagePath.slice('app'.length))
399-
assets[
400-
'server/app' +
401-
pageBundlePath +
402-
'_' +
403-
CLIENT_REFERENCE_MANIFEST +
404-
'.js'
405-
] = new sources.RawSource(
406-
`globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify(
407-
pagePath.slice('app'.length)
408-
)}]=${JSON.stringify(json)}`
409-
) as unknown as webpack.sources.RawSource
410-
411-
if (pagePath === 'app/not-found') {
412-
// Create a separate special manifest for the root not-found page.
413-
assets[
414-
'server/' +
415-
'app/_not-found' +
416-
'_' +
417-
CLIENT_REFERENCE_MANIFEST +
418-
'.js'
419-
] = new sources.RawSource(
386+
const json = JSON.stringify(mergedManifest)
387+
388+
const pagePath = pageName.replace(/%5F/g, '_')
389+
const pageBundlePath = normalizePagePath(pagePath.slice('app'.length))
390+
assets[
391+
'server/app' + pageBundlePath + '_' + CLIENT_REFERENCE_MANIFEST + '.js'
392+
] = new sources.RawSource(
393+
`globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify(
394+
pagePath.slice('app'.length)
395+
)}]=${JSON.stringify(json)}`
396+
) as unknown as webpack.sources.RawSource
397+
398+
if (pagePath === 'app/not-found') {
399+
// Create a separate special manifest for the root not-found page.
400+
assets['server/app/_not-found_' + CLIENT_REFERENCE_MANIFEST + '.js'] =
401+
new sources.RawSource(
420402
`globalThis.__RSC_MANIFEST=(globalThis.__RSC_MANIFEST||{});globalThis.__RSC_MANIFEST[${JSON.stringify(
421403
'/_not-found'
422404
)}]=${JSON.stringify(json)}`
423405
) as unknown as webpack.sources.RawSource
424-
}
425406
}
426407
}
427408

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use client'
2+
3+
export function Foo() {
4+
return <h2>it works</h2>
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Foo } from './client'
2+
3+
export default function Page() {
4+
return <Foo />
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Layout({ named }) {
2+
return <div>{named}</div>
3+
}

test/e2e/app-dir/rsc-basic/rsc-basic.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ createNextDescribe(
158158
expect(html).toContain('foo.client')
159159
})
160160

161+
it('should create client reference successfully for all file conventions', async () => {
162+
const html = await next.render('/conventions')
163+
expect(html).toContain('it works')
164+
})
165+
161166
it('should be able to navigate between rsc routes', async () => {
162167
const browser = await next.browser('/root')
163168

0 commit comments

Comments
 (0)