diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 7636ab0361933f..a89b22e8c3a1b5 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -708,7 +708,6 @@ export default async function build( .traceAsyncFn(() => loadCustomRoutes(config)) const { headers, rewrites, redirects } = customRoutes - NextBuildContext.rewrites = rewrites NextBuildContext.originalRewrites = config._originalRewrites NextBuildContext.originalRedirects = config._originalRedirects @@ -972,6 +971,8 @@ export default async function build( ...generateInterceptionRoutesRewrites(appPaths, config.basePath) ) + NextBuildContext.rewrites = rewrites + const totalAppPagesCount = appPaths.length const pageKeys = { diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index 8b7fe317537d52..b720e724f6fb45 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -44,6 +44,9 @@ const subresourceIntegrityManifest = sriEnabled : undefined const nextFontManifest = maybeJSONParse(self.__NEXT_FONT_MANIFEST) +const interceptionRouteRewrites = + maybeJSONParse(self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST) ?? [] + const render = getRender({ pagesType: PAGE_TYPES.APP, dev, @@ -65,6 +68,7 @@ const render = getRender({ buildId: 'VAR_BUILD_ID', nextFontManifest, incrementalCacheHandler, + interceptionRouteRewrites, }) export const ComponentMod = pageMod diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 0471429b0ac88a..81925dba0ee7a0 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1769,6 +1769,7 @@ export default async function getBaseWebpackConfig( new MiddlewarePlugin({ dev, sriEnabled: !dev && !!config.experimental.sri?.algorithm, + rewrites, }), isClient && new BuildManifestPlugin({ diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts index 8be86cef4333aa..67b96f6e41d855 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -13,7 +13,7 @@ import { WebNextResponse, } from '../../../../server/base-http/web' import { SERVER_RUNTIME } from '../../../../lib/constants' -import type { PrerenderManifest } from '../../..' +import type { ManifestRewriteRoute, PrerenderManifest } from '../../..' import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths' import type { SizeLimit } from '../../../../../types' import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until' @@ -31,6 +31,7 @@ export function getRender({ buildManifest, prerenderManifest, reactLoadableManifest, + interceptionRouteRewrites, renderToHTML, clientReferenceManifest, subresourceIntegrityManifest, @@ -54,6 +55,7 @@ export function getRender({ prerenderManifest: PrerenderManifest reactLoadableManifest: ReactLoadableManifest subresourceIntegrityManifest?: Record + interceptionRouteRewrites?: ManifestRewriteRoute[] clientReferenceManifest?: ClientReferenceManifest serverActionsManifest?: any serverActions?: { @@ -85,6 +87,7 @@ export function getRender({ pathname: isAppPath ? normalizeAppPath(page) : page, pagesType, prerenderManifest, + interceptionRouteRewrites, extendRenderOpts: { buildId, runtime: SERVER_RUNTIME.experimentalEdge, diff --git a/packages/next/src/build/webpack/plugins/middleware-plugin.ts b/packages/next/src/build/webpack/plugins/middleware-plugin.ts index aea342d47a2831..de43f8169f356a 100644 --- a/packages/next/src/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/src/build/webpack/plugins/middleware-plugin.ts @@ -21,6 +21,7 @@ import { NEXT_FONT_MANIFEST, SERVER_REFERENCE_MANIFEST, PRERENDER_MANIFEST, + INTERCEPTION_ROUTE_REWRITE_MANIFEST, } from '../../../shared/lib/constants' import type { MiddlewareConfig } from '../../analysis/get-page-static-info' import type { Telemetry } from '../../../telemetry/storage' @@ -28,6 +29,8 @@ import { traceGlobals } from '../../../trace/shared' import { EVENT_BUILD_FEATURE_USAGE } from '../../../telemetry/events' import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths' import { INSTRUMENTATION_HOOK_FILENAME } from '../../../lib/constants' +import type { CustomRoutes } from '../../../lib/load-custom-routes' +import { isInterceptionRouteRewrite } from '../../../lib/generate-interception-routes-rewrites' const KNOWN_SAFE_DYNAMIC_PACKAGES = require('../../../lib/known-edge-safe-packages.json') as string[] @@ -118,10 +121,10 @@ function getEntryFiles( files.push( `server/${MIDDLEWARE_BUILD_MANIFEST}.js`, - `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js` + `server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js`, + `server/${NEXT_FONT_MANIFEST}.js`, + `server/${INTERCEPTION_ROUTE_REWRITE_MANIFEST}.js` ) - - files.push(`server/${NEXT_FONT_MANIFEST}.js`) } if (hasInstrumentationHook) { @@ -144,9 +147,7 @@ function getEntryFiles( function getCreateAssets(params: { compilation: webpack.Compilation metadataByEntry: Map - opts: { - sriEnabled: boolean - } + opts: Omit }) { const { compilation, metadataByEntry, opts } = params return (assets: any) => { @@ -161,6 +162,17 @@ function getCreateAssets(params: { INSTRUMENTATION_HOOK_FILENAME ) + // we only emit this entry for the edge runtime since it doesn't have access to a routes manifest + // and we don't need to provide the entire route manifest, just the interception routes. + const interceptionRewrites = JSON.stringify( + opts.rewrites.beforeFiles.filter(isInterceptionRouteRewrite) + ) + assets[`${INTERCEPTION_ROUTE_REWRITE_MANIFEST}.js`] = new sources.RawSource( + `self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST=${JSON.stringify( + interceptionRewrites + )}` + ) as unknown as webpack.sources.RawSource + for (const entrypoint of compilation.entrypoints.values()) { if (!entrypoint.name) { continue @@ -718,13 +730,22 @@ function getExtractMetadata(params: { } } } + +interface Options { + dev: boolean + sriEnabled: boolean + rewrites: CustomRoutes['rewrites'] +} + export default class MiddlewarePlugin { - private readonly dev: boolean - private readonly sriEnabled: boolean + private readonly dev: Options['dev'] + private readonly sriEnabled: Options['sriEnabled'] + private readonly rewrites: Options['rewrites'] - constructor({ dev, sriEnabled }: { dev: boolean; sriEnabled: boolean }) { + constructor({ dev, sriEnabled, rewrites }: Options) { this.dev = dev this.sriEnabled = sriEnabled + this.rewrites = rewrites } public apply(compiler: webpack.Compiler) { @@ -769,6 +790,7 @@ export default class MiddlewarePlugin { metadataByEntry, opts: { sriEnabled: this.sriEnabled, + rewrites: this.rewrites, }, }) ) diff --git a/packages/next/src/client/route-loader.ts b/packages/next/src/client/route-loader.ts index f3ebc4740d1687..35480529ffd3e2 100644 --- a/packages/next/src/client/route-loader.ts +++ b/packages/next/src/client/route-loader.ts @@ -23,6 +23,7 @@ declare global { __RSC_SERVER_MANIFEST?: any __NEXT_FONT_MANIFEST?: any __SUBRESOURCE_INTEGRITY_MANIFEST?: string + __INTERCEPTION_ROUTE_REWRITE_MANIFEST?: string } } diff --git a/packages/next/src/lib/generate-interception-routes-rewrites.ts b/packages/next/src/lib/generate-interception-routes-rewrites.ts index 4de355fc51b3b6..3876077ed7677f 100644 --- a/packages/next/src/lib/generate-interception-routes-rewrites.ts +++ b/packages/next/src/lib/generate-interception-routes-rewrites.ts @@ -6,7 +6,6 @@ import { isInterceptionRouteAppPath, } from '../server/future/helpers/interception-routes' import type { Rewrite } from './load-custom-routes' -import type { ManifestRewriteRoute } from '../build' // a function that converts normalised paths (e.g. /foo/[bar]/[baz]) to the format expected by pathToRegexp (e.g. /foo/:bar/:baz) function toPathToRegexpPath(path: string): string { @@ -88,7 +87,7 @@ export function generateInterceptionRoutesRewrites( return rewrites } -export function isInterceptionRouteRewrite(route: ManifestRewriteRoute) { +export function isInterceptionRouteRewrite(route: Rewrite) { // When we generate interception rewrites in the above implementation, we always do so with only a single `has` condition. return route.has?.[0].key === NEXT_URL } diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index bc88b139767d02..72470133aad717 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2001,7 +2001,7 @@ export default abstract class Server { res.setHeader('vary', RSC_VARY_HEADER) if (isPrefetchRSCRequest) { - const couldBeRewritten = this.interceptionRouteRewrites?.some( + const couldBeRewritten = this.interceptionRouteRewrites.some( (rewrite) => { return new RegExp(rewrite.regex).test(resolvedUrlPathname) } diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index df0107524e388f..feaeff61400fce 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -325,7 +325,6 @@ export async function setupFsCheck(opts: { ) ) const rewrites = { - // TODO: add interception routes generateInterceptionRoutesRewrites() beforeFiles: customRoutes.rewrites.beforeFiles.map((item) => buildCustomRoute('before_files_rewrite', item) ), diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index dc7767d19465a4..0d95ec38883cdd 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -38,6 +38,7 @@ import { } from '../../../shared/lib/router/utils/prepare-destination' import { createRequestResponseMocks } from '../mock-request' import type { TLSSocket } from 'tls' +import { isInterceptionRouteRewrite } from '../../../lib/generate-interception-routes-rewrites' const debug = setupDebug('next:router-server:resolve-routes') @@ -71,7 +72,14 @@ export function getResolveRoutes( // check middleware (using matchers) { match: () => ({}), name: 'middleware' }, - ...(opts.minimalMode ? [] : fsChecker.rewrites.beforeFiles), + ...(opts.minimalMode + ? [] + : fsChecker.rewrites.beforeFiles.filter( + // TODO: Currently have to filter interception routes out of `beforeFiles` because for some reason + // they are also processed in `fsChecker.interceptionRoutes` despite both being modeled as beforeFiles rewrites + // Need to look into why that's the case + (r) => !isInterceptionRouteRewrite(r) + )), // check middleware (using matchers) { match: () => ({}), name: 'before_files_end' }, @@ -359,6 +367,7 @@ export function getResolveRoutes( } if (params) { + // TODO: Why is this using a special `interceptionRoutes` value and not `fsChecker.beforeFiles.rewrites`? if (fsChecker.interceptionRoutes && route.name === 'before_files_end') { for (const interceptionRoute of fsChecker.interceptionRoutes) { const result = await handleRoute(interceptionRoute) diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 5316806a27b4b4..a16c9dcc89b7a1 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -2296,18 +2296,20 @@ async function startWatcher(opts: SetupOpts) { ? getMiddlewareRouteMatcher(serverFields.middleware?.matchers) : undefined - opts.fsChecker.interceptionRoutes = - generateInterceptionRoutesRewrites( - Object.keys(appPaths), - opts.nextConfig.basePath - )?.map((item) => - buildCustomRoute( - 'before_files_rewrite', - item, - opts.nextConfig.basePath, - opts.nextConfig.experimental.caseSensitiveRoutes - ) - ) || [] + const interceptionRoutes = generateInterceptionRoutesRewrites( + Object.keys(appPaths), + opts.nextConfig.basePath + ).map((item) => + buildCustomRoute( + 'before_files_rewrite', + item, + opts.nextConfig.basePath, + opts.nextConfig.experimental.caseSensitiveRoutes + ) + ) + + opts.fsChecker.interceptionRoutes = interceptionRoutes + opts.fsChecker.rewrites.beforeFiles.push(...interceptionRoutes) const exportPathMap = (typeof nextConfig.exportPathMap === 'function' && diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index fe14870862409f..da14118cf6ad03 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -371,6 +371,8 @@ export default class NextNodeServer extends BaseServer { } protected getInterceptionRouteRewrites(): ManifestRewriteRoute[] { + if (!this.enabledDirectories.app) return [] + const routesManifest = this.getRoutesManifest() return ( routesManifest?.rewrites.beforeFiles.filter(isInterceptionRouteRewrite) ?? diff --git a/packages/next/src/server/web-server.ts b/packages/next/src/server/web-server.ts index 3539ebda3d0ef4..38569833a6ae7c 100644 --- a/packages/next/src/server/web-server.ts +++ b/packages/next/src/server/web-server.ts @@ -44,6 +44,7 @@ interface WebServerOptions extends Options { | undefined incrementalCacheHandler?: any prerenderManifest: PrerenderManifest | undefined + interceptionRouteRewrites?: ManifestRewriteRoute[] } } @@ -395,7 +396,6 @@ export default class NextWebServer extends BaseServer { } protected getInterceptionRouteRewrites(): ManifestRewriteRoute[] { - // TODO: This needs to be implemented. - return [] + return this.serverOptions.webServerConfig.interceptionRouteRewrites ?? [] } } diff --git a/packages/next/src/shared/lib/constants.ts b/packages/next/src/shared/lib/constants.ts index f0e667feb13796..c3b3ed8563cc4c 100644 --- a/packages/next/src/shared/lib/constants.ts +++ b/packages/next/src/shared/lib/constants.ts @@ -77,6 +77,9 @@ export const MIDDLEWARE_BUILD_MANIFEST = 'middleware-build-manifest' // server/middleware-react-loadable-manifest.js export const MIDDLEWARE_REACT_LOADABLE_MANIFEST = 'middleware-react-loadable-manifest' +// server/interception-route-rewrite-manifest.js +export const INTERCEPTION_ROUTE_REWRITE_MANIFEST = + 'interception-route-rewrite-manifest' // static/runtime/main.js export const CLIENT_STATIC_FILES_RUNTIME_MAIN = `main` diff --git a/test/e2e/app-dir/interception-route-prefetch-cache/app/layout-edge.tsx b/test/e2e/app-dir/interception-route-prefetch-cache/app/layout-edge.tsx new file mode 100644 index 00000000000000..9a4b0ee3ec3268 --- /dev/null +++ b/test/e2e/app-dir/interception-route-prefetch-cache/app/layout-edge.tsx @@ -0,0 +1,17 @@ +// this file is swapped in for the normal layout file in the edge runtime test + +import Link from 'next/link' + +export const runtime = 'edge' + +export default function RootLayout({ children }) { + return ( + + + + home + {children} + + + ) +} diff --git a/test/e2e/app-dir/interception-route-prefetch-cache/interception-route-prefetch-cache.test.ts b/test/e2e/app-dir/interception-route-prefetch-cache/interception-route-prefetch-cache.test.ts index b830f64f4a9e55..8579b284fcfb95 100644 --- a/test/e2e/app-dir/interception-route-prefetch-cache/interception-route-prefetch-cache.test.ts +++ b/test/e2e/app-dir/interception-route-prefetch-cache/interception-route-prefetch-cache.test.ts @@ -1,13 +1,10 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup, FileRef } from 'e2e-utils' import { check } from 'next-test-utils' +import { join } from 'path' import { Response } from 'playwright-chromium' -createNextDescribe( - 'interception-route-prefetch-cache', - { - files: __dirname, - }, - ({ next, isNextStart }) => { +describe('interception-route-prefetch-cache', () => { + function runTests({ next }: ReturnType) { it('should render the correct interception when two distinct layouts share the same path structure', async () => { const browser = await next.browser('/') @@ -42,7 +39,17 @@ createNextDescribe( /Intercepted on Bar Page/ ) }) + } + + describe('runtime = nodejs', () => { + const testSetup = nextTestSetup({ + files: __dirname, + }) + runTests(testSetup) + + const { next, isNextStart } = testSetup + // this is a node runtime specific test as edge doesn't support static rendering if (isNextStart) { it('should not be a cache HIT when prefetching an interception route', async () => { const responses: { cacheStatus: string; pathname: string }[] = [] @@ -78,5 +85,16 @@ createNextDescribe( expect(interceptionPrefetchResponse.cacheStatus).toBeUndefined() }) } - } -) + }) + + describe('runtime = edge', () => { + runTests( + nextTestSetup({ + files: { + app: new FileRef(join(__dirname, 'app')), + 'app/layout.tsx': new FileRef(join(__dirname, 'app/layout-edge.tsx')), + }, + }) + ) + }) +})