24f450e8aa
Reverts vercel/next.js#35245
437 lines
13 KiB
TypeScript
437 lines
13 KiB
TypeScript
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 { MIDDLEWARE_ROUTE } from '../lib/constants'
|
|
import { __ApiPreviewProps } from '../server/api-utils'
|
|
import { isTargetLikeServerless } from '../server/utils'
|
|
import { normalizePagePath } from '../server/normalize-page-path'
|
|
import { warn } from './output/log'
|
|
import { MiddlewareLoaderOptions } from './webpack/loaders/next-middleware-loader'
|
|
import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader'
|
|
import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader'
|
|
import { LoadedEnvFiles } from '@next/env'
|
|
import { NextConfigComplete } from '../server/config-shared'
|
|
import { parse } from '../build/swc'
|
|
import { isCustomErrorPage, isFlightPage, isReservedPage } from './utils'
|
|
import { ssrEntries } from './webpack/plugins/middleware-plugin'
|
|
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
|
|
import {
|
|
MIDDLEWARE_RUNTIME_WEBPACK,
|
|
MIDDLEWARE_SSR_RUNTIME_WEBPACK,
|
|
} from '../shared/lib/constants'
|
|
|
|
type ObjectValue<T> = T extends { [key: string]: infer V } ? V : never
|
|
export type PagesMapping = {
|
|
[page: string]: string
|
|
}
|
|
|
|
export function getPageFromPath(pagePath: string, extensions: string[]) {
|
|
let page = pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '')
|
|
page = page.replace(/\\/g, '/').replace(/\/index$/, '')
|
|
return page === '' ? '/' : page
|
|
}
|
|
|
|
export function createPagesMapping(
|
|
pagePaths: string[],
|
|
extensions: string[],
|
|
{
|
|
isDev,
|
|
hasServerComponents,
|
|
}: {
|
|
isDev: boolean
|
|
hasServerComponents: boolean
|
|
}
|
|
): PagesMapping {
|
|
const previousPages: PagesMapping = {}
|
|
|
|
// Do not process .d.ts files inside the `pages` folder
|
|
pagePaths = extensions.includes('ts')
|
|
? pagePaths.filter((pagePath) => !pagePath.endsWith('.d.ts'))
|
|
: pagePaths
|
|
|
|
const pages: PagesMapping = pagePaths.reduce(
|
|
(result: PagesMapping, pagePath): PagesMapping => {
|
|
const pageKey = getPageFromPath(pagePath, extensions)
|
|
|
|
if (hasServerComponents && /\.client$/.test(pageKey)) {
|
|
// Assume that if there's a Client Component, that there is
|
|
// a matching Server Component that will map to the page.
|
|
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] = join(PAGES_DIR_ALIAS, pagePath).replace(/\\/g, '/')
|
|
return result
|
|
},
|
|
{}
|
|
)
|
|
|
|
// we alias these in development and allow webpack to
|
|
// allow falling back to the correct source file so
|
|
// that HMR can work properly when a file is added/removed
|
|
if (isDev) {
|
|
pages['/_app'] = `${PAGES_DIR_ALIAS}/_app`
|
|
pages['/_error'] = `${PAGES_DIR_ALIAS}/_error`
|
|
pages['/_document'] = `${PAGES_DIR_ALIAS}/_document`
|
|
} else {
|
|
pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app'
|
|
pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error'
|
|
pages['/_document'] = pages['/_document'] || `next/dist/pages/_document`
|
|
}
|
|
return pages
|
|
}
|
|
|
|
type Entrypoints = {
|
|
client: webpack5.EntryObject
|
|
server: webpack5.EntryObject
|
|
edgeServer: webpack5.EntryObject
|
|
}
|
|
|
|
const cachedPageRuntimeConfig = new Map<
|
|
string,
|
|
[number, 'nodejs' | 'edge' | undefined]
|
|
>()
|
|
|
|
// @TODO: We should limit the maximum concurrency of this function as there
|
|
// could be thousands of pages existing.
|
|
export async function getPageRuntime(
|
|
pageFilePath: string,
|
|
globalRuntimeFallback?: 'nodejs' | 'edge'
|
|
): Promise<'nodejs' | 'edge' | undefined> {
|
|
const cached = cachedPageRuntimeConfig.get(pageFilePath)
|
|
if (cached) {
|
|
return cached[1]
|
|
}
|
|
|
|
let pageContent: string
|
|
try {
|
|
pageContent = await fs.promises.readFile(pageFilePath, {
|
|
encoding: 'utf8',
|
|
})
|
|
} catch (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: 'nodejs' | 'edge' | undefined = 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: true,
|
|
})
|
|
|
|
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 = globalRuntimeFallback
|
|
} else {
|
|
// @TODO: Remove this branch to fully implement the RFC.
|
|
pageRuntime = globalRuntimeFallback
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
export async function createEntrypoints(
|
|
pages: PagesMapping,
|
|
target: 'server' | 'serverless' | 'experimental-serverless-trace',
|
|
buildId: string,
|
|
previewMode: __ApiPreviewProps,
|
|
config: NextConfigComplete,
|
|
loadedEnvFiles: LoadedEnvFiles,
|
|
pagesDir: string
|
|
): Promise<Entrypoints> {
|
|
const client: webpack5.EntryObject = {}
|
|
const server: webpack5.EntryObject = {}
|
|
const edgeServer: webpack5.EntryObject = {}
|
|
|
|
const hasRuntimeConfig =
|
|
Object.keys(config.publicRuntimeConfig).length > 0 ||
|
|
Object.keys(config.serverRuntimeConfig).length > 0
|
|
|
|
const defaultServerlessOptions = {
|
|
absoluteAppPath: pages['/_app'],
|
|
absoluteDocumentPath: pages['/_document'],
|
|
absoluteErrorPath: pages['/_error'],
|
|
absolute404Path: pages['/404'] || '',
|
|
distDir: DOT_NEXT_ALIAS,
|
|
buildId,
|
|
assetPrefix: config.assetPrefix,
|
|
generateEtags: config.generateEtags ? 'true' : '',
|
|
poweredByHeader: config.poweredByHeader ? 'true' : '',
|
|
canonicalBase: config.amp.canonicalBase || '',
|
|
basePath: config.basePath,
|
|
runtimeConfig: hasRuntimeConfig
|
|
? JSON.stringify({
|
|
publicRuntimeConfig: config.publicRuntimeConfig,
|
|
serverRuntimeConfig: config.serverRuntimeConfig,
|
|
})
|
|
: '',
|
|
previewProps: JSON.stringify(previewMode),
|
|
// base64 encode to make sure contents don't break webpack URL loading
|
|
loadedEnvFiles: Buffer.from(JSON.stringify(loadedEnvFiles)).toString(
|
|
'base64'
|
|
),
|
|
i18n: config.i18n ? JSON.stringify(config.i18n) : '',
|
|
reactRoot: config.experimental.reactRoot ? 'true' : '',
|
|
}
|
|
|
|
const globalRuntime = config.experimental.runtime
|
|
|
|
await Promise.all(
|
|
Object.keys(pages).map(async (page) => {
|
|
const absolutePagePath = pages[page]
|
|
const bundleFile = normalizePagePath(page)
|
|
const isApiRoute = page.match(API_ROUTE)
|
|
|
|
const clientBundlePath = posix.join('pages', bundleFile)
|
|
const serverBundlePath = posix.join('pages', bundleFile)
|
|
|
|
const isLikeServerless = isTargetLikeServerless(target)
|
|
const isReserved = isReservedPage(page)
|
|
const isCustomError = isCustomErrorPage(page)
|
|
const isFlight = isFlightPage(config, absolutePagePath)
|
|
const isEdgeRuntime =
|
|
(await getPageRuntime(
|
|
join(pagesDir, absolutePagePath.slice(PAGES_DIR_ALIAS.length + 1)),
|
|
globalRuntime
|
|
)) === 'edge'
|
|
|
|
if (page.match(MIDDLEWARE_ROUTE)) {
|
|
const loaderOpts: MiddlewareLoaderOptions = {
|
|
absolutePagePath: pages[page],
|
|
page,
|
|
}
|
|
|
|
client[clientBundlePath] = `next-middleware-loader?${stringify(
|
|
loaderOpts
|
|
)}!`
|
|
return
|
|
}
|
|
|
|
if (isEdgeRuntime && !isReserved && !isCustomError && !isApiRoute) {
|
|
ssrEntries.set(clientBundlePath, { requireFlightManifest: isFlight })
|
|
edgeServer[serverBundlePath] = finalizeEntrypoint({
|
|
name: '[name].js',
|
|
value: `next-middleware-ssr-loader?${stringify({
|
|
dev: false,
|
|
page,
|
|
stringifiedConfig: JSON.stringify(config),
|
|
absolute500Path: pages['/500'] || '',
|
|
absolutePagePath,
|
|
isServerComponent: isFlight,
|
|
...defaultServerlessOptions,
|
|
} as any)}!`,
|
|
isServer: false,
|
|
isEdgeServer: true,
|
|
})
|
|
}
|
|
|
|
if (isApiRoute && isLikeServerless) {
|
|
const serverlessLoaderOptions: ServerlessLoaderQuery = {
|
|
page,
|
|
absolutePagePath,
|
|
...defaultServerlessOptions,
|
|
}
|
|
server[serverBundlePath] = `next-serverless-loader?${stringify(
|
|
serverlessLoaderOptions
|
|
)}!`
|
|
} else if (isApiRoute || target === 'server') {
|
|
if (!isEdgeRuntime || isReserved || isCustomError) {
|
|
server[serverBundlePath] = [absolutePagePath]
|
|
}
|
|
} else if (
|
|
isLikeServerless &&
|
|
page !== '/_app' &&
|
|
page !== '/_document' &&
|
|
!isEdgeRuntime
|
|
) {
|
|
const serverlessLoaderOptions: ServerlessLoaderQuery = {
|
|
page,
|
|
absolutePagePath,
|
|
...defaultServerlessOptions,
|
|
}
|
|
server[serverBundlePath] = `next-serverless-loader?${stringify(
|
|
serverlessLoaderOptions
|
|
)}!`
|
|
}
|
|
|
|
if (page === '/_document') {
|
|
return
|
|
}
|
|
|
|
if (!isApiRoute) {
|
|
const pageLoaderOpts: ClientPagesLoaderOptions = {
|
|
page,
|
|
absolutePagePath,
|
|
}
|
|
const pageLoader = `next-client-pages-loader?${stringify(
|
|
pageLoaderOpts
|
|
)}!`
|
|
|
|
// 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
|
|
|
|
client[clientBundlePath] =
|
|
page === '/_app'
|
|
? [pageLoader, require.resolve('../client/router')]
|
|
: pageLoader
|
|
}
|
|
})
|
|
)
|
|
|
|
return {
|
|
client,
|
|
server,
|
|
edgeServer,
|
|
}
|
|
}
|
|
|
|
export function finalizeEntrypoint({
|
|
name,
|
|
value,
|
|
isServer,
|
|
isMiddleware,
|
|
isEdgeServer,
|
|
}: {
|
|
isServer: boolean
|
|
name: string
|
|
value: ObjectValue<webpack5.EntryObject>
|
|
isMiddleware?: boolean
|
|
isEdgeServer?: boolean
|
|
}): ObjectValue<webpack5.EntryObject> {
|
|
const entry =
|
|
typeof value !== 'object' || Array.isArray(value)
|
|
? { import: value }
|
|
: value
|
|
|
|
if (isServer) {
|
|
const isApi = name.startsWith('pages/api/')
|
|
return {
|
|
publicPath: isApi ? '' : undefined,
|
|
runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime',
|
|
layer: isApi ? 'api' : undefined,
|
|
...entry,
|
|
}
|
|
}
|
|
|
|
if (isEdgeServer) {
|
|
const ssrMiddlewareEntry = {
|
|
library: {
|
|
name: ['_ENTRIES', `middleware_[name]`],
|
|
type: 'assign',
|
|
},
|
|
runtime: MIDDLEWARE_SSR_RUNTIME_WEBPACK,
|
|
asyncChunks: false,
|
|
...entry,
|
|
}
|
|
return ssrMiddlewareEntry
|
|
}
|
|
if (isMiddleware) {
|
|
const middlewareEntry = {
|
|
filename: 'server/[name].js',
|
|
layer: 'middleware',
|
|
library: {
|
|
name: ['_ENTRIES', `middleware_[name]`],
|
|
type: 'assign',
|
|
},
|
|
runtime: MIDDLEWARE_RUNTIME_WEBPACK,
|
|
asyncChunks: false,
|
|
...entry,
|
|
}
|
|
return middlewareEntry
|
|
}
|
|
|
|
if (
|
|
name !== 'polyfills' &&
|
|
name !== 'main' &&
|
|
name !== 'amp' &&
|
|
name !== 'react-refresh'
|
|
) {
|
|
return {
|
|
dependOn:
|
|
name.startsWith('pages/') && name !== 'pages/_app'
|
|
? 'pages/_app'
|
|
: 'main',
|
|
...entry,
|
|
}
|
|
}
|
|
|
|
return entry
|
|
}
|