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

Fix client component hydration #37134

Merged
merged 13 commits into from
May 24, 2022
2 changes: 1 addition & 1 deletion packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import { recursiveCopy } from '../lib/recursive-copy'
import { recursiveReadDir } from '../lib/recursive-readdir'
import { lockfilePatchPromise, teardownTraceSubscriber } from './swc'
import { injectedClientEntries } from './webpack/plugins/flight-manifest-plugin'
import { injectedClientEntries } from './webpack/plugins/client-entry-plugin'
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
import { flatReaddir } from '../lib/flat-readdir'

Expand Down
20 changes: 13 additions & 7 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin
import { regexLikeCss } from './webpack/config/blocks/css'
import { CopyFilePlugin } from './webpack/plugins/copy-file-plugin'
import { FlightManifestPlugin } from './webpack/plugins/flight-manifest-plugin'
import { ClientEntryPlugin } from './webpack/plugins/client-entry-plugin'
import {
Feature,
SWC_TARGET_TRIPLE,
Expand Down Expand Up @@ -1016,6 +1017,7 @@ export default async function getBaseWebpackConfig(
? {
// We have to use the names here instead of hashes to ensure the consistency between compilers.
moduleIds: 'named',
chunkIds: 'named',
}
: {}),
splitChunks: ((): webpack.Options.SplitChunksOptions | false => {
Expand Down Expand Up @@ -1182,7 +1184,7 @@ export default async function getBaseWebpackConfig(
? `[name].js`
: `../[name].js`
: `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${
dev ? '' : '-[contenthash]'
dev ? '' : viewsDir ? '' : '-[contenthash]'
}.js`,
library: isClient || isEdgeServer ? '_N_E' : undefined,
libraryTarget: isClient || isEdgeServer ? 'assign' : 'commonjs2',
Expand Down Expand Up @@ -1641,12 +1643,16 @@ export default async function getBaseWebpackConfig(
},
}),
hasServerComponents &&
!isClient &&
new FlightManifestPlugin({
dev,
pageExtensions: rawPageExtensions,
isEdgeServer,
}),
(isClient
? new FlightManifestPlugin({
dev,
viewsDir: !!config.experimental.viewsDir,
pageExtensions: rawPageExtensions,
})
: new ClientEntryPlugin({
dev,
isEdgeServer,
})),
!dev &&
isClient &&
new TelemetryPlugin(
Expand Down
192 changes: 192 additions & 0 deletions packages/next/build/webpack/plugins/client-entry-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { stringify } from 'querystring'
import { webpack } from 'next/dist/compiled/webpack/webpack'
import {
EDGE_RUNTIME_WEBPACK,
NEXT_CLIENT_SSR_ENTRY_SUFFIX,
} from '../../../shared/lib/constants'
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,
} from '../../../server/dev/on-demand-entry-handler'
import { getPageStaticInfo } from '../../analysis/get-page-static-info'

type Options = {
dev: boolean
isEdgeServer: boolean
}

const PLUGIN_NAME = 'ClientEntryPlugin'

export const injectedClientEntries = new Map()

export class ClientEntryPlugin {
dev: boolean = false
isEdgeServer: boolean

constructor(options: Options) {
if (typeof options.dev === 'boolean') {
this.dev = options.dev
}
this.isEdgeServer = options.isEdgeServer
}

apply(compiler: any) {
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation: any, { normalModuleFactory }: any) => {
compilation.dependencyFactories.set(
(webpack as any).dependencies.ModuleDependency,
normalModuleFactory
)
compilation.dependencyTemplates.set(
(webpack as any).dependencies.ModuleDependency,
new (webpack as any).dependencies.NullDependency.Template()
)
}
)

// Only for webpack 5
compiler.hooks.finishMake.tapAsync(
PLUGIN_NAME,
async (compilation: any, callback: any) => {
this.createClientEndpoints(compilation, callback)
}
)
}

async createClientEndpoints(compilation: any, callback: () => void) {
const context = (this as any).context
const promises: any = []

// For each SC server compilation entry, we need to create its corresponding
// client component entry.
for (const [name, entry] of compilation.entries.entries()) {
// Check if the page entry is a server component or not.
const entryDependency = entry.dependencies?.[0]
const request = entryDependency?.request

if (request && entry.options?.layer === 'sc_server') {
const visited = new Set()
const clientComponentImports: string[] = []

function filterClientComponents(dependency: any) {
const module = compilation.moduleGraph.getResolvedModule(dependency)
if (!module) return

if (visited.has(module.userRequest)) return
visited.add(module.userRequest)

if (clientComponentRegex.test(module.userRequest)) {
clientComponentImports.push(module.userRequest)
}

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

// Traverse the module graph to find all client components.
filterClientComponents(entryDependency)

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

// Parse gSSP and gSP exports from the page source.
const pageStaticInfo = this.isEdgeServer
? {}
: await getPageStaticInfo({
pageFilePath: routeInfo.absolutePagePath,
nextConfig: {},
isDev: this.dev,
})

const clientLoader = `next-flight-client-entry-loader?${stringify({
modules: clientComponentImports,
runtime: this.isEdgeServer ? 'edge' : 'nodejs',
ssr: pageStaticInfo.ssr,
// Adding name here to make the entry key unique.
name,
})}!`

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

// Inject the entry to the client compiler.
if (this.dev) {
const pageKey = 'client' + routeInfo.page
if (!entries[pageKey]) {
entries[pageKey] = {
bundlePath,
absolutePagePath: routeInfo.absolutePagePath,
clientLoader,
dispose: false,
lastActiveTime: Date.now(),
} as any
const invalidator = getInvalidator()
if (invalidator) {
invalidator.invalidate()
}
}
} else {
injectedClientEntries.set(
bundlePath,
`next-client-pages-loader?${stringify({
isServerComponent: true,
page: denormalizePagePath(bundlePath.replace(/^pages/, '')),
absolutePagePath: clientLoader,
})}!` + clientLoader
)
}

// Inject the entry to the server compiler.
const clientComponentEntryDep = (
webpack as any
).EntryPlugin.createDependency(
clientLoader,
name + NEXT_CLIENT_SSR_ENTRY_SUFFIX
)
promises.push(
new Promise<void>((res, rej) => {
compilation.addEntry(
context,
clientComponentEntryDep,
this.isEdgeServer
? {
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
library: {
name: ['self._CLIENT_ENTRY'],
type: 'assign',
},
runtime: EDGE_RUNTIME_WEBPACK,
asyncChunks: false,
}
: {
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
runtime: 'webpack-runtime',
},
(err: any) => {
if (err) {
rej(err)
} else {
res()
}
}
)
})
)
}
}

Promise.all(promises)
.then(() => callback())
.catch(callback)
}
}
Loading