Update server manifest and entry creation (#45722)

This PR changes the format of server manifest, and makes the module in
the same page entry. Also adds an initial implementation of requests
handling.

NEXT-417, NEXT-488

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/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`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
This commit is contained in:
Shu Ding 2023-02-11 20:54:43 +01:00 committed by GitHub
parent f49cc8da17
commit 93311950e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 191 additions and 101 deletions

View file

@ -1,3 +1,9 @@
import type { RemotePattern } from '../shared/lib/image-config'
import type { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
import type { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
import type { NextConfigComplete } from '../server/config-shared'
import type { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import '../lib/setup-exception-listeners'
import { loadEnvConfig } from '@next/env'
import chalk from 'next/dist/compiled/chalk'
@ -99,13 +105,10 @@ import {
isReservedPage,
AppConfig,
} from './utils'
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
import { writeBuildId } from './write-build-id'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { NextConfigComplete } from '../server/config-shared'
import isError, { NextError } from '../lib/is-error'
import { isEdgeRuntime } from '../lib/is-edge-runtime'
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import { recursiveCopy } from '../lib/recursive-copy'
import { recursiveReadDir } from '../lib/recursive-readdir'
import {
@ -116,10 +119,8 @@ import {
} from './swc'
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
import { flatReaddir } from '../lib/flat-readdir'
import { RemotePattern } from '../shared/lib/image-config'
import { eventSwcPlugins } from '../telemetry/events/swc-plugins'
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
import {
RSC,
RSC_CONTENT_TYPE_HEADER,

View file

@ -1,4 +1,16 @@
import type { NextConfigComplete } from '../server/config-shared'
import type { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
import type { AssetBinding } from './webpack/loaders/get-module-build-info'
import type { GetStaticPaths, PageConfig, ServerRuntime } from 'next/types'
import type { BuildManifest } from '../server/get-page-files'
import type {
Redirect,
Rewrite,
Header,
CustomRoutes,
} from '../lib/load-custom-routes'
import type { UnwrapPromise } from '../lib/coalesced-function'
import type { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import '../server/node-polyfill-fetch'
import chalk from 'next/dist/compiled/chalk'
@ -9,12 +21,6 @@ import { promises as fs } from 'fs'
import { isValidElementType } from 'next/dist/compiled/react-is'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import browserslist from 'next/dist/compiled/browserslist'
import {
Redirect,
Rewrite,
Header,
CustomRoutes,
} from '../lib/load-custom-routes'
import {
SSG_GET_INITIAL_PROPS_CONFLICT,
SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
@ -28,10 +34,7 @@ import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters'
import { findPageFile } from '../server/lib/find-page-file'
import { GetStaticPaths, PageConfig, ServerRuntime } from 'next/types'
import { BuildManifest } from '../server/get-page-files'
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
import { UnwrapPromise } from '../lib/coalesced-function'
import { isEdgeRuntime } from '../lib/is-edge-runtime'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import * as Log from './output/log'
@ -43,16 +46,13 @@ import { trace } from '../trace'
import { setHttpClientAndAgentOptions } from '../server/config'
import { recursiveDelete } from '../lib/recursive-delete'
import { Sema } from 'next/dist/compiled/async-sema'
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
import { getRuntimeContext } from '../server/web/sandbox'
import {
loadRequireHook,
overrideBuiltInReactPackages,
} from './webpack/require-hook'
import { AssetBinding } from './webpack/loaders/get-module-build-info'
import { isClientReference } from './is-client-reference'
loadRequireHook()

View file

@ -1,28 +1,38 @@
import { generateActionId } from './utils'
export type NextFlightActionEntryLoaderOptions = {
actionPath: string
actionName: string
actions: string
}
function nextFlightActionEntryLoader(this: any) {
const { actionPath, actionName }: NextFlightActionEntryLoaderOptions =
this.getOptions()
const { actions }: NextFlightActionEntryLoaderOptions = this.getOptions()
const actionList = JSON.parse(actions) as [string, string[]][]
return `
import { ${actionName} } from ${JSON.stringify(actionPath)}
export default async function endpoint(req, res) {
try {
const result = await ${actionName}()
const serializedResult = JSON.stringify(result)
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.body(serializedResult)
} catch (err) {
res.statusCode = 500
res.setHeader('Content-Type', 'text/plain')
res.body(err.message)
}
res.send()
}`
const actions = {
${actionList
.map(([path, names]) => {
return names
.map(
(name) =>
` '${generateActionId(
path,
name
)}': () => import(/* webpackMode: "eager" */ ${JSON.stringify(
path
)}).then(mod => mod[${JSON.stringify(name)}]),`
)
.join('\n')
})
.join('\n')}
}
export default async function endpoint(id, bound) {
const action = await actions[id]()
return action.apply(null, bound)
}
`
}
export default nextFlightActionEntryLoader

View file

@ -45,9 +45,16 @@ export const injectedClientEntries = new Map()
export const serverModuleIds = new Map<string, string | number>()
export const edgeServerModuleIds = new Map<string, string | number>()
// A map to track "path:name" -> "hash".
let serverActions: Record<string, string> = {}
export type ActionManifest = {
[actionId: string]: {
workers: {
[name: string]: string | number
}
}
}
// A map to track "action" -> "list of bundles".
let serverActions: ActionManifest = {}
let serverCSSManifest: FlightCSSManifest = {}
let edgeServerCSSManifest: FlightCSSManifest = {}
@ -82,17 +89,6 @@ export class FlightClientEntryPlugin {
})
compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation) => {
traverseModules(compilation, (mod) => {
// const modId = compilation.chunkGraph.getModuleId(mod) + ''
// The module must has request, and resource so it's not a new entry created with loader.
// Using the client layer module, which doesn't have `rsc` tag in buildInfo.
if (mod.request && mod.resource && !mod.buildInfo.rsc) {
if (compilation.moduleGraph.isAsync(mod)) {
ASYNC_CLIENT_MODULES.add(mod.resource)
}
}
})
const recordModule = (modId: string, mod: any) => {
const modResource = mod.resourceResolveData?.path || mod.resource
@ -124,6 +120,14 @@ export class FlightClientEntryPlugin {
}
traverseModules(compilation, (mod, _chunk, _chunkGroup, modId) => {
// The module must has request, and resource so it's not a new entry created with loader.
// Using the client layer module, which doesn't have `rsc` tag in buildInfo.
if (mod.request && mod.resource && !mod.buildInfo.rsc) {
if (compilation.moduleGraph.isAsync(mod)) {
ASYNC_CLIENT_MODULES.add(mod.resource)
}
}
recordModule(String(modId), mod)
})
})
@ -136,7 +140,7 @@ export class FlightClientEntryPlugin {
// asset hash via extract mini css plugin.
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH,
},
(assets) => this.createAsset(assets)
(assets) => this.createAsset(compilation, assets)
)
})
}
@ -261,20 +265,19 @@ export class FlightClientEntryPlugin {
})
)
// Create action entries
// Create action entry
serverActions = {}
actionEntryImports.forEach((actionNames, actionPath) => {
for (const actionName of actionNames) {
addActionEntryList.push(
this.injectActionEntry({
compiler,
compilation,
actionPath,
actionName,
})
)
}
})
if (actionEntryImports.size > 0) {
addActionEntryList.push(
this.injectActionEntry({
compiler,
compilation,
actions: actionEntryImports,
entryName: name,
bundlePath: name,
})
)
}
})
// After optimizing all the modules, we collect the CSS that are still used
@ -434,6 +437,9 @@ export class FlightClientEntryPlugin {
(addClientEntryAndSSRModules) => addClientEntryAndSSRModules[1]
)
)
// Wait for action entries to be added.
await Promise.all(addActionEntryList)
}
collectComponentInfoFromDependencies({
@ -653,43 +659,48 @@ export class FlightClientEntryPlugin {
injectActionEntry({
compiler,
compilation,
actionPath,
actionName,
actions,
entryName,
bundlePath,
}: {
compiler: webpack.Compiler
compilation: webpack.Compilation
actionPath: string
actionName: string
actions: Map<string, string[]>
entryName: string
bundlePath: string
}) {
const actionsArray = Array.from(actions.entries())
const actionLoader = `next-flight-action-entry-loader?${stringify({
actionPath,
actionName,
actions: JSON.stringify(actionsArray),
})}!`
const actionHash = generateActionId(actionPath, actionName)
serverActions[actionPath + ':' + actionName] = actionHash
// Inject the entry to the server compiler (__sc_client__).
const actionEntryDep = webpack.EntryPlugin.createDependency(actionLoader, {
name: 'action/' + actionHash,
})
return new Promise((resolve, reject) => {
compilation.addEntry(
compiler.context,
actionEntryDep,
{
name: 'action/' + actionHash,
layer: WEBPACK_LAYERS.server,
},
(err?: Error | null, entry?: any) => {
if (err) {
return reject(err)
for (const [p, names] of actionsArray) {
for (const name of names) {
const id = generateActionId(p, name)
if (typeof serverActions[id] === 'undefined') {
serverActions[id] = {
workers: {},
}
resolve(entry)
}
)
serverActions[id].workers[bundlePath] = ''
}
}
// Inject the entry to the server compiler
const actionEntryDep = webpack.EntryPlugin.createDependency(actionLoader, {
name: bundlePath,
})
return this.addEntry(
compilation,
// Reuse compilation context.
compiler.context,
actionEntryDep,
{
name: entryName,
layer: WEBPACK_LAYERS.server,
}
)
}
addEntry(
@ -721,7 +732,30 @@ export class FlightClientEntryPlugin {
})
}
createAsset(assets: webpack.Compilation['assets']) {
createAsset(
compilation: webpack.Compilation,
assets: webpack.Compilation['assets']
) {
const actionModId: Record<string, string | number> = {}
traverseModules(compilation, (mod, _chunk, chunkGroup, modId) => {
// Go through all action entries and record the module ID for each entry.
if (
chunkGroup.name &&
mod.request &&
/next-flight-action-entry-loader/.test(mod.request)
) {
actionModId[chunkGroup.name] = modId
}
})
for (let id in serverActions) {
const action = serverActions[id]
for (let name in action.workers) {
action.workers[name] = actionModId[name]
}
}
const file = ACTIONS_MANIFEST
const json = JSON.stringify(serverActions, null, this.dev ? 2 : undefined)
assets[file + '.json'] = new sources.RawSource(

View file

@ -1,4 +1,6 @@
export const RSC = 'RSC' as const
export const ACTION = 'Action' as const
export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
export const FETCH_CACHE_HEADER = 'x-vercel-sc-headers' as const

View file

@ -39,6 +39,7 @@ import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/lazy-dynamic/no-ssr-erro
import { HeadManagerContext } from '../shared/lib/head-manager-context'
import stringHash from 'next/dist/compiled/string-hash'
import {
ACTION,
NEXT_ROUTER_PREFETCH,
NEXT_ROUTER_STATE_TREE,
RSC,
@ -204,6 +205,15 @@ class FlightRenderResult extends RenderResult {
}
}
/**
* Action Response is set to application/json for now, but could be changed in the future.
*/
class ActionRenderResult extends RenderResult {
constructor(response: string) {
super(response, { contentType: 'application/json' })
}
}
/**
* Interop between "export default" and "module.exports".
*/
@ -896,6 +906,11 @@ export async function renderToHTMLOrFlight(
renderOpts: RenderOpts
): Promise<RenderResult | null> {
const isFlight = req.headers[RSC.toLowerCase()] !== undefined
const actionId = req.headers[ACTION.toLowerCase()]
const isAction =
actionId !== undefined &&
typeof actionId === 'string' &&
req.method === 'POST'
const capturedErrors: Error[] = []
const allCapturedErrors: Error[] = []
@ -922,6 +937,7 @@ export async function renderToHTMLOrFlight(
buildManifest,
subresourceIntegrityManifest,
serverComponentManifest,
serverActionsManifest,
serverCSSManifest = {},
ComponentMod,
dev,
@ -1794,6 +1810,23 @@ export async function renderToHTMLOrFlight(
return generateFlight()
}
// For action requests, we handle them differently with a sepcial render result.
if (isAction && process.env.NEXT_RUNTIME !== 'edge') {
const workerName = 'app' + renderOpts.pathname
const actionModId = serverActionsManifest[actionId].workers[workerName]
const { parseBody } =
require('./api-utils/node') as typeof import('./api-utils/node')
const actionData = (await parseBody(req, '1mb')) || {}
const actionHandler =
ComponentMod.__next_app_webpack_require__(actionModId).default
return new ActionRenderResult(
JSON.stringify(await actionHandler(actionId, actionData.bound || []))
)
}
// Below this line is handling for rendering to HTML.
// AppRouter is provided by next-app-loader

View file

@ -13,6 +13,7 @@ import {
BUILD_MANIFEST,
REACT_LOADABLE_MANIFEST,
FLIGHT_MANIFEST,
ACTIONS_MANIFEST,
} from '../shared/lib/constants'
import { join } from 'path'
import { requirePage } from './require'
@ -33,6 +34,7 @@ export type LoadComponentsReturnType = {
subresourceIntegrityManifest?: Record<string, string>
reactLoadableManifest: ReactLoadableManifest
serverComponentManifest?: any
serverActionsManifest?: any
Document: DocumentType
App: AppType
getStaticProps?: GetStaticProps
@ -97,16 +99,23 @@ export async function loadComponents({
requirePage(pathname, distDir, isAppPath)
)
const [buildManifest, reactLoadableManifest, serverComponentManifest] =
await Promise.all([
loadManifest<BuildManifest>(join(distDir, BUILD_MANIFEST)),
loadManifest<ReactLoadableManifest>(
join(distDir, REACT_LOADABLE_MANIFEST)
),
hasServerComponents
? loadManifest(join(distDir, 'server', FLIGHT_MANIFEST + '.json'))
: null,
])
const [
buildManifest,
reactLoadableManifest,
serverComponentManifest,
serverActionsManifest,
] = await Promise.all([
loadManifest<BuildManifest>(join(distDir, BUILD_MANIFEST)),
loadManifest<ReactLoadableManifest>(join(distDir, REACT_LOADABLE_MANIFEST)),
hasServerComponents
? loadManifest(join(distDir, 'server', FLIGHT_MANIFEST + '.json'))
: null,
hasServerComponents
? loadManifest(join(distDir, 'server', ACTIONS_MANIFEST + '.json')).catch(
() => null
)
: null,
])
const Component = interopDefault(ComponentMod)
const Document = interopDefault(DocumentMod)
@ -126,6 +135,7 @@ export async function loadComponents({
getStaticProps,
getStaticPaths,
serverComponentManifest,
serverActionsManifest,
isAppPath,
pathname,
}