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:
parent
f49cc8da17
commit
93311950e1
7 changed files with 191 additions and 101 deletions
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue