UNPKG

next

Version:

The React Framework

593 lines (592 loc) • 29 kB
import { dirname, posix, join, normalize, relative } from 'path'; import { stringify } from 'querystring'; import { PAGES_DIR_ALIAS, ROOT_DIR_ALIAS, APP_DIR_ALIAS, WEBPACK_LAYERS, INSTRUMENTATION_HOOK_FILENAME } from '../lib/constants'; import { isAPIRoute } from '../lib/is-api-route'; import { isEdgeRuntime } from '../lib/is-edge-runtime'; import { APP_CLIENT_INTERNALS, RSC_MODULE_TYPES } from '../shared/lib/constants'; import { CLIENT_STATIC_FILES_RUNTIME_MAIN, CLIENT_STATIC_FILES_RUNTIME_MAIN_APP, CLIENT_STATIC_FILES_RUNTIME_POLYFILLS, CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH, COMPILER_NAMES, EDGE_RUNTIME_WEBPACK } from '../shared/lib/constants'; import { isMiddlewareFile, isMiddlewareFilename, isProxyFile, isInstrumentationHookFile, isInstrumentationHookFilename } from './utils'; import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'; import { normalizeAppPath, compareAppPaths, selectAppPageEntry } from '../shared/lib/router/utils/app-paths'; import { encodeMatchers } from './webpack/loaders/next-middleware-loader'; import { isAppRouteRoute } from '../lib/is-app-route-route'; import { isAppPageRoute } from '../lib/is-app-page-route'; import { getRouteLoaderEntry } from './webpack/loaders/next-route-loader'; import { isInternalComponent, isNonRoutePagesPage } from '../lib/is-internal-component'; import { RouteKind } from '../server/route-kind'; import { encodeToBase64 } from './webpack/loaders/utils'; import { normalizeCatchAllRoutes } from './normalize-catchall-routes'; import { PAGE_TYPES } from '../lib/page-types'; import { UnmatchedAppPagesError } from '../shared/lib/errors/unmatched-app-pages-error'; import { MissingCanonicalInterceptionRoutesError } from '../shared/lib/errors/missing-canonical-interception-routes-error'; import { IncompatibleParallelRouteSlotsError } from '../shared/lib/errors/incompatible-parallel-route-slots-error'; import { findMissingCanonicalInterceptionRoutes } from '../shared/lib/router/utils/interception-routes'; import { findPageFile } from '../server/lib/find-page-file'; import * as Log from './output/log'; import { getStrictRouteMatchingDefaultWarning } from '../server/lib/router-utils/strict-route-matching-config'; import { getStaticInfoIncludingLayouts } from './get-static-info-including-layouts'; import { getPageFromPath } from './route-discovery'; export function getPageFilePath({ absolutePagePath, pagesDir, appDir, rootDir }) { if (absolutePagePath.startsWith(PAGES_DIR_ALIAS) && pagesDir) { return absolutePagePath.replace(PAGES_DIR_ALIAS, pagesDir); } if (absolutePagePath.startsWith(APP_DIR_ALIAS) && appDir) { return absolutePagePath.replace(APP_DIR_ALIAS, appDir); } if (absolutePagePath.startsWith(ROOT_DIR_ALIAS)) { return absolutePagePath.replace(ROOT_DIR_ALIAS, rootDir); } return require.resolve(absolutePagePath); } /** * Checks if a page path matches any of the deferred entry patterns. * @param page - The page path (e.g., '/about', '/api/hello') * @param deferredEntries - Array of path patterns to match against * @returns true if the page matches a deferred entry pattern */ export function isDeferredEntry(page, deferredEntries) { if (!deferredEntries || deferredEntries.length === 0) { return false; } // Normalize the page path const normalizedPage = page.startsWith('/') ? page : `/${page}`; for (const pattern of deferredEntries){ // Normalize the pattern const normalizedPattern = pattern.startsWith('/') ? pattern : `/${pattern}`; // Check for exact match or prefix match for directories if (normalizedPage === normalizedPattern) { return true; } // Check if the page is under the deferred directory if (normalizedPage.startsWith(normalizedPattern + '/')) { return true; } } return false; } export function getEdgeServerEntry(opts) { var _opts_config_experimental_sri; const cacheHandler = opts.config.cacheHandler || undefined; if (opts.pagesType === 'app' && isAppRouteRoute(opts.page) && opts.appDirLoader) { const loaderParams = { absolutePagePath: opts.absolutePagePath, page: opts.page, appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'), preferredRegion: opts.preferredRegion, middlewareConfig: Buffer.from(JSON.stringify(opts.middlewareConfig || {})).toString('base64'), cacheHandlers: JSON.stringify(opts.config.cacheHandlers || {}), ...cacheHandler ? { cacheHandler } : {} }; return { import: `next-edge-app-route-loader?${stringify(loaderParams)}!`, layer: WEBPACK_LAYERS.reactServerComponents }; } if (isMiddlewareFile(opts.page)) { var _opts_middleware; const loaderParams = { absolutePagePath: opts.absolutePagePath, page: opts.page, rootDir: opts.rootDir, matchers: ((_opts_middleware = opts.middleware) == null ? void 0 : _opts_middleware.matchers) ? encodeMatchers(opts.middleware.matchers) : '', preferredRegion: opts.preferredRegion, middlewareConfig: Buffer.from(JSON.stringify(opts.middlewareConfig || {})).toString('base64'), ...cacheHandler ? { cacheHandler } : {} }; return { import: `next-middleware-loader?${stringify(loaderParams)}!`, layer: WEBPACK_LAYERS.middleware, filename: opts.isDev ? 'middleware.js' : undefined }; } if (isAPIRoute(opts.page)) { const loaderParams = { absolutePagePath: opts.absolutePagePath, page: opts.page, rootDir: opts.rootDir, preferredRegion: opts.preferredRegion, middlewareConfig: Buffer.from(JSON.stringify(opts.middlewareConfig || {})).toString('base64'), ...cacheHandler ? { cacheHandler } : {} }; return { import: `next-edge-function-loader?${stringify(loaderParams)}!`, layer: WEBPACK_LAYERS.apiEdge }; } const loaderParams = { absolute500Path: opts.pages['/500'] || '', absoluteAppPath: opts.pages['/_app'], absoluteDocumentPath: opts.pages['/_document'], absoluteErrorPath: opts.pages['/_error'], absolutePagePath: opts.absolutePagePath, dev: opts.isDev, isServerComponent: opts.isServerComponent, page: opts.page, pagesType: opts.pagesType, appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'), sriEnabled: !opts.isDev && !!((_opts_config_experimental_sri = opts.config.experimental.sri) == null ? void 0 : _opts_config_experimental_sri.algorithm), preferredRegion: opts.preferredRegion, middlewareConfig: Buffer.from(JSON.stringify(opts.middlewareConfig || {})).toString('base64'), serverActions: opts.config.experimental.serverActions, cacheHandlers: JSON.stringify(opts.config.cacheHandlers || {}), ...cacheHandler ? { cacheHandler } : {} }; return { import: `next-edge-ssr-loader?${JSON.stringify(loaderParams)}!`, // The Edge bundle includes the server in its entrypoint, so it has to // be in the SSR layer — we later convert the page request to the RSC layer // via a webpack rule. layer: opts.appDirLoader ? WEBPACK_LAYERS.serverSideRendering : undefined }; } export function getInstrumentationEntry(opts) { // the '../' is needed to make sure the file is not chunked const filename = `${opts.isEdgeServer ? 'edge-' : opts.isDev ? '' : '../'}${INSTRUMENTATION_HOOK_FILENAME}.js`; return { import: opts.absolutePagePath, filename, layer: WEBPACK_LAYERS.instrument }; } export function getAppLoader() { return process.env.BUILTIN_APP_LOADER ? `builtin:next-app-loader` : 'next-app-loader'; } export function getAppEntry(opts) { if (process.env.NEXT_RSPACK && process.env.BUILTIN_APP_LOADER) { ; opts.projectRoot = normalize(join(__dirname, '../../..')); } return { import: `${getAppLoader()}?${stringify(opts)}!`, layer: WEBPACK_LAYERS.reactServerComponents }; } export function getClientEntry(opts) { const loaderOptions = { 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 function runDependingOnPageType(params) { if (params.pageType === PAGE_TYPES.ROOT && isInstrumentationHookFile(params.page)) { params.onServer(); params.onEdgeServer(); return; } if (isProxyFile(params.page)) { params.onServer(); return; } if (isMiddlewareFile(params.page)) { if (params.pageRuntime === 'nodejs') { params.onServer(); return; } else { params.onEdgeServer(); return; } } if (isAPIRoute(params.page)) { if (isEdgeRuntime(params.pageRuntime)) { params.onEdgeServer(); return; } params.onServer(); return; } if (params.page === '/_document') { params.onServer(); return; } if (params.page === '/_app' || params.page === '/_error' || params.page === '/404' || params.page === '/500') { params.onClient(); params.onServer(); return; } if (isEdgeRuntime(params.pageRuntime)) { params.onClient(); params.onEdgeServer(); return; } params.onClient(); params.onServer(); return; } export async function createEntrypoints(params) { const { config, pages, pagesDir, isDev, rootDir, rootPaths, appDir, appPaths, appDefaultPaths, pageExtensions, deferredEntriesFilter } = params; const deferredEntries = config.experimental.deferredEntries; const edgeServer = {}; const server = {}; const client = {}; const entrySourceDirectories = new Set(); let middlewareMatchers = undefined; let appPathsPerRoute = {}; if (appDir && appPaths) { const appPageFiles = new Map(); for(const pathname in appPaths){ const normalizedPath = normalizeAppPath(pathname); const actualPath = appPaths[pathname]; if (!appPathsPerRoute[normalizedPath]) { appPathsPerRoute[normalizedPath] = []; } // TODO-APP: refactor to pass the page path from createPagesMapping instead. const appPath = getPageFromPath(actualPath, pageExtensions).replace(APP_DIR_ALIAS, ''); appPathsPerRoute[normalizedPath].push(appPath); appPageFiles.set(appPath, actualPath); } // TODO: find a better place to do this const { unmatchedAppPages, incompatibleParallelRouteSlots } = normalizeCatchAllRoutes(appPathsPerRoute, { strictRouteMatching: config.experimental.strictRouteMatching, defaultAppPaths: Object.keys(appDefaultPaths ?? {}) }); // Only App Router pages can make an intercepted URL directly renderable. // Route handlers and metadata routes may share the pathname, but they // cannot provide the canonical page shown by an initial request. const appPagePathsPerRoute = Object.fromEntries(Object.entries(appPathsPerRoute).flatMap(([route, routeAppPaths])=>{ const pageAppPaths = routeAppPaths.filter(isAppPageRoute); return pageAppPaths.length > 0 ? [ [ route, pageAppPaths ] ] : []; })); const missingCanonicalInterceptionRoutes = config.experimental.strictRouteMatching ? findMissingCanonicalInterceptionRoutes(appPagePathsPerRoute) : []; const routeMatchingErrors = []; if (missingCanonicalInterceptionRoutes.length > 0) { routeMatchingErrors.push(new MissingCanonicalInterceptionRoutesError(missingCanonicalInterceptionRoutes)); } if (incompatibleParallelRouteSlots.length > 0) { routeMatchingErrors.push(new IncompatibleParallelRouteSlotsError(await Promise.all(incompatibleParallelRouteSlots.map(async (incompatibleRoute)=>{ const layoutPagePath = posix.join(incompatibleRoute.layoutPath, 'layout'); const layoutFile = await findPageFile(appDir, layoutPagePath, pageExtensions, true); return { ...incompatibleRoute, layoutFile: relative(rootDir, layoutFile ? join(appDir, layoutFile) : join(appDir, layoutPagePath)) }; })))); } if (unmatchedAppPages.length > 0) { routeMatchingErrors.push(new UnmatchedAppPagesError(unmatchedAppPages.map((appPath)=>{ const absolutePagePath = appPageFiles.get(appPath); if (!absolutePagePath) return appPath; return relative(rootDir, getPageFilePath({ absolutePagePath, pagesDir, appDir, rootDir })); }))); } if (routeMatchingErrors.length > 0) { const warning = getStrictRouteMatchingDefaultWarning(config); if (warning) { Log.warnOnce(warning); } } if (routeMatchingErrors.length === 1) { throw routeMatchingErrors[0]; } if (routeMatchingErrors.length > 1) { throw new Error(routeMatchingErrors.map((error)=>error.message).join('\n\n')); } // Make sure to sort parallel routes to make the result deterministic. appPathsPerRoute = Object.fromEntries(Object.entries(appPathsPerRoute).map(([k, v])=>[ k, v.sort(compareAppPaths) ])); } const getEntryHandler = (mappings, pagesType)=>async (page)=>{ if (pagesType === PAGE_TYPES.APP && config.experimental.strictRouteMatching && !(normalizeAppPath(page) in appPathsPerRoute)) { return; } // Apply deferred entries filter if specified if (deferredEntriesFilter) { const isDeferred = isDeferredEntry(page, deferredEntries); if (deferredEntriesFilter === 'exclude' && isDeferred) { // Skip deferred entries when excluding them return; } if (deferredEntriesFilter === 'only' && !isDeferred) { // Skip non-deferred entries when only including deferred ones return; } } const bundleFile = normalizePagePath(page); const clientBundlePath = posix.join(pagesType, bundleFile); const serverBundlePath = pagesType === PAGE_TYPES.PAGES ? posix.join('pages', bundleFile) : pagesType === PAGE_TYPES.APP ? posix.join('app', bundleFile) : bundleFile.slice(1); const absolutePagePath = mappings[page]; // Handle paths that have aliases const pageFilePath = getPageFilePath({ absolutePagePath, pagesDir, appDir, rootDir }); // A deferred-entry callback may materialize source beside this route. // Keep the owning directory so the bundler can invalidate that subtree // without discarding filesystem cache entries for the rest of the app. entrySourceDirectories.add(dirname(pageFilePath)); const isInsideAppDir = !!appDir && (absolutePagePath.startsWith(APP_DIR_ALIAS) || absolutePagePath.startsWith(appDir)); const staticInfo = await getStaticInfoIncludingLayouts({ isInsideAppDir, pageExtensions, pageFilePath, appDir, config, isDev, page }); // TODO(timneutkens): remove this const isServerComponent = isInsideAppDir && staticInfo.rsc !== RSC_MODULE_TYPES.client; if (isMiddlewareFile(page)) { var _staticInfo_middleware; middlewareMatchers = ((_staticInfo_middleware = staticInfo.middleware) == null ? void 0 : _staticInfo_middleware.matchers) ?? [ { regexp: '.*', originalSource: '/:path*' } ]; } const isInstrumentation = isInstrumentationHookFile(page) && pagesType === PAGE_TYPES.ROOT; const matchedAppPaths = pagesType === PAGE_TYPES.APP ? appPathsPerRoute[normalizeAppPath(page)] ?? null : null; const normalizedAppPage = normalizeAppPath(page); const isFinalRouteMatcher = config.experimental.strictRouteMatching && (matchedAppPaths == null ? void 0 : matchedAppPaths.length) && matchedAppPaths.some((appPath)=>normalizeAppPath(appPath) === normalizedAppPage) && selectAppPageEntry(normalizedAppPage, matchedAppPaths) === page; runDependingOnPageType({ page, pageRuntime: staticInfo.runtime, pageType: pagesType, onClient: ()=>{ if (isServerComponent || isInsideAppDir) { // We skip the initial entries for server component pages and let the // server compiler inject them instead. } else { client[clientBundlePath] = getClientEntry({ absolutePagePath, page }); } }, onServer: ()=>{ if (pagesType === 'app' && appDir) { server[serverBundlePath] = getAppEntry({ page, name: serverBundlePath, pagePath: absolutePagePath, appDir, appPaths: matchedAppPaths, allNormalizedAppPaths: Object.keys(appPathsPerRoute), pageExtensions, basePath: config.basePath, assetPrefix: config.assetPrefix, nextConfigOutput: config.output, preferredRegion: staticInfo.preferredRegion, middlewareConfig: encodeToBase64(staticInfo.middleware || {}), isGlobalNotFoundEnabled: config.experimental.globalNotFound ? true : undefined, explicitParallelRouteChildren: config.experimental.explicitParallelRouteChildren ? true : undefined, strictRouteMatching: config.experimental.strictRouteMatching ? true : undefined, isFinalRouteMatcher: isFinalRouteMatcher ? true : undefined }); } else if (isInstrumentation) { server[serverBundlePath.replace('src/', '')] = getInstrumentationEntry({ absolutePagePath, isEdgeServer: false, isDev: false }); } else if (isMiddlewareFile(page)) { server[serverBundlePath.replace('src/', '')] = getEdgeServerEntry({ ...params, rootDir, absolutePagePath: absolutePagePath, bundlePath: clientBundlePath, isDev: false, isServerComponent, page, middleware: staticInfo == null ? void 0 : staticInfo.middleware, pagesType, preferredRegion: staticInfo.preferredRegion, middlewareConfig: staticInfo.middleware }); } else if (isAPIRoute(page)) { server[serverBundlePath] = [ getRouteLoaderEntry({ kind: RouteKind.PAGES_API, page, absolutePagePath, preferredRegion: staticInfo.preferredRegion, middlewareConfig: staticInfo.middleware || {} }) ]; } else if (!isMiddlewareFile(page) && !isInternalComponent(absolutePagePath) && !isNonRoutePagesPage(page)) { server[serverBundlePath] = [ getRouteLoaderEntry({ kind: RouteKind.PAGES, page, pages, absolutePagePath, preferredRegion: staticInfo.preferredRegion, middlewareConfig: staticInfo.middleware ?? {} }) ]; } else { server[serverBundlePath] = [ absolutePagePath ]; } }, onEdgeServer: ()=>{ let appDirLoader = ''; if (isInstrumentation) { edgeServer[serverBundlePath.replace('src/', '')] = getInstrumentationEntry({ absolutePagePath, isEdgeServer: true, isDev: false }); } else { if (pagesType === 'app') { appDirLoader = getAppEntry({ name: serverBundlePath, page, pagePath: absolutePagePath, appDir: appDir, appPaths: matchedAppPaths, allNormalizedAppPaths: Object.keys(appPathsPerRoute), pageExtensions, basePath: config.basePath, assetPrefix: config.assetPrefix, nextConfigOutput: config.output, // This isn't used with edge as it needs to be set on the entry module, which will be the `edgeServerEntry` instead. // Still passing it here for consistency. preferredRegion: staticInfo.preferredRegion, middlewareConfig: Buffer.from(JSON.stringify(staticInfo.middleware || {})).toString('base64'), isGlobalNotFoundEnabled: config.experimental.globalNotFound ? true : undefined, explicitParallelRouteChildren: config.experimental.explicitParallelRouteChildren ? true : undefined, strictRouteMatching: config.experimental.strictRouteMatching ? true : undefined, isFinalRouteMatcher: isFinalRouteMatcher ? true : undefined }).import; } edgeServer[serverBundlePath] = getEdgeServerEntry({ ...params, rootDir, absolutePagePath: absolutePagePath, bundlePath: clientBundlePath, isDev: false, isServerComponent, page, middleware: staticInfo == null ? void 0 : staticInfo.middleware, pagesType, appDirLoader, preferredRegion: staticInfo.preferredRegion, middlewareConfig: staticInfo.middleware }); } } }); }; const promises = []; if (appPaths) { const entryHandler = getEntryHandler(appPaths, PAGE_TYPES.APP); promises.push(Promise.all(Object.keys(appPaths).map(entryHandler))); } if (rootPaths) { promises.push(Promise.all(Object.keys(rootPaths).map(getEntryHandler(rootPaths, PAGE_TYPES.ROOT)))); } promises.push(Promise.all(Object.keys(pages).map(getEntryHandler(pages, PAGE_TYPES.PAGES)))); await Promise.all(promises); // Optimization: If there's only one instrumentation hook in edge compiler, which means there's no edge server entry. // We remove the edge instrumentation entry from edge compiler as it can be pure server side. if (edgeServer.instrumentation && Object.keys(edgeServer).length === 1) { delete edgeServer.instrumentation; } return { client, server, edgeServer, entrySourceDirectories: [ ...entrySourceDirectories ].sort(), middlewareMatchers }; } export function finalizeEntrypoint({ name, compilerType, value, isServerComponent, hasAppDir }) { const entry = typeof value !== 'object' || Array.isArray(value) ? { import: value } : value; const isApi = name.startsWith('pages/api/'); const isInstrumentation = isInstrumentationHookFilename(name); switch(compilerType){ case COMPILER_NAMES.server: { const layer = isApi ? WEBPACK_LAYERS.apiNode : isInstrumentation ? WEBPACK_LAYERS.instrument : isServerComponent ? WEBPACK_LAYERS.reactServerComponents : name.startsWith('pages/') ? WEBPACK_LAYERS.pagesDirNode : undefined; return { publicPath: isApi ? '' : undefined, runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime', layer, ...entry }; } case COMPILER_NAMES.edgeServer: { return { layer: isApi ? WEBPACK_LAYERS.apiEdge : isMiddlewareFilename(name) || isInstrumentation ? WEBPACK_LAYERS.middleware : name.startsWith('pages/') ? WEBPACK_LAYERS.pagesDirEdge : undefined, library: { name: [ '_ENTRIES', `middleware_[name]` ], type: 'assign' }, runtime: EDGE_RUNTIME_WEBPACK, asyncChunks: false, ...entry }; } case COMPILER_NAMES.client: { const isAppLayer = hasAppDir && (name === CLIENT_STATIC_FILES_RUNTIME_MAIN_APP || name === APP_CLIENT_INTERNALS || name.startsWith('app/')); if (// Client special cases name !== CLIENT_STATIC_FILES_RUNTIME_POLYFILLS && name !== CLIENT_STATIC_FILES_RUNTIME_MAIN && name !== CLIENT_STATIC_FILES_RUNTIME_MAIN_APP && name !== CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH) { if (isAppLayer) { return { dependOn: CLIENT_STATIC_FILES_RUNTIME_MAIN_APP, layer: WEBPACK_LAYERS.appPagesBrowser, ...entry }; } return { dependOn: name.startsWith('pages/') && name !== 'pages/_app' ? 'pages/_app' : CLIENT_STATIC_FILES_RUNTIME_MAIN, layer: WEBPACK_LAYERS.pagesDirBrowser, ...entry }; } if (isAppLayer) { return { layer: WEBPACK_LAYERS.appPagesBrowser, ...entry }; } return { layer: WEBPACK_LAYERS.pagesDirBrowser, ...entry }; } default: return compilerType; } } //# sourceMappingURL=entries.js.map