rsnext/packages/next/build/entries.ts
Javi Velasco 0bf6655f1c
Use edge compiler for middleware (#36486)
* Refactor `path-match`

* Simplify `createPagesMapping`

* Rename `getRawPageExtensions` -> `withoutRSCExtensions`

* Remove unused `functions-manifest-plugin.ts`

* Enable `eval-source-map` for the edge server compiler

* Use Edge Compiler for Middleware & Refactor

* Update some comments

Co-authored-by: JJ Kasper <jj@jjsweb.site>

Update packages/next/shared/lib/router/utils/path-match.ts

Co-authored-by: JJ Kasper <jj@jjsweb.site>

Update packages/next/shared/lib/router/utils/path-match.ts

Co-authored-by: JJ Kasper <jj@jjsweb.site>

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-04-27 11:50:29 +02:00

471 lines
15 KiB
TypeScript

import type { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader'
import type { MiddlewareLoaderOptions } from './webpack/loaders/next-middleware-loader'
import type { MiddlewareSSRLoaderQuery } from './webpack/loaders/next-middleware-ssr-loader'
import type { NextConfigComplete, NextConfig } from '../server/config-shared'
import type { PageRuntime } from '../server/config-shared'
import type { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader'
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
import type { LoadedEnvFiles } from '@next/env'
import fs from 'fs'
import chalk from 'next/dist/compiled/chalk'
import { posix, join } from 'path'
import { stringify } from 'querystring'
import { API_ROUTE, DOT_NEXT_ALIAS, PAGES_DIR_ALIAS } from '../lib/constants'
import { EDGE_RUNTIME_WEBPACK } from '../shared/lib/constants'
import { MIDDLEWARE_ROUTE } from '../lib/constants'
import { __ApiPreviewProps } from '../server/api-utils'
import { isTargetLikeServerless } from '../server/utils'
import { normalizePagePath } from '../server/normalize-page-path'
import { normalizePathSep } from '../server/denormalize-page-path'
import { ssrEntries } from './webpack/plugins/middleware-plugin'
import { warn } from './output/log'
import { parse } from '../build/swc'
import { isFlightPage, withoutRSCExtensions } from './utils'
type ObjectValue<T> = T extends { [key: string]: infer V } ? V : never
/**
* For a given page path removes the provided extensions. `/_app.server` is a
* special case because it is the only page where we want to preserve the RSC
* server extension.
*/
export function getPageFromPath(pagePath: string, pageExtensions: string[]) {
const extensions = pagePath.includes('/_app.server.')
? withoutRSCExtensions(pageExtensions)
: pageExtensions
const page = normalizePathSep(
pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '')
).replace(/\/index$/, '')
return page === '' ? '/' : page
}
export function createPagesMapping({
hasServerComponents,
isDev,
pageExtensions,
pagePaths,
}: {
hasServerComponents: boolean
isDev: boolean
pageExtensions: string[]
pagePaths: string[]
}): { [page: string]: string } {
const previousPages: { [key: string]: string } = {}
const pages = pagePaths.reduce<{ [key: string]: string }>(
(result, pagePath) => {
// Do not process .d.ts files inside the `pages` folder
if (pagePath.endsWith('.d.ts') && pageExtensions.includes('ts')) {
return result
}
const pageKey = getPageFromPath(pagePath, pageExtensions)
// Assume that if there's a Client Component, that there is
// a matching Server Component that will map to the page.
// so we will not process it
if (hasServerComponents && /\.client$/.test(pageKey)) {
return result
}
if (pageKey in result) {
warn(
`Duplicate page detected. ${chalk.cyan(
join('pages', previousPages[pageKey])
)} and ${chalk.cyan(
join('pages', pagePath)
)} both resolve to ${chalk.cyan(pageKey)}.`
)
} else {
previousPages[pageKey] = pagePath
}
result[pageKey] = normalizePathSep(join(PAGES_DIR_ALIAS, pagePath))
return result
},
{}
)
// In development we always alias these to allow Webpack to fallback to
// the correct source file so that HMR can work properly when a file is
// added or removed.
if (isDev) {
delete pages['/_app']
delete pages['/_app.server']
delete pages['/_error']
delete pages['/_document']
}
const root = isDev ? PAGES_DIR_ALIAS : 'next/dist/pages'
return {
'/_app': `${root}/_app`,
'/_error': `${root}/_error`,
'/_document': `${root}/_document`,
...(hasServerComponents ? { '/_app.server': `${root}/_app.server` } : {}),
...pages,
}
}
const cachedPageRuntimeConfig = new Map<string, [number, PageRuntime]>()
// @TODO: We should limit the maximum concurrency of this function as there
// could be thousands of pages existing.
export async function getPageRuntime(
pageFilePath: string,
nextConfig: Partial<NextConfig>,
isDev?: boolean
): Promise<PageRuntime> {
if (!nextConfig.experimental?.reactRoot) return undefined
const globalRuntime = nextConfig.experimental?.runtime
const cached = cachedPageRuntimeConfig.get(pageFilePath)
if (cached) {
return cached[1]
}
let pageContent: string
try {
pageContent = await fs.promises.readFile(pageFilePath, {
encoding: 'utf8',
})
} catch (err) {
if (!isDev) throw err
return undefined
}
// When gSSP or gSP is used, this page requires an execution runtime. If the
// page config is not present, we fallback to the global runtime. Related
// discussion:
// https://github.com/vercel/next.js/discussions/34179
let isRuntimeRequired: boolean = false
let pageRuntime: PageRuntime = undefined
// Since these configurations should always be static analyzable, we can
// skip these cases that "runtime" and "gSP", "gSSP" are not included in the
// source code.
if (/runtime|getStaticProps|getServerSideProps/.test(pageContent)) {
try {
const { body } = await parse(pageContent, {
filename: pageFilePath,
isModule: 'unknown',
})
for (const node of body) {
const { type, declaration } = node
if (type === 'ExportDeclaration') {
// Match `export const config`
const valueNode = declaration?.declarations?.[0]
if (valueNode?.id?.value === 'config') {
const props = valueNode.init.properties
const runtimeKeyValue = props.find(
(prop: any) => prop.key.value === 'runtime'
)
const runtime = runtimeKeyValue?.value?.value
pageRuntime =
runtime === 'edge' || runtime === 'nodejs' ? runtime : pageRuntime
} else if (declaration?.type === 'FunctionDeclaration') {
// Match `export function getStaticProps | getServerSideProps`
const identifier = declaration.identifier?.value
if (
identifier === 'getStaticProps' ||
identifier === 'getServerSideProps'
) {
isRuntimeRequired = true
}
}
} else if (type === 'ExportNamedDeclaration') {
// Match `export { getStaticProps | getServerSideProps } <from '../..'>`
const { specifiers } = node
for (const specifier of specifiers) {
const { orig } = specifier
const hasDataFetchingExports =
specifier.type === 'ExportSpecifier' &&
orig?.type === 'Identifier' &&
(orig?.value === 'getStaticProps' ||
orig?.value === 'getServerSideProps')
if (hasDataFetchingExports) {
isRuntimeRequired = true
break
}
}
}
}
} catch (err) {}
}
if (!pageRuntime) {
if (isRuntimeRequired) {
pageRuntime = globalRuntime
}
}
cachedPageRuntimeConfig.set(pageFilePath, [Date.now(), pageRuntime])
return pageRuntime
}
export function invalidatePageRuntimeCache(
pageFilePath: string,
safeTime: number
) {
const cached = cachedPageRuntimeConfig.get(pageFilePath)
if (cached && cached[0] < safeTime) {
cachedPageRuntimeConfig.delete(pageFilePath)
}
}
interface CreateEntrypointsParams {
buildId: string
config: NextConfigComplete
envFiles: LoadedEnvFiles
isDev?: boolean
pages: { [page: string]: string }
pagesDir: string
previewMode: __ApiPreviewProps
target: 'server' | 'serverless' | 'experimental-serverless-trace'
}
export function getEdgeServerEntry(opts: {
absolutePagePath: string
buildId: string
bundlePath: string
config: NextConfigComplete
isDev: boolean
page: string
pages: { [page: string]: string }
ssrEntries: Map<string, { requireFlightManifest: boolean }>
}): ObjectValue<webpack5.EntryObject> {
if (opts.page.match(MIDDLEWARE_ROUTE)) {
const loaderParams: MiddlewareLoaderOptions = {
absolutePagePath: opts.absolutePagePath,
page: opts.page,
}
return `next-middleware-loader?${stringify(loaderParams)}!`
}
const loaderParams: MiddlewareSSRLoaderQuery = {
absolute500Path: opts.pages['/500'] || '',
absoluteAppPath: opts.pages['/_app'],
absoluteAppServerPath: opts.pages['/_app.server'],
absoluteDocumentPath: opts.pages['/_document'],
absoluteErrorPath: opts.pages['/_error'],
absolutePagePath: opts.absolutePagePath,
buildId: opts.buildId,
dev: opts.isDev,
isServerComponent: isFlightPage(opts.config, opts.absolutePagePath),
page: opts.page,
stringifiedConfig: JSON.stringify(opts.config),
}
ssrEntries.set(opts.bundlePath, {
requireFlightManifest: isFlightPage(opts.config, opts.absolutePagePath),
})
return `next-middleware-ssr-loader?${stringify(loaderParams)}!`
}
export function getServerlessEntry(opts: {
absolutePagePath: string
buildId: string
config: NextConfigComplete
envFiles: LoadedEnvFiles
page: string
previewMode: __ApiPreviewProps
pages: { [page: string]: string }
}): ObjectValue<webpack5.EntryObject> {
const loaderParams: ServerlessLoaderQuery = {
absolute404Path: opts.pages['/404'] || '',
absoluteAppPath: opts.pages['/_app'],
absoluteAppServerPath: opts.pages['/_app.server'],
absoluteDocumentPath: opts.pages['/_document'],
absoluteErrorPath: opts.pages['/_error'],
absolutePagePath: opts.absolutePagePath,
assetPrefix: opts.config.assetPrefix,
basePath: opts.config.basePath,
buildId: opts.buildId,
canonicalBase: opts.config.amp.canonicalBase || '',
distDir: DOT_NEXT_ALIAS,
generateEtags: opts.config.generateEtags ? 'true' : '',
i18n: opts.config.i18n ? JSON.stringify(opts.config.i18n) : '',
// base64 encode to make sure contents don't break webpack URL loading
loadedEnvFiles: Buffer.from(JSON.stringify(opts.envFiles)).toString(
'base64'
),
page: opts.page,
poweredByHeader: opts.config.poweredByHeader ? 'true' : '',
previewProps: JSON.stringify(opts.previewMode),
reactRoot: !!opts.config.experimental.reactRoot ? 'true' : '',
runtimeConfig:
Object.keys(opts.config.publicRuntimeConfig).length > 0 ||
Object.keys(opts.config.serverRuntimeConfig).length > 0
? JSON.stringify({
publicRuntimeConfig: opts.config.publicRuntimeConfig,
serverRuntimeConfig: opts.config.serverRuntimeConfig,
})
: '',
}
return `next-serverless-loader?${stringify(loaderParams)}!`
}
export function getClientEntry(opts: {
absolutePagePath: string
page: string
}) {
const loaderOptions: ClientPagesLoaderOptions = {
absolutePagePath: opts.absolutePagePath,
page: opts.page,
}
const pageLoader = `next-client-pages-loader?${stringify(loaderOptions)}!`
// Make sure next/router is a dependency of _app or else chunk splitting
// might cause the router to not be able to load causing hydration
// to fail
return opts.page === '/_app'
? [pageLoader, require.resolve('../client/router')]
: pageLoader
}
export async function createEntrypoints(params: CreateEntrypointsParams) {
const { config, pages, pagesDir, isDev, target } = params
const edgeServer: webpack5.EntryObject = {}
const server: webpack5.EntryObject = {}
const client: webpack5.EntryObject = {}
await Promise.all(
Object.keys(pages).map(async (page) => {
const bundleFile = normalizePagePath(page)
const clientBundlePath = posix.join('pages', bundleFile)
const serverBundlePath = posix.join('pages', bundleFile)
runDependingOnPageType({
page,
pageRuntime: await getPageRuntime(
!pages[page].startsWith(PAGES_DIR_ALIAS)
? require.resolve(pages[page])
: join(pagesDir, pages[page].replace(PAGES_DIR_ALIAS, '')),
config,
isDev
),
onClient: () => {
client[clientBundlePath] = getClientEntry({
absolutePagePath: pages[page],
page,
})
},
onServer: () => {
if (isTargetLikeServerless(target)) {
if (page !== '/_app' && page !== '/_document') {
server[serverBundlePath] = getServerlessEntry({
...params,
absolutePagePath: pages[page],
page,
})
}
} else {
server[serverBundlePath] = [pages[page]]
}
},
onEdgeServer: () => {
edgeServer[serverBundlePath] = getEdgeServerEntry({
...params,
absolutePagePath: pages[page],
bundlePath: clientBundlePath,
isDev: false,
page,
ssrEntries,
})
},
})
})
)
return {
client,
server,
edgeServer,
}
}
export function runDependingOnPageType<T>(params: {
onClient: () => T
onEdgeServer: () => T
onServer: () => T
page: string
pageRuntime: PageRuntime
}) {
if (params.page.match(MIDDLEWARE_ROUTE)) {
return [params.onEdgeServer()]
} else if (params.page.match(API_ROUTE)) {
return [params.onServer()]
} else if (params.page === '/_document') {
return [params.onServer()]
} else if (
params.page === '/_app' ||
params.page === '/_error' ||
params.page === '/404' ||
params.page === '/500'
) {
return [params.onClient(), params.onServer()]
} else {
return [
params.onClient(),
params.pageRuntime === 'edge' ? params.onEdgeServer() : params.onServer(),
]
}
}
export function finalizeEntrypoint({
name,
compilerType,
value,
}: {
compilerType?: 'client' | 'server' | 'edge-server'
name: string
value: ObjectValue<webpack5.EntryObject>
}): ObjectValue<webpack5.EntryObject> {
const entry =
typeof value !== 'object' || Array.isArray(value)
? { import: value }
: value
if (compilerType === 'server') {
const isApi = name.startsWith('pages/api/')
return {
publicPath: isApi ? '' : undefined,
runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime',
layer: isApi ? 'api' : undefined,
...entry,
}
}
if (compilerType === 'edge-server') {
return {
layer: MIDDLEWARE_ROUTE.test(name) ? 'middleware' : undefined,
library: { name: ['_ENTRIES', `middleware_[name]`], type: 'assign' },
runtime: EDGE_RUNTIME_WEBPACK,
asyncChunks: false,
...entry,
}
}
if (
// Client special cases
name !== 'polyfills' &&
name !== 'main' &&
name !== 'amp' &&
name !== 'react-refresh'
) {
return {
dependOn:
name.startsWith('pages/') && name !== 'pages/_app'
? 'pages/_app'
: 'main',
...entry,
}
}
return entry
}