rsnext/packages/next/build/analysis/get-page-static-info.ts
JJ Kasper 097574d4c3
Optimize middleware regex handling (#37688)
* Optimize middleware regex handling

* fix data route matching for middleware
2022-06-14 11:50:05 -05:00

180 lines
5.2 KiB
TypeScript

import type { PageRuntime } from '../../server/config-shared'
import type { NextConfig } from '../../server/config-shared'
import { tryToExtractExportedConstValue } from './extract-const-value'
import { parseModule } from './parse-module'
import { promises as fs } from 'fs'
import { tryToParsePath } from '../../lib/try-to-parse-path'
import * as Log from '../output/log'
interface MiddlewareConfig {
pathMatcher: RegExp
}
export interface PageStaticInfo {
runtime?: PageRuntime
ssg?: boolean
ssr?: boolean
middleware?: Partial<MiddlewareConfig>
}
/**
* For a given pageFilePath and nextConfig, if the config supports it, this
* function will read the file and return the runtime that should be used.
* It will look into the file content only if the page *requires* a runtime
* to be specified, that is, when gSSP or gSP is used.
* Related discussion: https://github.com/vercel/next.js/discussions/34179
*/
export async function getPageStaticInfo(params: {
nextConfig: Partial<NextConfig>
pageFilePath: string
isDev?: boolean
page?: string
}): Promise<PageStaticInfo> {
const { isDev, pageFilePath, nextConfig } = params
const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || ''
if (/runtime|getStaticProps|getServerSideProps|matcher/.test(fileContent)) {
const swcAST = await parseModule(pageFilePath, fileContent)
const { ssg, ssr } = checkExports(swcAST)
const config = tryToExtractExportedConstValue(swcAST, 'config') || {}
let runtime = ['experimental-edge', 'edge'].includes(config?.runtime)
? 'edge'
: ssr || ssg
? config?.runtime || nextConfig.experimental?.runtime
: undefined
if (runtime === 'experimental-edge' || runtime === 'edge') {
warnAboutExperimentalEdgeApiFunctions()
runtime = 'edge'
}
const middlewareConfig = getMiddlewareConfig(config)
return {
ssr,
ssg,
...(middlewareConfig && { middleware: middlewareConfig }),
...(runtime && { runtime }),
}
}
return { ssr: false, ssg: false }
}
/**
* Receives a parsed AST from SWC and checks if it belongs to a module that
* requires a runtime to be specified. Those are:
* - Modules with `export function getStaticProps | getServerSideProps`
* - Modules with `export { getStaticProps | getServerSideProps } <from ...>`
*/
function checkExports(swcAST: any) {
if (Array.isArray(swcAST?.body)) {
try {
for (const node of swcAST.body) {
if (
node.type === 'ExportDeclaration' &&
node.declaration?.type === 'FunctionDeclaration' &&
['getStaticProps', 'getServerSideProps'].includes(
node.declaration.identifier?.value
)
) {
return {
ssg: node.declaration.identifier.value === 'getStaticProps',
ssr: node.declaration.identifier.value === 'getServerSideProps',
}
}
if (node.type === 'ExportNamedDeclaration') {
const values = node.specifiers.map(
(specifier: any) =>
specifier.type === 'ExportSpecifier' &&
specifier.orig?.type === 'Identifier' &&
specifier.orig?.value
)
return {
ssg: values.some((value: any) =>
['getStaticProps'].includes(value)
),
ssr: values.some((value: any) =>
['getServerSideProps'].includes(value)
),
}
}
}
} catch (err) {}
}
return { ssg: false, ssr: false }
}
async function tryToReadFile(filePath: string, shouldThrow: boolean) {
try {
return await fs.readFile(filePath, {
encoding: 'utf8',
})
} catch (error) {
if (shouldThrow) {
throw error
}
}
}
function getMiddlewareConfig(config: any): Partial<MiddlewareConfig> {
const result: Partial<MiddlewareConfig> = {}
if (config.matcher) {
result.pathMatcher = new RegExp(
getMiddlewareRegExpStrings(config.matcher).join('|')
)
if (result.pathMatcher.source.length > 4096) {
throw new Error(
`generated matcher config must be less than 4096 characters.`
)
}
}
return result
}
function getMiddlewareRegExpStrings(matcherOrMatchers: unknown): string[] {
if (Array.isArray(matcherOrMatchers)) {
return matcherOrMatchers.flatMap((x) => getMiddlewareRegExpStrings(x))
}
if (typeof matcherOrMatchers !== 'string') {
throw new Error(
'`matcher` must be a path matcher or an array of path matchers'
)
}
let matcher: string = matcherOrMatchers
if (!matcher.startsWith('/')) {
throw new Error('`matcher`: path matcher must start with /')
}
const parsedPage = tryToParsePath(matcher)
if (parsedPage.error) {
throw new Error(`Invalid path matcher: ${matcher}`)
}
const regexes = [parsedPage.regexStr].filter((x): x is string => !!x)
if (regexes.length < 1) {
throw new Error("Can't parse matcher")
} else {
return regexes
}
}
function warnAboutExperimentalEdgeApiFunctions() {
if (warnedAboutExperimentalEdgeApiFunctions) {
return
}
Log.warn(`You are using an experimental edge runtime, the API might change.`)
warnedAboutExperimentalEdgeApiFunctions = true
}
let warnedAboutExperimentalEdgeApiFunctions = false