UNPKG

next

Version:

The React Framework

484 lines (483 loc) • 25.6 kB
import { BUILD_ID_FILE, BUILD_MANIFEST, CLIENT_REFERENCE_MANIFEST, DYNAMIC_CSS_MANIFEST, NEXT_FONT_MANIFEST, PRERENDER_MANIFEST, REACT_LOADABLE_MANIFEST, ROUTES_MANIFEST, SERVER_FILES_MANIFEST, SERVER_REFERENCE_MANIFEST, SUBRESOURCE_INTEGRITY_MANIFEST } from '../../shared/lib/constants'; import { parseReqUrl } from '../../lib/url'; import { normalizeLocalePath } from '../../shared/lib/i18n/normalize-locale-path'; import { isDynamicRoute } from '../../shared/lib/router/utils'; import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'; import { getServerUtils } from '../server-utils'; import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale'; import { getHostname } from '../../shared/lib/get-hostname'; import { checkIsOnDemandRevalidate } from '../api-utils'; import { normalizeDataPath } from '../../shared/lib/page-path/normalize-data-path'; import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'; import { addRequestMeta, getRequestMeta } from '../request-meta'; import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path'; import { isStaticMetadataRoute } from '../../lib/metadata/is-metadata-route'; import { IncrementalCache } from '../lib/incremental-cache'; import { initializeCacheHandlers, setCacheHandler } from '../use-cache/handlers'; import { interopDefault } from '../app-render/interop-default'; import ResponseCache from '../response-cache'; import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'; import { RouterServerContextSymbol, routerServerGlobal } from '../lib/router-utils/router-server-context'; import { decodePathParams } from '../lib/router-utils/decode-path-params'; import { removeTrailingSlash } from '../../shared/lib/router/utils/remove-trailing-slash'; const dynamicImportEsmDefault = (id)=>import(/* webpackIgnore: true */ /* turbopackIgnore: true */ id).then((mod)=>mod.default || mod); /** * RouteModule is the base class for all route modules. This class should be * extended by all route modules. */ export class RouteModule { constructor({ userland, definition, distDir, projectDir }){ this.userland = userland; this.definition = definition; this.isDev = process.env.NODE_ENV === 'development'; this.distDir = distDir; this.projectDir = projectDir; } async instrumentationOnRequestError(req, ...args) { if (process.env.NEXT_RUNTIME === 'edge') { const { getEdgeInstrumentationModule } = await import('../web/globals'); const instrumentation = await getEdgeInstrumentationModule(); if (instrumentation) { await (instrumentation.onRequestError == null ? void 0 : instrumentation.onRequestError.call(instrumentation, ...args)); } } else { const { join } = require('node:path'); const absoluteProjectDir = getRequestMeta(req, 'projectDir') || join(process.cwd(), this.projectDir); const { instrumentationOnRequestError } = await import('../lib/router-utils/instrumentation-globals.external'); return instrumentationOnRequestError(absoluteProjectDir, this.distDir, ...args); } } loadManifests(srcPage, projectDir) { if (process.env.NEXT_RUNTIME === 'edge') { var _self___RSC_MANIFEST; const { getEdgePreviewProps } = require('../web/get-edge-preview-props'); const maybeJSONParse = (str)=>str ? JSON.parse(str) : undefined; return { buildId: process.env.__NEXT_BUILD_ID || '', buildManifest: self.__BUILD_MANIFEST, reactLoadableManifest: maybeJSONParse(self.__REACT_LOADABLE_MANIFEST), nextFontManifest: maybeJSONParse(self.__NEXT_FONT_MANIFEST), prerenderManifest: { routes: {}, dynamicRoutes: {}, notFoundRoutes: [], version: 4, preview: getEdgePreviewProps() }, routesManifest: { version: 4, caseSensitive: Boolean(process.env.__NEXT_CASE_SENSITIVE_ROUTES), basePath: process.env.__NEXT_BASE_PATH || '', rewrites: process.env.__NEXT_REWRITES || { beforeFiles: [], afterFiles: [], fallback: [] }, redirects: [], headers: [], i18n: process.env.__NEXT_I18N_CONFIG || undefined, skipMiddlewareUrlNormalize: Boolean(process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) }, serverFilesManifest: { config: globalThis.nextConfig || {} }, clientReferenceManifest: (_self___RSC_MANIFEST = self.__RSC_MANIFEST) == null ? void 0 : _self___RSC_MANIFEST[srcPage], serverActionsManifest: maybeJSONParse(self.__RSC_SERVER_MANIFEST), subresourceIntegrityManifest: maybeJSONParse(self.__SUBRESOURCE_INTEGRITY_MANIFEST), dynamicCssManifest: maybeJSONParse(self.__DYNAMIC_CSS_MANIFEST) }; } else { var _clientReferenceManifest___RSC_MANIFEST; if (!projectDir) { throw Object.defineProperty(new Error('Invariant: projectDir is required for node runtime'), "__NEXT_ERROR_CODE", { value: "E718", enumerable: false, configurable: true }); } const { loadManifestFromRelativePath } = require('../load-manifest.external'); const normalizedPagePath = normalizePagePath(srcPage); const [routesManifest, prerenderManifest, buildManifest, reactLoadableManifest, nextFontManifest, clientReferenceManifest, serverActionsManifest, subresourceIntegrityManifest, serverFilesManifest, buildId, dynamicCssManifest] = [ loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: ROUTES_MANIFEST, shouldCache: !this.isDev }), loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: PRERENDER_MANIFEST, shouldCache: !this.isDev }), loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: BUILD_MANIFEST, shouldCache: !this.isDev }), loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: process.env.TURBOPACK ? `server/${this.isAppRouter ? 'app' : 'pages'}${normalizedPagePath}/${REACT_LOADABLE_MANIFEST}` : REACT_LOADABLE_MANIFEST, handleMissing: true, shouldCache: !this.isDev }), loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: `server/${NEXT_FONT_MANIFEST}.json`, shouldCache: !this.isDev }), this.isAppRouter && !isStaticMetadataRoute(srcPage) ? loadManifestFromRelativePath({ distDir: this.distDir, projectDir, useEval: true, handleMissing: true, manifest: `server/app${srcPage.replace(/%5F/g, '_') + '_' + CLIENT_REFERENCE_MANIFEST}.js`, shouldCache: !this.isDev }) : undefined, this.isAppRouter ? loadManifestFromRelativePath({ distDir: this.distDir, projectDir, manifest: `server/${SERVER_REFERENCE_MANIFEST}.json`, handleMissing: true, shouldCache: !this.isDev }) : {}, loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: `server/${SUBRESOURCE_INTEGRITY_MANIFEST}.json`, handleMissing: true, shouldCache: !this.isDev }), this.isDev ? {} : loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: SERVER_FILES_MANIFEST }), this.isDev ? 'development' : loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: BUILD_ID_FILE, skipParse: true }), loadManifestFromRelativePath({ projectDir, distDir: this.distDir, manifest: DYNAMIC_CSS_MANIFEST, handleMissing: true }) ]; return { buildId, buildManifest, routesManifest, nextFontManifest, prerenderManifest, serverFilesManifest, reactLoadableManifest, clientReferenceManifest: clientReferenceManifest == null ? void 0 : (_clientReferenceManifest___RSC_MANIFEST = clientReferenceManifest.__RSC_MANIFEST) == null ? void 0 : _clientReferenceManifest___RSC_MANIFEST[srcPage.replace(/%5F/g, '_')], serverActionsManifest, subresourceIntegrityManifest, dynamicCssManifest }; } } async loadCustomCacheHandlers(req, nextConfig) { if (process.env.NEXT_RUNTIME !== 'edge') { const { cacheHandlers } = nextConfig.experimental; if (!cacheHandlers) return; // If we've already initialized the cache handlers interface, don't do it // again. if (!initializeCacheHandlers()) return; for (const [kind, handler] of Object.entries(cacheHandlers)){ if (!handler) continue; const { formatDynamicImportPath } = require('../../lib/format-dynamic-import-path'); const { join } = require('node:path'); const absoluteProjectDir = getRequestMeta(req, 'projectDir') || join(process.cwd(), this.projectDir); setCacheHandler(kind, interopDefault(await dynamicImportEsmDefault(formatDynamicImportPath(`${absoluteProjectDir}/${this.distDir}`, handler)))); } } } async getIncrementalCache(req, nextConfig, prerenderManifest) { if (process.env.NEXT_RUNTIME === 'edge') { return globalThis.__incrementalCache; } else { let CacheHandler; const { cacheHandler } = nextConfig; if (cacheHandler) { const { formatDynamicImportPath } = require('../../lib/format-dynamic-import-path'); CacheHandler = interopDefault(await dynamicImportEsmDefault(formatDynamicImportPath(this.distDir, cacheHandler))); } const { join } = require('node:path'); const projectDir = getRequestMeta(req, 'projectDir') || join(process.cwd(), this.projectDir); await this.loadCustomCacheHandlers(req, nextConfig); // incremental-cache is request specific // although can have shared caches in module scope // per-cache handler return new IncrementalCache({ fs: require('../lib/node-fs-methods').nodeFs, dev: this.isDev, requestHeaders: req.headers, allowedRevalidateHeaderKeys: nextConfig.experimental.allowedRevalidateHeaderKeys, minimalMode: getRequestMeta(req, 'minimalMode'), serverDistDir: `${projectDir}/${this.distDir}/server`, fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix, maxMemoryCacheSize: nextConfig.cacheMaxMemorySize, flushToDisk: nextConfig.experimental.isrFlushToDisk, getPrerenderManifest: ()=>prerenderManifest, CurCacheHandler: CacheHandler }); } } async onRequestError(req, err, errorContext, routerServerContext) { if (routerServerContext == null ? void 0 : routerServerContext.logErrorWithOriginalStack) { routerServerContext.logErrorWithOriginalStack(err, 'app-dir'); } else { console.error(err); } await this.instrumentationOnRequestError(req, err, { path: req.url || '/', headers: req.headers, method: req.method || 'GET' }, errorContext); } async prepare(req, res, { srcPage, multiZoneDraftMode }) { var _routerServerGlobal_RouterServerContextSymbol; let projectDir; // edge runtime handles loading instrumentation at the edge adapter level if (process.env.NEXT_RUNTIME !== 'edge') { const { join, relative } = require('node:path'); projectDir = getRequestMeta(req, 'projectDir') || join(process.cwd(), this.projectDir); const absoluteDistDir = getRequestMeta(req, 'distDir'); if (absoluteDistDir) { this.distDir = relative(projectDir, absoluteDistDir); } const { ensureInstrumentationRegistered } = await import('../lib/router-utils/instrumentation-globals.external'); // ensure instrumentation is registered and pass // onRequestError below ensureInstrumentationRegistered(projectDir, this.distDir); } const manifests = await this.loadManifests(srcPage, projectDir); const { routesManifest, prerenderManifest, serverFilesManifest } = manifests; const { basePath, i18n, rewrites } = routesManifest; if (basePath) { req.url = removePathPrefix(req.url || '/', basePath); } const parsedUrl = parseReqUrl(req.url || '/'); // if we couldn't parse the URL we can't continue if (!parsedUrl) { return; } let isNextDataRequest = false; if (pathHasPrefix(parsedUrl.pathname || '/', '/_next/data')) { isNextDataRequest = true; parsedUrl.pathname = normalizeDataPath(parsedUrl.pathname || '/'); } let originalPathname = parsedUrl.pathname || '/'; const originalQuery = { ...parsedUrl.query }; const pageIsDynamic = isDynamicRoute(srcPage); let localeResult; let detectedLocale; if (i18n) { localeResult = normalizeLocalePath(parsedUrl.pathname || '/', i18n.locales); if (localeResult.detectedLocale) { req.url = `${localeResult.pathname}${parsedUrl.search}`; originalPathname = localeResult.pathname; if (!detectedLocale) { detectedLocale = localeResult.detectedLocale; } } } const serverUtils = getServerUtils({ page: srcPage, i18n, basePath, rewrites, pageIsDynamic, trailingSlash: process.env.__NEXT_TRAILING_SLASH, caseSensitive: Boolean(routesManifest.caseSensitive) }); const domainLocale = detectDomainLocale(i18n == null ? void 0 : i18n.domains, getHostname(parsedUrl, req.headers), detectedLocale); addRequestMeta(req, 'isLocaleDomain', Boolean(domainLocale)); const defaultLocale = (domainLocale == null ? void 0 : domainLocale.defaultLocale) || (i18n == null ? void 0 : i18n.defaultLocale); // Ensure parsedUrl.pathname includes locale before processing // rewrites or they won't match correctly. if (defaultLocale && !detectedLocale) { parsedUrl.pathname = `/${defaultLocale}${parsedUrl.pathname === '/' ? '' : parsedUrl.pathname}`; } const locale = getRequestMeta(req, 'locale') || detectedLocale || defaultLocale; const rewriteParamKeys = Object.keys(serverUtils.handleRewrites(req, parsedUrl)); // after processing rewrites we want to remove locale // from parsedUrl pathname if (i18n) { parsedUrl.pathname = normalizeLocalePath(parsedUrl.pathname || '/', i18n.locales).pathname; } let params = getRequestMeta(req, 'params'); // attempt parsing from pathname if (!params && serverUtils.dynamicRouteMatcher) { const paramsMatch = serverUtils.dynamicRouteMatcher(normalizeDataPath((localeResult == null ? void 0 : localeResult.pathname) || parsedUrl.pathname || '/')); const paramsResult = serverUtils.normalizeDynamicRouteParams(paramsMatch || {}, true); if (paramsResult.hasValidParams) { params = paramsResult.params; } } // Local "next start" expects the routing parsed query values // to not be present in the URL although when deployed proxies // will add query values from resolving the routes to pass to function. // TODO: do we want to change expectations for "next start" // to include these query values in the URL which affects asPath // but would match deployed behavior, e.g. a rewrite from middleware // that adds a query param would be in asPath as query but locally // it won't be in the asPath but still available in the query object const query = getRequestMeta(req, 'query') || { ...parsedUrl.query }; const routeParamKeys = new Set(); const combinedParamKeys = []; // we don't include rewriteParamKeys in the combinedParamKeys // for app router since the searchParams is populated from the // URL so we don't want to strip the rewrite params from the URL // so that searchParams can include them if (!this.isAppRouter) { for (const key of [ ...rewriteParamKeys, ...Object.keys(serverUtils.defaultRouteMatches || {}) ]){ // We only want to filter rewrite param keys from the URL // if they are matches from the URL e.g. the key/value matches // before and after applying the rewrites /:path for /hello and // { path: 'hello' } but not for { path: 'another' } and /hello // TODO: we should prefix rewrite param keys the same as we do // for dynamic routes so we can identify them properly const originalValue = Array.isArray(originalQuery[key]) ? originalQuery[key].join('') : originalQuery[key]; const queryValue = Array.isArray(query[key]) ? query[key].join('') : query[key]; if (!(key in originalQuery) || originalValue === queryValue) { combinedParamKeys.push(key); } } } serverUtils.normalizeCdnUrl(req, combinedParamKeys); serverUtils.normalizeQueryParams(query, routeParamKeys); serverUtils.filterInternalQuery(originalQuery, combinedParamKeys); if (pageIsDynamic) { const queryResult = serverUtils.normalizeDynamicRouteParams(query, true); const paramsResult = serverUtils.normalizeDynamicRouteParams(params || {}, true); const paramsToInterpolate = paramsResult.hasValidParams && params ? params : queryResult.hasValidParams ? query : {}; req.url = serverUtils.interpolateDynamicPath(req.url || '/', paramsToInterpolate); parsedUrl.pathname = serverUtils.interpolateDynamicPath(parsedUrl.pathname || '/', paramsToInterpolate); originalPathname = serverUtils.interpolateDynamicPath(originalPathname, paramsToInterpolate); // try pulling from query if valid if (!params) { if (queryResult.hasValidParams) { params = Object.assign({}, queryResult.params); // If we pulled from query remove it so it's // only in params for(const key in serverUtils.defaultRouteMatches){ delete query[key]; } } else { // use final params from URL matching const paramsMatch = serverUtils.dynamicRouteMatcher == null ? void 0 : serverUtils.dynamicRouteMatcher.call(serverUtils, normalizeDataPath((localeResult == null ? void 0 : localeResult.pathname) || parsedUrl.pathname || '/')); // we don't normalize these as they are allowed to be // the literal slug matches here e.g. /blog/[slug] // actually being requested if (paramsMatch) { params = Object.assign({}, paramsMatch); } } } } // Remove any normalized params from the query if they // weren't present as non-prefixed query key e.g. // ?search=1&nxtPsearch=hello we don't delete search for (const key of routeParamKeys){ if (!(key in originalQuery)) { delete query[key]; } } const { isOnDemandRevalidate, revalidateOnlyGenerated } = checkIsOnDemandRevalidate(req, prerenderManifest.preview); let isDraftMode = false; let previewData; // preview data relies on non-edge utils if (process.env.NEXT_RUNTIME !== 'edge' && res) { const { tryGetPreviewData } = require('../api-utils/node/try-get-preview-data'); previewData = tryGetPreviewData(req, res, prerenderManifest.preview, Boolean(multiZoneDraftMode)); isDraftMode = previewData !== false; } const routerServerContext = (_routerServerGlobal_RouterServerContextSymbol = routerServerGlobal[RouterServerContextSymbol]) == null ? void 0 : _routerServerGlobal_RouterServerContextSymbol[this.projectDir]; const nextConfig = (routerServerContext == null ? void 0 : routerServerContext.nextConfig) || serverFilesManifest.config; const normalizedSrcPage = normalizeAppPath(srcPage); let resolvedPathname = getRequestMeta(req, 'rewroteURL') || normalizedSrcPage; if (isDynamicRoute(resolvedPathname) && params) { resolvedPathname = serverUtils.interpolateDynamicPath(resolvedPathname, params); } if (resolvedPathname === '/index') { resolvedPathname = '/'; } try { resolvedPathname = decodePathParams(resolvedPathname); } catch (_) {} resolvedPathname = removeTrailingSlash(resolvedPathname); return { query, originalQuery, originalPathname, params, parsedUrl, locale, isNextDataRequest, locales: i18n == null ? void 0 : i18n.locales, defaultLocale, isDraftMode, previewData, pageIsDynamic, resolvedPathname, isOnDemandRevalidate, revalidateOnlyGenerated, ...manifests, serverActionsManifest: manifests.serverActionsManifest, clientReferenceManifest: manifests.clientReferenceManifest, nextConfig, routerServerContext }; } getResponseCache(req) { if (!this.responseCache) { const minimalMode = getRequestMeta(req, 'minimalMode') ?? false; this.responseCache = new ResponseCache(minimalMode); } return this.responseCache; } async handleResponse({ req, nextConfig, cacheKey, routeKind, isFallback, prerenderManifest, isRoutePPREnabled, isOnDemandRevalidate, revalidateOnlyGenerated, responseGenerator, waitUntil }) { const responseCache = this.getResponseCache(req); const cacheEntry = await responseCache.get(cacheKey, responseGenerator, { routeKind, isFallback, isRoutePPREnabled, isOnDemandRevalidate, isPrefetch: req.headers.purpose === 'prefetch', incrementalCache: await this.getIncrementalCache(req, nextConfig, prerenderManifest), waitUntil }); if (!cacheEntry) { if (cacheKey && // revalidate only generated can bail even if cacheKey is provided !(isOnDemandRevalidate && revalidateOnlyGenerated)) { // A cache entry might not be generated if a response is written // in `getInitialProps` or `getServerSideProps`, but those shouldn't // have a cache key. If we do have a cache key but we don't end up // with a cache entry, then either Next.js or the application has a // bug that needs fixing. throw Object.defineProperty(new Error('invariant: cache entry required but not generated'), "__NEXT_ERROR_CODE", { value: "E62", enumerable: false, configurable: true }); } } return cacheEntry; } } //# sourceMappingURL=route-module.js.map