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

Decouple entries for server components and client components #36860

Merged
merged 16 commits into from May 13, 2022
81 changes: 60 additions & 21 deletions packages/next/build/entries.ts
Expand Up @@ -31,6 +31,7 @@ import { parse } from '../build/swc'
import { isServerComponentPage, withoutRSCExtensions } from './utils'
import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { serverComponentRegex } from './webpack/loaders/utils'

type ObjectValue<T> = T extends { [key: string]: infer V } ? V : never

Expand Down Expand Up @@ -127,19 +128,19 @@ export function createPagesMapping({
}
}

const cachedPageRuntimeConfig = new Map<string, [number, PageRuntime]>()
type PageStaticInfo = { runtime?: PageRuntime; ssr?: boolean; ssg?: boolean }

const cachedPageStaticInfo = new Map<string, [number, PageStaticInfo]>()

// @TODO: We should limit the maximum concurrency of this function as there
// could be thousands of pages existing.
export async function getPageRuntime(
export async function getPageStaticInfo(
pageFilePath: string,
nextConfig: Partial<NextConfig>,
isDev?: boolean
): Promise<PageRuntime> {
if (!nextConfig.experimental?.reactRoot) return undefined

): Promise<PageStaticInfo> {
const globalRuntime = nextConfig.experimental?.runtime
const cached = cachedPageRuntimeConfig.get(pageFilePath)
const cached = cachedPageStaticInfo.get(pageFilePath)
if (cached) {
return cached[1]
}
Expand All @@ -151,7 +152,7 @@ export async function getPageRuntime(
})
} catch (err) {
if (!isDev) throw err
return undefined
return {}
}

// When gSSP or gSP is used, this page requires an execution runtime. If the
Expand All @@ -160,6 +161,8 @@ export async function getPageRuntime(
// https://github.com/vercel/next.js/discussions/34179
let isRuntimeRequired: boolean = false
let pageRuntime: PageRuntime = undefined
let ssr = false
let ssg = false

// Since these configurations should always be static analyzable, we can
// skip these cases that "runtime" and "gSP", "gSSP" are not included in the
Expand Down Expand Up @@ -192,6 +195,8 @@ export async function getPageRuntime(
identifier === 'getServerSideProps'
) {
isRuntimeRequired = true
ssg = identifier === 'getStaticProps'
ssr = identifier === 'getServerSideProps'
}
}
} else if (type === 'ExportNamedDeclaration') {
Expand All @@ -206,6 +211,8 @@ export async function getPageRuntime(
orig?.value === 'getServerSideProps')
if (hasDataFetchingExports) {
isRuntimeRequired = true
ssg = orig.value === 'getStaticProps'
ssr = orig.value === 'getServerSideProps'
break
}
}
Expand All @@ -218,19 +225,29 @@ export async function getPageRuntime(
if (isRuntimeRequired) {
pageRuntime = globalRuntime
}
} else {
// For Node.js runtime, we do static optimization.
if (!isRuntimeRequired && pageRuntime === 'nodejs') {
pageRuntime = undefined
}
}

cachedPageRuntimeConfig.set(pageFilePath, [Date.now(), pageRuntime])
return pageRuntime
const info = {
runtime: pageRuntime,
ssr,
ssg,
}
cachedPageStaticInfo.set(pageFilePath, [Date.now(), info])
return info
}

export function invalidatePageRuntimeCache(
pageFilePath: string,
safeTime: number
) {
const cached = cachedPageRuntimeConfig.get(pageFilePath)
const cached = cachedPageStaticInfo.get(pageFilePath)
if (cached && cached[0] < safeTime) {
cachedPageRuntimeConfig.delete(pageFilePath)
cachedPageStaticInfo.delete(pageFilePath)
}
}

Expand All @@ -254,9 +271,10 @@ export function getEdgeServerEntry(opts: {
bundlePath: string
config: NextConfigComplete
isDev: boolean
isServerComponent: boolean
page: string
pages: { [page: string]: string }
}): ObjectValue<webpack5.EntryObject> {
}) {
if (opts.page.match(MIDDLEWARE_ROUTE)) {
const loaderParams: MiddlewareLoaderOptions = {
absolutePagePath: opts.absolutePagePath,
Expand All @@ -283,10 +301,14 @@ export function getEdgeServerEntry(opts: {
stringifiedConfig: JSON.stringify(opts.config),
}

return `next-middleware-ssr-loader?${stringify(loaderParams)}!`
return {
import: `next-middleware-ssr-loader?${stringify(loaderParams)}!`,
layer: opts.isServerComponent ? 'sc_server' : undefined,
}
}

export function getViewsEntry(opts: {
name: string
pagePath: string
viewsDir: string
pageExtensions: string[]
Expand Down Expand Up @@ -381,10 +403,10 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
isViews ? 'views' : 'pages',
bundleFile
)
const absolutePagePath = mappings[page]

// Handle paths that have aliases
const pageFilePath = (() => {
const absolutePagePath = mappings[page]
if (absolutePagePath.startsWith(PAGES_DIR_ALIAS)) {
return absolutePagePath.replace(PAGES_DIR_ALIAS, pagesDir)
}
Expand All @@ -396,18 +418,27 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
return require.resolve(absolutePagePath)
})()

const isServerComponent = serverComponentRegex.test(absolutePagePath)

runDependingOnPageType({
page,
pageRuntime: await getPageRuntime(pageFilePath, config, isDev),
pageRuntime: (await getPageStaticInfo(pageFilePath, config, isDev))
.runtime,
onClient: () => {
client[clientBundlePath] = getClientEntry({
absolutePagePath: mappings[page],
page,
})
if (isServerComponent) {
// We skip the initial entries for server component pages and let the
// server compiler inject them instead.
} else {
client[clientBundlePath] = getClientEntry({
absolutePagePath: mappings[page],
page,
})
}
},
onServer: () => {
if (isViews && viewsDir) {
server[serverBundlePath] = getViewsEntry({
name: serverBundlePath,
pagePath: mappings[page],
viewsDir,
pageExtensions,
Expand All @@ -421,7 +452,12 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
})
}
} else {
server[serverBundlePath] = [mappings[page]]
server[serverBundlePath] = isServerComponent
? {
import: mappings[page],
layer: 'sc_server',
}
: [mappings[page]]
}
},
onEdgeServer: () => {
Expand All @@ -430,6 +466,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
absolutePagePath: mappings[page],
bundlePath: clientBundlePath,
isDev: false,
isServerComponent,
page,
})
},
Expand Down Expand Up @@ -481,10 +518,12 @@ export function finalizeEntrypoint({
name,
compilerType,
value,
isServerComponent,
}: {
compilerType?: 'client' | 'server' | 'edge-server'
name: string
value: ObjectValue<webpack5.EntryObject>
isServerComponent?: boolean
}): ObjectValue<webpack5.EntryObject> {
const entry =
typeof value !== 'object' || Array.isArray(value)
Expand All @@ -496,7 +535,7 @@ export function finalizeEntrypoint({
return {
publicPath: isApi ? '' : undefined,
runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime',
layer: isApi ? 'api' : undefined,
layer: isApi ? 'api' : isServerComponent ? 'sc_server' : undefined,
...entry,
}
}
Expand Down
117 changes: 82 additions & 35 deletions packages/next/build/index.ts
Expand Up @@ -82,7 +82,7 @@ import { runCompiler } from './compiler'
import {
createEntrypoints,
createPagesMapping,
getPageRuntime,
getPageStaticInfo,
} from './entries'
import { generateBuildId } from './generate-build-id'
import { isWriteable } from './is-writeable'
Expand Down Expand Up @@ -114,6 +114,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'

export type SsgRoute = {
initialRevalidateSeconds: number | false
Expand All @@ -139,11 +140,13 @@ export type PrerenderManifest = {
type CompilerResult = {
errors: webpack.StatsError[]
warnings: webpack.StatsError[]
stats: [
webpack.Stats | undefined,
webpack.Stats | undefined,
webpack.Stats | undefined
]
stats: (webpack.Stats | undefined)[]
}

type SingleCompilerResult = {
errors: webpack.StatsError[]
warnings: webpack.StatsError[]
stats: webpack.Stats | undefined
}

export default async function build(
Expand Down Expand Up @@ -665,7 +668,7 @@ export default async function build(
let result: CompilerResult = {
warnings: [],
errors: [],
stats: [undefined, undefined, undefined],
stats: [],
}
let webpackBuildStart
let telemetryPlugin
Expand Down Expand Up @@ -724,42 +727,85 @@ export default async function build(

// We run client and server compilation separately to optimize for memory usage
await runWebpackSpan.traceAsyncFn(async () => {
const clientResult = await runCompiler(clientConfig, {
runWebpackSpan,
})
// Fail build if clientResult contains errors
if (clientResult.errors.length > 0) {
result = {
warnings: [...clientResult.warnings],
errors: [...clientResult.errors],
stats: [clientResult.stats, undefined, undefined],
// If we are under the serverless build, we will have to run the client
// compiler first because the server compiler depends on the manifest
// files that are created by the client compiler.
// Otherwise, we run the server compilers first and then the client
// compiler to track the boundary of server/client components.

let clientResult: SingleCompilerResult | null = null
let serverResult: SingleCompilerResult | null = null
let edgeServerResult: SingleCompilerResult | null = null

if (isLikeServerless) {
if (config.experimental.serverComponents) {
throw new Error(
'Server Components are not supported in serverless mode.'
)
}

// Build client first
clientResult = await runCompiler(clientConfig, {
runWebpackSpan,
})

// Only continue if there were no errors
if (!clientResult.errors.length) {
serverResult = await runCompiler(configs[1], {
runWebpackSpan,
})
edgeServerResult = configs[2]
? await runCompiler(configs[2], { runWebpackSpan })
: null
}
} else {
const serverResult = await runCompiler(configs[1], {
// During the server compilations, entries of client components will be
// injected to this set and then will be consumed by the client compiler.
injectedClientEntries.clear()

serverResult = await runCompiler(configs[1], {
runWebpackSpan,
})
const edgeServerResult = configs[2]
edgeServerResult = configs[2]
? await runCompiler(configs[2], { runWebpackSpan })
: null

result = {
warnings: [
...clientResult.warnings,
...serverResult.warnings,
...(edgeServerResult?.warnings || []),
],
errors: [
...clientResult.errors,
...serverResult.errors,
...(edgeServerResult?.errors || []),
],
stats: [
clientResult.stats,
serverResult.stats,
edgeServerResult?.stats,
],
// Only continue if there were no errors
if (
!serverResult.errors.length &&
!edgeServerResult?.errors.length
) {
injectedClientEntries.forEach((value, key) => {
;(clientConfig.entry as webpack.EntryObject)[key] = value
})

clientResult = await runCompiler(clientConfig, {
runWebpackSpan,
})
}
}

result = {
warnings: ([] as any[])
.concat(
clientResult?.warnings,
serverResult?.warnings,
edgeServerResult?.warnings
)
.filter(nonNullable),
errors: ([] as any[])
.concat(
clientResult?.errors,
serverResult?.errors,
edgeServerResult?.errors
)
.filter(nonNullable),
stats: [
clientResult?.stats,
serverResult?.stats,
edgeServerResult?.stats,
],
}
})
result = nextBuildSpan
.traceChild('format-webpack-messages')
Expand Down Expand Up @@ -1031,7 +1077,8 @@ export default async function build(
p.startsWith(actualPage + '/index.')
)
const pageRuntime = pagePath
? await getPageRuntime(join(pagesDir, pagePath), config)
? (await getPageStaticInfo(join(pagesDir, pagePath), config))
.runtime
: undefined

if (hasServerComponents && pagePath) {
Expand Down