UNPKG

next

Version:

The React Framework

569 lines (568 loc) • 28.7 kB
import { readFileSync } from 'fs'; import { relative } from 'path'; import { LRUCache } from '../../server/lib/lru-cache'; import { extractExportedConstValue } from './extract-const-value'; import { parseModule } from './parse-module'; import * as Log from '../output/log'; import { SERVER_RUNTIME, MIDDLEWARE_FILENAME, PROXY_FILENAME, RSC_SUFFIX, RSC_SEGMENT_SUFFIX, RSC_SEGMENTS_DIR_SUFFIX } from '../../lib/constants'; import { tryToParsePath } from '../../lib/try-to-parse-path'; import { isAPIRoute } from '../../lib/is-api-route'; import { isEdgeRuntime } from '../../lib/is-edge-runtime'; import { RSC_MODULE_TYPES } from '../../shared/lib/constants'; import { escapeStringRegexp } from '../../shared/lib/escape-regexp'; import { PAGE_TYPES } from '../../lib/page-types'; import { AppSegmentConfigSchemaKeys, parseAppSegmentConfig } from '../segment-config/app/app-segment-config'; import { reportZodError } from '../../shared/lib/zod'; import { PagesSegmentConfigSchemaKeys, parsePagesSegmentConfig } from '../segment-config/pages/pages-segment-config'; import { MiddlewareConfigInputSchema, SourceSchema } from '../segment-config/middleware/middleware-config'; import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'; import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path'; import { isProxyFile } from '../utils'; const PARSE_PATTERN = /(?<!(_jsx|jsx-))runtime|preferredRegion|getStaticProps|getServerSideProps|generateStaticParams|export const|generateImageMetadata|generateSitemaps|middleware|proxy/; const APP_ROUTE_RSC_SUFFIX_MATCHER = escapeStringRegexp(RSC_SUFFIX); const APP_ROUTE_SEGMENT_PREFETCH_SUFFIX_MATCHER = `${escapeStringRegexp(RSC_SEGMENTS_DIR_SUFFIX)}/.+${escapeStringRegexp(RSC_SEGMENT_SUFFIX)}`; const APP_ROUTE_TRANSPORT_SUFFIX_MATCHER = `${APP_ROUTE_RSC_SUFFIX_MATCHER}|${APP_ROUTE_SEGMENT_PREFETCH_SUFFIX_MATCHER}`; const ROOT_APP_ROUTE_TRANSPORT_MATCHER = `/?index(?:${APP_ROUTE_TRANSPORT_SUFFIX_MATCHER})`; const MIDDLEWARE_DATA_SUFFIX_MATCHER = `\\.json|${APP_ROUTE_TRANSPORT_SUFFIX_MATCHER}`; const OPTIONAL_MIDDLEWARE_NEXT_DATA_PREFIX = '/:nextData(_next/data/[^/]{1,})?'; const CLIENT_MODULE_LABEL = /\/\* __next_internal_client_entry_do_not_use__ ([^ ]*) (cjs|auto) \*\//; // Match JSON object that may contain nested objects (for loc info) // The JSON ends right before the closing " */" const ACTION_MODULE_LABEL = /\/\* __next_internal_action_entry_do_not_use__ (\{.*\}) \*\//; const CLIENT_DIRECTIVE = 'use client'; const SERVER_ACTION_DIRECTIVE = 'use server'; export function getRSCModuleInformation(source, isReactServerLayer) { const actionsJson = source.match(ACTION_MODULE_LABEL); // Parse action metadata - supports both old format (string) and new format (object with loc) const parsedActionsMeta = actionsJson ? JSON.parse(actionsJson[1]) : undefined; const clientInfoMatch = source.match(CLIENT_MODULE_LABEL); const isClientRef = !!clientInfoMatch; if (!isReactServerLayer) { return { type: RSC_MODULE_TYPES.client, actionIds: parsedActionsMeta, isClientRef }; } const clientRefsString = clientInfoMatch == null ? void 0 : clientInfoMatch[1]; const clientRefs = clientRefsString ? clientRefsString.split(',') : []; const clientEntryType = clientInfoMatch == null ? void 0 : clientInfoMatch[2]; const type = clientInfoMatch ? RSC_MODULE_TYPES.client : RSC_MODULE_TYPES.server; return { type, actionIds: parsedActionsMeta, clientRefs, clientEntryType, isClientRef }; } /** * 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 ...>` * - Modules with `export const runtime = ...` */ function checkExports(ast, expectedExports, page) { const exportsSet = new Set([ 'getStaticProps', 'getServerSideProps', 'generateImageMetadata', 'generateSitemaps', 'generateStaticParams' ]); if (!Array.isArray(ast == null ? void 0 : ast.body)) { return {}; } try { let getStaticProps = false; let getServerSideProps = false; let generateImageMetadata = false; let generateSitemaps = false; let generateStaticParams = false; let exports = new Set(); let directives = new Set(); let hasLeadingNonDirectiveNode = false; for (const node of ast.body){ var _node_declaration, _node_declaration1, _node_declaration_identifier, _node_declaration2; // There should be no non-string literals nodes before directives if (node.type === 'ExpressionStatement' && node.expression.type === 'StringLiteral') { if (!hasLeadingNonDirectiveNode) { const directive = node.expression.value; if (CLIENT_DIRECTIVE === directive) { directives.add('client'); } if (SERVER_ACTION_DIRECTIVE === directive) { directives.add('server'); } } } else { hasLeadingNonDirectiveNode = true; } if (node.type === 'ExportDeclaration' && ((_node_declaration = node.declaration) == null ? void 0 : _node_declaration.type) === 'VariableDeclaration') { var _node_declaration3; for (const declaration of (_node_declaration3 = node.declaration) == null ? void 0 : _node_declaration3.declarations){ if (expectedExports.includes(declaration.id.value)) { exports.add(declaration.id.value); } } } if (node.type === 'ExportDeclaration' && ((_node_declaration1 = node.declaration) == null ? void 0 : _node_declaration1.type) === 'FunctionDeclaration' && exportsSet.has((_node_declaration_identifier = node.declaration.identifier) == null ? void 0 : _node_declaration_identifier.value)) { const id = node.declaration.identifier.value; getServerSideProps = id === 'getServerSideProps'; getStaticProps = id === 'getStaticProps'; generateImageMetadata = id === 'generateImageMetadata'; generateSitemaps = id === 'generateSitemaps'; generateStaticParams = id === 'generateStaticParams'; } if (node.type === 'ExportDeclaration' && ((_node_declaration2 = node.declaration) == null ? void 0 : _node_declaration2.type) === 'VariableDeclaration') { var _node_declaration_declarations_, _node_declaration4; const id = (_node_declaration4 = node.declaration) == null ? void 0 : (_node_declaration_declarations_ = _node_declaration4.declarations[0]) == null ? void 0 : _node_declaration_declarations_.id.value; if (exportsSet.has(id)) { getServerSideProps = id === 'getServerSideProps'; getStaticProps = id === 'getStaticProps'; generateImageMetadata = id === 'generateImageMetadata'; generateSitemaps = id === 'generateSitemaps'; generateStaticParams = id === 'generateStaticParams'; } } if (node.type === 'ExportNamedDeclaration') { for (const specifier of node.specifiers){ var _specifier_orig; if (specifier.type === 'ExportSpecifier' && ((_specifier_orig = specifier.orig) == null ? void 0 : _specifier_orig.type) === 'Identifier') { const value = specifier.orig.value; if (!getServerSideProps && value === 'getServerSideProps') { getServerSideProps = true; } if (!getStaticProps && value === 'getStaticProps') { getStaticProps = true; } if (!generateImageMetadata && value === 'generateImageMetadata') { generateImageMetadata = true; } if (!generateSitemaps && value === 'generateSitemaps') { generateSitemaps = true; } if (!generateStaticParams && value === 'generateStaticParams') { generateStaticParams = true; } if (expectedExports.includes(value) && !exports.has(value)) { // An export was found that was actually a re-export, and not a // literal value. We should warn here. Log.warn(`Next.js can't recognize the exported \`${value}\` field in "${page}", it may be re-exported from another file. The default config will be used instead.`); } } } } } return { getStaticProps, getServerSideProps, generateImageMetadata, generateSitemaps, generateStaticParams, directives, exports }; } catch {} return {}; } function validateMiddlewareProxyExports({ ast, page, pageFilePath, isDev }) { // Check if this is middleware/proxy const isMiddleware = page === `/${MIDDLEWARE_FILENAME}` || page === `/src/${MIDDLEWARE_FILENAME}`; const isProxy = page === `/${PROXY_FILENAME}` || page === `/src/${PROXY_FILENAME}`; if (!isMiddleware && !isProxy) { return; } if (!ast || !Array.isArray(ast.body)) { return; } const fileName = isProxy ? PROXY_FILENAME : MIDDLEWARE_FILENAME; // Parse AST to get export info (since checkExports doesn't return middleware/proxy info) let hasDefaultExport = false; let hasMiddlewareExport = false; let hasProxyExport = false; for (const node of ast.body){ var _node_declaration, _node_declaration1; if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportDefaultExpression') { hasDefaultExport = true; } if (node.type === 'ExportDeclaration' && ((_node_declaration = node.declaration) == null ? void 0 : _node_declaration.type) === 'FunctionDeclaration') { var _node_declaration_identifier; const id = (_node_declaration_identifier = node.declaration.identifier) == null ? void 0 : _node_declaration_identifier.value; if (id === 'middleware') { hasMiddlewareExport = true; } if (id === 'proxy') { hasProxyExport = true; } } if (node.type === 'ExportDeclaration' && ((_node_declaration1 = node.declaration) == null ? void 0 : _node_declaration1.type) === 'VariableDeclaration') { var _node_declaration_declarations_, _node_declaration2; const id = (_node_declaration2 = node.declaration) == null ? void 0 : (_node_declaration_declarations_ = _node_declaration2.declarations[0]) == null ? void 0 : _node_declaration_declarations_.id.value; if (id === 'middleware') { hasMiddlewareExport = true; } if (id === 'proxy') { hasProxyExport = true; } } if (node.type === 'ExportNamedDeclaration') { for (const specifier of node.specifiers){ var _specifier_orig; if (specifier.type === 'ExportSpecifier' && ((_specifier_orig = specifier.orig) == null ? void 0 : _specifier_orig.type) === 'Identifier') { // Use the exported name if it exists (for aliased exports like `export { foo as proxy }`), // otherwise fall back to the original name (for simple re-exports like `export { proxy }`) const exportedIdentifier = specifier.exported || specifier.orig; const value = exportedIdentifier.value; if (value === 'middleware') { hasMiddlewareExport = true; } if (value === 'proxy') { hasProxyExport = true; } } } } } const hasValidExport = hasDefaultExport || isMiddleware && hasMiddlewareExport || isProxy && hasProxyExport; const relativePath = relative(process.cwd(), pageFilePath); const resolvedPath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`; if (!hasValidExport) { const message = `The file "${resolvedPath}" must export a function, either as a default export or as a named "${fileName}" export.\n` + `This function is what Next.js runs for every request handled by this ${fileName === 'proxy' ? 'proxy (previously called middleware)' : 'middleware'}.\n\n` + `Why this happens:\n` + (isProxy ? "- You are migrating from `middleware` to `proxy`, but haven't updated the exported function.\n" : '') + `- The file exists but doesn't export a function.\n` + `- The export is not a function (e.g., an object or constant).\n` + `- There's a syntax error preventing the export from being recognized.\n\n` + `To fix it:\n` + `- Ensure this file has either a default or "${fileName}" function export.\n\n` + `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy`; if (isDev) { // errorOnce as proxy/middleware runs per request including multiple // internal _next/ routes and spams logs. Log.errorOnce(message); } else { throw Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", { value: "E903", enumerable: false, configurable: true }); } } } function tryToReadFile(filePath, shouldThrow) { try { return readFileSync(filePath, { encoding: 'utf8' }); } catch (error) { if (shouldThrow) { error.message = `Next.js ERROR: Failed to read file ${filePath}:\n${error.message}`; throw error; } } } /** * @internal - required to exclude zod types from the build */ export function getMiddlewareMatchers(matcherOrMatchers, nextConfig) { const matchers = Array.isArray(matcherOrMatchers) ? matcherOrMatchers : [ matcherOrMatchers ]; const { i18n } = nextConfig; return matchers.map((matcher)=>{ matcher = typeof matcher === 'string' ? { source: matcher } : matcher; const originalSource = matcher.source; let { source, ...rest } = matcher; const isRoot = source === '/'; if ((i18n == null ? void 0 : i18n.locales) && matcher.locale !== false) { source = `/:nextInternalLocale((?!_next/)[^/.]{1,})${isRoot ? '' : source}`; } // Match transport-specific route forms that resolve to the same page. // - Pages Router data routes: /_next/data/<build-id>/... // - App Router transport routes: .rsc, ...segments/...segment.rsc const sourceSuffix = `${isRoot ? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json|${ROOT_APP_ROUTE_TRANSPORT_MATCHER})?` : `{(${MIDDLEWARE_DATA_SUFFIX_MATCHER})}?`}`; source = `${OPTIONAL_MIDDLEWARE_NEXT_DATA_PREFIX}${source}${sourceSuffix}`; if (nextConfig.basePath) { source = `${nextConfig.basePath}${source}`; } // Validate that the source is still. const result = SourceSchema.safeParse(source); if (!result.success) { reportZodError('Failed to parse middleware source', result.error); // We need to exit here because middleware being built occurs before we // finish setting up the server. Exiting here is the only way to ensure // that we don't hang. process.exit(1); } return { ...rest, // We know that parsed.regexStr is not undefined because we already // checked that the source is valid. regexp: tryToParsePath(result.data).regexStr, originalSource: originalSource || source }; }); } function parseMiddlewareConfig(page, rawConfig, nextConfig) { // If there's no config to parse, then return nothing. if (typeof rawConfig !== 'object' || !rawConfig) return {}; const input = MiddlewareConfigInputSchema.safeParse(rawConfig); if (!input.success) { reportZodError(`${page} contains invalid middleware config`, input.error); // We need to exit here because middleware being built occurs before we // finish setting up the server. Exiting here is the only way to ensure // that we don't hang. process.exit(1); } const config = {}; if (input.data.matcher) { config.matchers = getMiddlewareMatchers(input.data.matcher, nextConfig); } if (input.data.unstable_allowDynamic) { config.unstable_allowDynamic = Array.isArray(input.data.unstable_allowDynamic) ? input.data.unstable_allowDynamic : [ input.data.unstable_allowDynamic ]; } if (input.data.regions) { config.regions = input.data.regions; } return config; } const apiRouteWarnings = new LRUCache(250); function warnAboutExperimentalEdge(apiRoute) { if (process.env.NODE_ENV === 'production' && process.env.NEXT_PRIVATE_BUILD_WORKER === '1') { return; } if (apiRoute && apiRouteWarnings.has(apiRoute)) { return; } Log.warn(apiRoute ? `${apiRoute} provided runtime 'experimental-edge'. It can be updated to 'edge' instead.` : `You are using an experimental edge runtime, the API might change.`); if (apiRoute) { apiRouteWarnings.set(apiRoute, 1); } } let hadUnsupportedValue = false; const warnedUnsupportedValueMap = new LRUCache(250, ()=>1); function warnAboutUnsupportedValue(pageFilePath, page, result) { hadUnsupportedValue = true; const isProductionBuild = process.env.NODE_ENV === 'production'; if (// we only log for the server compilation so it's not // duplicated due to webpack build worker having fresh // module scope for each compiler process.env.NEXT_COMPILER_NAME !== 'server' || isProductionBuild && warnedUnsupportedValueMap.has(pageFilePath)) { return; } warnedUnsupportedValueMap.set(pageFilePath, true); const message = `Next.js can't recognize the exported \`config\` field in ` + (page ? `route "${page}"` : `"${pageFilePath}"`) + ':\n' + result.unsupported + (result.path ? ` at "${result.path}"` : '') + '.\n' + 'Read More - https://nextjs.org/docs/messages/invalid-page-config'; // for a build we use `Log.error` instead of throwing // so that all errors can be logged before exiting the process if (isProductionBuild) { Log.error(message); } else { throw Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", { value: "E1013", enumerable: false, configurable: true }); } } export async function getAppPageStaticInfo({ pageFilePath, nextConfig, isDev, page }) { const content = tryToReadFile(pageFilePath, !isDev); if (!content || !PARSE_PATTERN.test(content)) { return { type: PAGE_TYPES.APP, config: undefined, runtime: undefined, preferredRegion: undefined, maxDuration: undefined, hadUnsupportedValue: false }; } const ast = await parseModule(pageFilePath, content); validateMiddlewareProxyExports({ ast, page, pageFilePath, isDev }); const { generateStaticParams, generateImageMetadata, generateSitemaps, exports, directives } = checkExports(ast, AppSegmentConfigSchemaKeys, page); const { type: rsc } = getRSCModuleInformation(content, true); const exportedConfig = {}; if (exports) { for (const property of exports){ const result = extractExportedConstValue(ast, property); if (result !== null && 'unsupported' in result) { warnAboutUnsupportedValue(pageFilePath, page, result); } else if (result !== null) { exportedConfig[property] = result.value; } } } const configResult = extractExportedConstValue(ast, 'config'); if (configResult !== null && 'unsupported' in configResult) { warnAboutUnsupportedValue(pageFilePath, page, configResult); } else if (configResult !== null) { exportedConfig.config = configResult.value; } const route = normalizeAppPath(page); const config = parseAppSegmentConfig(exportedConfig, route); // Prevent edge runtime and generateStaticParams in the same file. if (isEdgeRuntime(config.runtime) && generateStaticParams) { throw Object.defineProperty(new Error(`Page "${page}" cannot use both \`export const runtime = 'edge'\` and export \`generateStaticParams\`.`), "__NEXT_ERROR_CODE", { value: "E42", enumerable: false, configurable: true }); } // Prevent use client and generateStaticParams in the same file. if ((directives == null ? void 0 : directives.has('client')) && generateStaticParams) { throw Object.defineProperty(new Error(`Page "${page}" cannot use both "use client" and export function "generateStaticParams()".`), "__NEXT_ERROR_CODE", { value: "E475", enumerable: false, configurable: true }); } // Prevent use client and unstable_instant in the same file. if ((directives == null ? void 0 : directives.has('client')) && 'unstable_instant' in config) { throw Object.defineProperty(new Error(`Page "${page}" cannot export "unstable_instant" from a Client Component module. To use this API, convert this module to a Server Component by removing the "use client" directive.`), "__NEXT_ERROR_CODE", { value: "E1075", enumerable: false, configurable: true }); } if ('unstable_instant' in config && !nextConfig.cacheComponents) { throw Object.defineProperty(new Error(`Page "${page}" cannot use \`export const unstable_instant = ...\` without enabling \`cacheComponents\`.`), "__NEXT_ERROR_CODE", { value: "E990", enumerable: false, configurable: true }); } // Prevent unstable_dynamicStaleTime in layouts. if ('unstable_dynamicStaleTime' in config) { const isLayout = /\/layout\.[^/]+$/.test(pageFilePath); if (isLayout) { throw Object.defineProperty(new Error(`"${page}" cannot use \`export const unstable_dynamicStaleTime\`. This config is only supported in page files, not layouts.`), "__NEXT_ERROR_CODE", { value: "E1137", enumerable: false, configurable: true }); } } // Prevent combining unstable_dynamicStaleTime and unstable_instant. if ('unstable_dynamicStaleTime' in config && 'unstable_instant' in config) { throw Object.defineProperty(new Error(`Page "${page}" cannot use both \`export const unstable_dynamicStaleTime\` and \`export const unstable_instant\`.`), "__NEXT_ERROR_CODE", { value: "E1136", enumerable: false, configurable: true }); } return { type: PAGE_TYPES.APP, rsc, generateImageMetadata, generateSitemaps, generateStaticParams, config, middleware: parseMiddlewareConfig(page, exportedConfig.config, nextConfig), runtime: config.runtime, preferredRegion: config.preferredRegion, maxDuration: config.maxDuration, hadUnsupportedValue }; } export async function getPagesPageStaticInfo({ pageFilePath, nextConfig, isDev, page }) { var _config_config, _config_config1, _config_config2; const content = tryToReadFile(pageFilePath, !isDev); if (!content || !PARSE_PATTERN.test(content)) { return { type: PAGE_TYPES.PAGES, config: undefined, runtime: undefined, preferredRegion: undefined, maxDuration: undefined, hadUnsupportedValue: false }; } const ast = await parseModule(pageFilePath, content); validateMiddlewareProxyExports({ ast, page, pageFilePath, isDev }); const { getServerSideProps, getStaticProps, exports } = checkExports(ast, PagesSegmentConfigSchemaKeys, page); const { type: rsc } = getRSCModuleInformation(content, true); const exportedConfig = {}; if (exports) { for (const property of exports){ const result = extractExportedConstValue(ast, property); if (result !== null && 'unsupported' in result) { warnAboutUnsupportedValue(pageFilePath, page, result); } else if (result !== null) { exportedConfig[property] = result.value; } } } const configResult = extractExportedConstValue(ast, 'config'); if (configResult !== null && 'unsupported' in configResult) { warnAboutUnsupportedValue(pageFilePath, page, configResult); } else if (configResult !== null) { exportedConfig.config = configResult.value; } // Validate the config. const route = normalizePagePath(page); const config = parsePagesSegmentConfig(exportedConfig, route); const isAnAPIRoute = isAPIRoute(route); let resolvedRuntime = config.runtime ?? ((_config_config = config.config) == null ? void 0 : _config_config.runtime); if (isProxyFile(page) && resolvedRuntime) { const relativePath = relative(process.cwd(), pageFilePath); const resolvedPath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`; const message = `Route segment config is not allowed in Proxy file at "${resolvedPath}". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy`; if (isDev) { // errorOnce as proxy/middleware runs per request including multiple // internal _next/ routes and spams logs. Log.errorOnce(message); resolvedRuntime = SERVER_RUNTIME.nodejs; } else { throw Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", { value: "E1031", enumerable: false, configurable: true }); } } if (resolvedRuntime === SERVER_RUNTIME.experimentalEdge) { warnAboutExperimentalEdge(isAnAPIRoute ? page : null); } if (!isProxyFile(page) && resolvedRuntime === SERVER_RUNTIME.edge && page && !isAnAPIRoute) { const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.`; if (isDev) { Log.error(message); } else { throw Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", { value: "E1015", enumerable: false, configurable: true }); } } return { type: PAGE_TYPES.PAGES, getStaticProps, getServerSideProps, rsc, config, middleware: parseMiddlewareConfig(page, exportedConfig.config, nextConfig), runtime: resolvedRuntime, preferredRegion: (_config_config1 = config.config) == null ? void 0 : _config_config1.regions, maxDuration: config.maxDuration ?? ((_config_config2 = config.config) == null ? void 0 : _config_config2.maxDuration), hadUnsupportedValue }; } /** * 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) { if (params.pageType === PAGE_TYPES.APP) { return getAppPageStaticInfo(params); } return getPagesPageStaticInfo(params); } //# sourceMappingURL=get-page-static-info.js.map