Skip to content

Commit

Permalink
Decouple entries for server components and client components (#36860)
Browse files Browse the repository at this point in the history
* (wip)

* dev mode

* build mode

* update comment

* fix tests

* fix _N_SSP and _N_SSG exports

* fix missing variables

* fix api route bug

* fix compiler stats

* fix lint errors

* add extra cache group for edge server

* fix test

* fix test

* fix views route meta and entries

* fix linter error

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
shuding and kodiakhq[bot] committed May 13, 2022
1 parent 9e568da commit b122178
Show file tree
Hide file tree
Showing 22 changed files with 600 additions and 437 deletions.
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

0 comments on commit b122178

Please sign in to comment.