Skip to content

Commit

Permalink
Add separate entry per layout/page. (#39611)
Browse files Browse the repository at this point in the history
Builds on top of #39162 which adds support for creating any kind of bundle path without breaking the compilation.
Ensures every layout gets a separate client-side bundle if it has client components being used.

Bug

 Related issues linked using fixes #number
 Integration tests added
 Errors have helpful link attached, see 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
 Integration tests added
 Documentation added
 Telemetry added. In case of a feature if it's used or not.
 Errors have helpful link attached, see contributing.md

Documentation / Examples

 Make sure the linting passes by running pnpm lint
 The examples guidelines are followed from our contributing doc

Co-authored-by: Jiachi Liu <inbox@huozhi.im>
Co-authored-by: Shu Ding <g@shud.in>
  • Loading branch information
3 people committed Aug 16, 2022
1 parent 63a8196 commit 57b6eff
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 110 deletions.
4 changes: 3 additions & 1 deletion packages/next/build/webpack/loaders/next-app-loader.ts
Expand Up @@ -24,8 +24,9 @@ async function createTreeCodeFromPath({

// First item in the list is the page which can't have layouts by itself
if (i === segments.length - 1) {
const resolvedPagePath = await resolve(pagePath)
// Use '' for segment as it's the page. There can't be a segment called '' so this is the safest way to add it.
tree = `['', {}, {page: () => require('${pagePath}')}]`
tree = `['', {}, {filePath: '${resolvedPagePath}', page: () => require('${resolvedPagePath}')}]`
continue
}

Expand All @@ -46,6 +47,7 @@ async function createTreeCodeFromPath({
children ? `children: ${children},` : ''
}
}, {
filePath: '${resolvedLayoutPath}',
${
resolvedLayoutPath
? `layout: () => require('${resolvedLayoutPath}'),`
Expand Down
183 changes: 91 additions & 92 deletions packages/next/build/webpack/plugins/flight-client-entry-plugin.ts
Expand Up @@ -2,8 +2,6 @@ import { stringify } from 'querystring'
import path from 'path'
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
import { clientComponentRegex } from '../loaders/utils'
import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path'
import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path'
import {
getInvalidator,
entries,
Expand Down Expand Up @@ -89,53 +87,68 @@ export class FlightClientEntryPlugin {
continue
}

// TODO-APP: create client-side entrypoint per layout/page.
// const entryModule: webpack.NormalModule =
// compilation.moduleGraph.getResolvedModule(entryDependency)

// for (const connection of compilation.moduleGraph.getOutgoingConnections(
// entryModule
// )) {
// const layoutOrPageDependency = connection.dependency
// // const layoutOrPageRequest = connection.dependency.request

// const [clientComponentImports, cssImports] =
// this.collectClientComponentsAndCSSForDependency(
// compiler.context,
// compilation,
// layoutOrPageDependency
// )

// Object.assign(serverCSSManifest, cssImports)

// promises.push(
// this.injectClientEntryAndSSRModules(
// compiler,
// compilation,
// name,
// entryDependency,
// clientComponentImports
// )
// )
// }
const entryModule: webpack.NormalModule =
compilation.moduleGraph.getResolvedModule(entryDependency)

const [clientComponentImports, cssImports] =
this.collectClientComponentsAndCSSForDependency(
compiler.context,
compilation,
entryDependency
const internalClientComponentEntryImports = new Set<
ClientComponentImports[0]
>()

for (const connection of compilation.moduleGraph.getOutgoingConnections(
entryModule
)) {
const layoutOrPageDependency = connection.dependency
const layoutOrPageRequest = connection.dependency.request

const [clientComponentImports, cssImports] =
this.collectClientComponentsAndCSSForDependency({
layoutOrPageRequest,
compilation,
dependency: layoutOrPageDependency,
})

Object.assign(flightCSSManifest, cssImports)

const isAbsoluteRequest = layoutOrPageRequest[0] === '/'

// Next.js internals are put into a separate entry.
if (!isAbsoluteRequest) {
clientComponentImports.forEach((value) =>
internalClientComponentEntryImports.add(value)
)
continue
}

const relativeRequest = isAbsoluteRequest
? path.relative(compilation.options.context, layoutOrPageRequest)
: layoutOrPageRequest

// Replace file suffix as `.js` will be added.
const bundlePath = relativeRequest.replace(
/(\.server|\.client)?\.(js|ts)x?$/,
''
)

Object.assign(flightCSSManifest, cssImports)
promises.push(
this.injectClientEntryAndSSRModules({
compiler,
compilation,
entryName: name,
clientComponentImports,
bundlePath,
})
)
}

// Create internal app
promises.push(
this.injectClientEntryAndSSRModules(
this.injectClientEntryAndSSRModules({
compiler,
compilation,
name,
entryDependency,
clientComponentImports
)
entryName: name,
clientComponentImports: [...internalClientComponentEntryImports],
bundlePath: 'app-internals',
})
)
}

Expand Down Expand Up @@ -164,22 +177,23 @@ export class FlightClientEntryPlugin {
}
}

collectClientComponentsAndCSSForDependency(
context: string,
compilation: any,
collectClientComponentsAndCSSForDependency({
layoutOrPageRequest,
compilation,
dependency,
}: {
layoutOrPageRequest: string
compilation: any
dependency: any /* Dependency */
): [ClientComponentImports, CssImports] {
}): [ClientComponentImports, CssImports] {
/**
* Keep track of checked modules to avoid infinite loops with recursive imports.
*/
const visitedBySegment: { [segment: string]: Set<string> } = {}
const clientComponentImports: ClientComponentImports = []
const serverCSSImports: CssImports = {}

const filterClientComponents = (
dependencyToFilter: any,
segmentPath: string
): void => {
const filterClientComponents = (dependencyToFilter: any): void => {
const mod: webpack.NormalModule =
compilation.moduleGraph.getResolvedModule(dependencyToFilter)
if (!mod) return
Expand All @@ -201,20 +215,24 @@ export class FlightClientEntryPlugin {
: mod.resourceResolveData?.path

// Ensure module is not walked again if it's already been visited
if (!visitedBySegment[segmentPath]) {
visitedBySegment[segmentPath] = new Set()
if (!visitedBySegment[layoutOrPageRequest]) {
visitedBySegment[layoutOrPageRequest] = new Set()
}
if (
!modRequest ||
visitedBySegment[layoutOrPageRequest].has(modRequest)
) {
return
}
if (!modRequest || visitedBySegment[segmentPath].has(modRequest)) return
visitedBySegment[segmentPath].add(modRequest)
visitedBySegment[layoutOrPageRequest].add(modRequest)

const isLayoutOrPage =
/\/(layout|page)(\.server|\.client)?\.(js|ts)x?$/.test(modRequest)
const isCSS = regexCSS.test(modRequest)
const isClientComponent = clientComponentRegex.test(modRequest)

if (isCSS) {
serverCSSImports[segmentPath] = serverCSSImports[segmentPath] || []
serverCSSImports[segmentPath].push(modRequest)
serverCSSImports[layoutOrPageRequest] =
serverCSSImports[layoutOrPageRequest] || []
serverCSSImports[layoutOrPageRequest].push(modRequest)
}

// Check if request is for css file.
Expand All @@ -223,50 +241,34 @@ export class FlightClientEntryPlugin {
return
}

if (isLayoutOrPage) {
segmentPath = path
.relative(path.join(context, 'app'), path.dirname(modRequest))
.replace(/\\/g, '/')

if (segmentPath !== '') {
segmentPath = '/' + segmentPath
}

// If it's a page, add an extra '/' to the segments
if (/\/(page)(\.server|\.client)?\.(js|ts)x?$/.test(modRequest)) {
segmentPath += '/'
}
}

compilation.moduleGraph
.getOutgoingConnections(mod)
.forEach((connection: any) => {
filterClientComponents(connection.dependency, segmentPath)
filterClientComponents(connection.dependency)
})
}

// Traverse the module graph to find all client components.
filterClientComponents(dependency, '')
filterClientComponents(dependency)

return [clientComponentImports, serverCSSImports]
}

async injectClientEntryAndSSRModules(
compiler: any,
compilation: any,
entryName: string,
entryDependency: any,
async injectClientEntryAndSSRModules({
compiler,
compilation,
entryName,
clientComponentImports,
bundlePath,
}: {
compiler: any
compilation: any
entryName: string
clientComponentImports: ClientComponentImports
): Promise<boolean> {
bundlePath: string
}): Promise<boolean> {
let shouldInvalidate = false

const entryModule =
compilation.moduleGraph.getResolvedModule(entryDependency)
const routeInfo = entryModule.buildInfo.route || {
page: denormalizePagePath(entryName.replace(/^pages/, '')),
absolutePagePath: entryModule.resource,
}

const loaderOptions: NextFlightClientEntryLoaderOptions = {
modules: clientComponentImports,
server: false,
Expand All @@ -279,18 +281,15 @@ export class FlightClientEntryPlugin {
server: true,
})}!`

const bundlePath = 'app' + normalizePagePath(routeInfo.page)

// Add for the client compilation
// Inject the entry to the client compiler.
if (this.dev) {
const pageKey = COMPILER_NAMES.client + routeInfo.page
const pageKey = COMPILER_NAMES.client + bundlePath
if (!entries[pageKey]) {
entries[pageKey] = {
type: EntryTypes.CHILD_ENTRY,
parentEntries: new Set([entryName]),
bundlePath,
// absolutePagePath: routeInfo.absolutePagePath,
request: clientLoader,
dispose: false,
lastActiveTime: Date.now(),
Expand Down

0 comments on commit 57b6eff

Please sign in to comment.