Skip to content

Commit

Permalink
Fix client reference manifest for interception routes (#52961)
Browse files Browse the repository at this point in the history
We have the logic to group the client compiler's entry names to make sure we generate one single manifest file for the page. This is complicated and requires a special step to "group" the entry names because a page can depend on a bunch of files from everywhere.

And currently, the normalization of "entryName → groupName" doesn't cover interception routes' conventions (`(.)`, `(..)` and `(...)`). This PR fixes that.

Closes #52862, closes #52681, closes #52958.
  • Loading branch information
shuding committed Jul 20, 2023
1 parent 2b49741 commit a96a9b0
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 17 deletions.
45 changes: 29 additions & 16 deletions packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,33 @@ function getAppPathRequiredChunks(chunkGroup: webpack.ChunkGroup) {
.filter(nonNullable)
}

// Normalize the entry names to their "group names" so a page can easily track
// all the manifest items it needs from parent groups by looking up the group
// segments:
// - app/foo/loading -> app/foo
// - app/foo/page -> app/foo
// - app/(group)/@named/foo/page -> app/foo
// - app/(.)foo/(..)bar/loading -> app/bar
function entryNameToGroupName(entryName: string) {
let groupName = entryName
.slice(0, entryName.lastIndexOf('/'))
.replace(/\/@[^/]+/g, '')
// Remove the group with lookahead to make sure it's not interception route
.replace(/\/\([^/]+\)(?=(\/|$))/g, '')

// Interception routes
groupName = groupName
.replace(/^.+\/\(\.\.\.\)/g, 'app/')
.replace(/\/\(\.\)/g, '/')

// Interception routes (recursive)
while (/\/[^/]+\/\(\.\.\)/.test(groupName)) {
groupName = groupName.replace(/\/[^/]+\/\(\.\.\)/g, '/')
}

return groupName
}

function mergeManifest(
manifest: ClientReferenceManifest,
manifestToMerge: ClientReferenceManifest
Expand Down Expand Up @@ -344,24 +371,13 @@ export class ClientReferenceManifestPlugin {
manifestEntryFiles.push('app/not-found')
}

// Group the entry by their route path, so the page has all manifest items
// it needs:
// - app/foo/loading -> app/foo
// - app/foo/page -> app/foo
// - app/(group)/@named/foo/page -> app/foo
const groupName = entryName
.slice(0, entryName.lastIndexOf('/'))
.replace(/\/@[^/]+/g, '')
.replace(/\/\([^/]+\)/g, '')

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

// console.log(manifestEntryFiles, manifestsPerGroup)

// Generate per-page manifests.
for (const pageName of manifestEntryFiles) {
const mergedManifest: ClientReferenceManifest = {
Expand All @@ -371,12 +387,9 @@ export class ClientReferenceManifestPlugin {
entryCSSFiles: {},
}

const segments = pageName.split('/')
const segments = [...entryNameToGroupName(pageName).split('/'), 'page']
let group = ''
for (const segment of segments) {
if (segment.startsWith('@')) continue
if (segment.startsWith('(') && segment.endsWith(')')) continue

for (const manifest of manifestsPerGroup.get(group) || []) {
mergeManifest(mergedManifest, manifest)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'

import { useState } from 'react'

export function Client() {
const value = useState('client component')[0]
return <p id="interception-slot-client">{value}</p>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from 'react'
import { Client } from './client'

export default function Page() {
return <p id="interception-slot">interception from @slot/nested</p>
return (
<>
<p id="interception-slot">interception from @slot/nested</p>
<Client />
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ createNextDescribe(
'interception from @slot/nested'
)

// Check if the client component is rendered
await check(
() => browser.waitForElementByCss('#interception-slot-client').text(),
'client component'
)

await check(
() => browser.refresh().waitForElementByCss('#nested').text(),
'hello world from /nested'
Expand Down

0 comments on commit a96a9b0

Please sign in to comment.