Skip to content

Commit

Permalink
Custom app for switchable runtime (#35666)
Browse files Browse the repository at this point in the history
x-ref: #33149
RFCs:
- #30996
- #31506 

## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`
  • Loading branch information
huozhi committed Apr 5, 2022
1 parent 739e6f0 commit 67d25a5
Show file tree
Hide file tree
Showing 25 changed files with 275 additions and 186 deletions.
29 changes: 25 additions & 4 deletions packages/next/build/entries.ts
Expand Up @@ -19,7 +19,12 @@ import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-lo
import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader'
import { LoadedEnvFiles } from '@next/env'
import { parse } from '../build/swc'
import { isCustomErrorPage, isFlightPage, isReservedPage } from './utils'
import {
getRawPageExtensions,
isCustomErrorPage,
isFlightPage,
isReservedPage,
} from './utils'
import { ssrEntries } from './webpack/plugins/middleware-plugin'
import {
MIDDLEWARE_RUNTIME_WEBPACK,
Expand All @@ -32,7 +37,14 @@ export type PagesMapping = {
}

export function getPageFromPath(pagePath: string, extensions: string[]) {
let page = pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '')
const rawExtensions = getRawPageExtensions(extensions)
const pickedExtensions = pagePath.includes('/_app.server.')
? rawExtensions
: extensions
let page = pagePath.replace(
new RegExp(`\\.+(${pickedExtensions.join('|')})$`),
''
)
page = page.replace(/\\/g, '/').replace(/\/index$/, '')
return page === '' ? '/' : page
}
Expand Down Expand Up @@ -86,13 +98,20 @@ export function createPagesMapping(
// allow falling back to the correct source file so
// that HMR can work properly when a file is added/removed
if (isDev) {
if (hasServerComponents) {
pages['/_app.server'] = `${PAGES_DIR_ALIAS}/_app.server`
}
pages['/_app'] = `${PAGES_DIR_ALIAS}/_app`
pages['/_error'] = `${PAGES_DIR_ALIAS}/_error`
pages['/_document'] = `${PAGES_DIR_ALIAS}/_document`
} else {
if (hasServerComponents) {
pages['/_app.server'] =
pages['/_app.server'] || 'next/dist/pages/_app.server'
}
pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app'
pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error'
pages['/_document'] = pages['/_document'] || `next/dist/pages/_document`
pages['/_document'] = pages['/_document'] || 'next/dist/pages/_document'
}
return pages
}
Expand Down Expand Up @@ -229,6 +248,7 @@ export async function createEntrypoints(

const defaultServerlessOptions = {
absoluteAppPath: pages['/_app'],
absoluteAppServerPath: pages['/_app.server'],
absoluteDocumentPath: pages['/_document'],
absoluteErrorPath: pages['/_error'],
absolute404Path: pages['/404'] || '',
Expand Down Expand Up @@ -320,6 +340,7 @@ export async function createEntrypoints(
} else if (
isLikeServerless &&
page !== '/_app' &&
page !== '/_app.server' &&
page !== '/_document' &&
!isEdgeRuntime
) {
Expand All @@ -333,7 +354,7 @@ export async function createEntrypoints(
)}!`
}

if (page === '/_document') {
if (page === '/_document' || page === '/_app.server') {
return
}

Expand Down
10 changes: 8 additions & 2 deletions packages/next/build/utils.ts
Expand Up @@ -144,6 +144,11 @@ export async function printTreeView(
]

const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions)
const hasCustomAppServer = await findPageFile(
pagesDir,
'/_app.server',
pageExtensions
)

pageInfos.set('/404', {
...(pageInfos.get('/404') || pageInfos.get('/_error')),
Expand All @@ -170,7 +175,8 @@ export async function printTreeView(
!(
e === '/_document' ||
e === '/_error' ||
(!hasCustomApp && e === '/_app')
(!hasCustomApp && e === '/_app') ||
(!hasCustomAppServer && e === '/_app.server')
)
)
.sort((a, b) => a.localeCompare(b))
Expand All @@ -192,7 +198,7 @@ export async function printTreeView(
(pageInfo?.ssgPageDurations?.reduce((a, b) => a + (b || 0), 0) || 0)

const symbol =
item === '/_app'
item === '/_app' || item === '/_app.server'
? ' '
: item.endsWith('/_middleware')
? 'ƒ'
Expand Down
14 changes: 9 additions & 5 deletions packages/next/build/webpack-config.ts
Expand Up @@ -477,9 +477,6 @@ export default async function getBaseWebpackConfig(
const serverComponentsRegex = new RegExp(
`\\.server\\.(${rawPageExtensions.join('|')})$`
)
const clientComponentsRegex = new RegExp(
`\\.client\\.(${rawPageExtensions.join('|')})$`
)

const babelIncludeRegexes: RegExp[] = [
/next[\\/]dist[\\/]shared[\\/]lib/,
Expand Down Expand Up @@ -554,12 +551,19 @@ export default async function getBaseWebpackConfig(

if (dev) {
customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [
...config.pageExtensions.reduce((prev, ext) => {
...rawPageExtensions.reduce((prev, ext) => {
prev.push(path.join(pagesDir, `_app.${ext}`))
return prev
}, [] as string[]),
'next/dist/pages/_app.js',
]
customAppAliases[`${PAGES_DIR_ALIAS}/_app.server`] = [
...rawPageExtensions.reduce((prev, ext) => {
prev.push(path.join(pagesDir, `_app.server.${ext}`))
return prev
}, [] as string[]),
'next/dist/pages/_app.server.js',
]
customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [
...config.pageExtensions.reduce((prev, ext) => {
prev.push(path.join(pagesDir, `_error.${ext}`))
Expand Down Expand Up @@ -1509,7 +1513,7 @@ export default async function getBaseWebpackConfig(
}),
hasServerComponents &&
!isServer &&
new FlightManifestPlugin({ dev, clientComponentsRegex }),
new FlightManifestPlugin({ dev, pageExtensions: rawPageExtensions }),
!dev &&
!isServer &&
new TelemetryPlugin(
Expand Down
Expand Up @@ -115,7 +115,7 @@ export default async function transformSource(
const names: string[] = []
await parseModuleInfo(resourcePath, transformedSource, names)

// next.js/packages/next/<component>.js
// Next.js built-in client components
if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) {
names.push('default')
}
Expand Down
28 changes: 10 additions & 18 deletions packages/next/build/webpack/loaders/next-flight-server-loader.ts
@@ -1,26 +1,25 @@
import { parse } from '../../swc'
import { getRawPageExtensions } from '../../utils'
import { buildExports } from './utils'

const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif']

const createClientComponentFilter = (pageExtensions: string[]) => {
export const createClientComponentFilter = (pageExtensions: string[]) => {
// Special cases for Next.js APIs that are considered as client components:
// - .client.[ext]
// - next/link, next/image
// - next built-in client components
// - .[imageExt]
const regex = new RegExp(
'(' +
`\\.client(\\.(${pageExtensions.join('|')}))?|` +
`next/link|next/image|` +
`next/(link|image)(\\.js)?|` +
`\\.(${imageExtensions.join('|')})` +
')$'
)

return (importSource: string) => regex.test(importSource)
}

const createServerComponentFilter = (pageExtensions: string[]) => {
export const createServerComponentFilter = (pageExtensions: string[]) => {
const regex = new RegExp(`\\.server(\\.(${pageExtensions.join('|')}))?$`)
return (importSource: string) => regex.test(importSource)
}
Expand Down Expand Up @@ -166,15 +165,8 @@ export default async function transformSource(
throw new Error('Expected source to have been transformed to a string.')
}

// We currently assume that all components are shared components (unsuffixed)
// from node_modules.
if (resourcePath.includes('/node_modules/')) {
return source
}

const rawRawPageExtensions = getRawPageExtensions(pageExtensions)
const isServerComponent = createServerComponentFilter(rawRawPageExtensions)
const isClientComponent = createClientComponentFilter(rawRawPageExtensions)
const isServerComponent = createServerComponentFilter(pageExtensions)
const isClientComponent = createClientComponentFilter(pageExtensions)

if (!isClientCompilation) {
// We only apply the loader to server components, or shared components that
Expand Down Expand Up @@ -206,19 +198,19 @@ export default async function transformSource(
*
* Server compilation output:
* (The content of the Server Component module will be kept.)
* export const __next_rsc__ = { __webpack_require__, _: () => { ... } }
* export const __next_rsc__ = { __webpack_require__, _: () => { ... }, server: true }
*
* Client compilation output:
* (The content of the Server Component module will be removed.)
* export const __next_rsc__ = { __webpack_require__, _: () => { ... } }
* export const __next_rsc__ = { __webpack_require__, _: () => { ... }, server: false }
*/

const rscExports: any = {
__next_rsc__: `{
__webpack_require__,
_: () => {\n${imports}\n}
_: () => {\n${imports}\n},
server: ${isServerComponent(resourcePath) ? 'true' : 'false'}
}`,
__next_rsc_server__: isServerComponent(resourcePath) ? 'true' : 'false',
}

if (isClientCompilation) {
Expand Down
Expand Up @@ -7,6 +7,7 @@ export default async function middlewareSSRLoader(this: any) {
buildId,
absolutePagePath,
absoluteAppPath,
absoluteAppServerPath,
absoluteDocumentPath,
absolute500Path,
absoluteErrorPath,
Expand All @@ -16,11 +17,15 @@ export default async function middlewareSSRLoader(this: any) {

const stringifiedPagePath = stringifyRequest(this, absolutePagePath)
const stringifiedAppPath = stringifyRequest(this, absoluteAppPath)
const stringifiedAppServerPath = absoluteAppServerPath
? stringifyRequest(this, absoluteAppServerPath)
: null

const stringifiedErrorPath = stringifyRequest(this, absoluteErrorPath)
const stringifiedDocumentPath = stringifyRequest(this, absoluteDocumentPath)
const stringified500Path = absolute500Path
? stringifyRequest(this, absolute500Path)
: 'null'
: null

const transformed = `
import { adapter } from 'next/dist/server/web/adapter'
Expand All @@ -31,9 +36,15 @@ export default async function middlewareSSRLoader(this: any) {
import Document from ${stringifiedDocumentPath}
const appMod = require(${stringifiedAppPath})
const appServerMod = ${
stringifiedAppServerPath ? `require(${stringifiedAppServerPath})` : 'null'
}
const pageMod = require(${stringifiedPagePath})
const errorMod = require(${stringifiedErrorPath})
const error500Mod = ${stringified500Path} ? require(${stringified500Path}) : null
const error500Mod = ${
stringified500Path ? `require(${stringified500Path})` : 'null'
}
const buildManifest = self.__BUILD_MANIFEST
const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST
Expand All @@ -44,7 +55,7 @@ export default async function middlewareSSRLoader(this: any) {
page: ${JSON.stringify(page)},
buildId: ${JSON.stringify(buildId)},
}
const render = getRender({
dev: ${dev},
page: ${JSON.stringify(page)},
Expand All @@ -56,6 +67,7 @@ export default async function middlewareSSRLoader(this: any) {
buildManifest,
reactLoadableManifest,
serverComponentManifest: ${isServerComponent} ? rscManifest : null,
appServerMod,
config: ${stringifiedConfig},
buildId: ${JSON.stringify(buildId)},
})
Expand Down
Expand Up @@ -27,6 +27,7 @@ export function getRender({
serverComponentManifest,
config,
buildId,
appServerMod,
}: {
dev: boolean
page: string
Expand All @@ -37,7 +38,8 @@ export function getRender({
Document: DocumentType
buildManifest: BuildManifest
reactLoadableManifest: ReactLoadableManifest
serverComponentManifest: any | null
serverComponentManifest: any
appServerMod: any
config: NextConfig
buildId: string
}) {
Expand All @@ -48,6 +50,7 @@ export function getRender({
Document,
App: appMod.default as AppType,
AppMod: appMod,
AppServerMod: appServerMod,
}

const server = new WebServer({
Expand Down
Expand Up @@ -18,6 +18,7 @@ export type ServerlessLoaderQuery = {
distDir: string
absolutePagePath: string
absoluteAppPath: string
absoluteAppServerPath: string
absoluteDocumentPath: string
absoluteErrorPath: string
absolute404Path: string
Expand Down
Expand Up @@ -65,7 +65,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {
_params?: any
) {
let Component
let App
let AppMod
let config
let Document
let Error
Expand All @@ -78,7 +78,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {
getServerSideProps,
getStaticPaths,
Component,
App,
AppMod,
config,
{ default: Document },
{ default: Error },
Expand All @@ -103,8 +103,9 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))

const options = {
App,
AppMod,
Document,
ComponentMod: { default: Component },
buildManifest,
getStaticProps,
getServerSideProps,
Expand Down

0 comments on commit 67d25a5

Please sign in to comment.