diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 3bb0b0ac6e0f5..b2f945a191f5d 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -892,6 +892,7 @@ impl AppEndpoint { "server/middleware-build-manifest.js".to_string(), "server/middleware-react-loadable-manifest.js".to_string(), "server/next-font-manifest.js".to_string(), + "server/interception-route-rewrite-manifest.js".to_string(), ]; let mut wasm_paths_from_root = vec![]; diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 513f7567fb915..c15e5c9ccc2e9 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -720,7 +720,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 @@ -984,6 +983,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 8b7fe317537d5..b720e724f6fb4 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 db193dba0db50..826010c76e495 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1789,6 +1789,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 8be86cef4333a..67b96f6e41d85 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 aea342d47a283..de43f8169f356 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 ca22834908464..ab0337eff033f 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 4de355fc51b3b..3876077ed7677 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/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index df0107524e388..937cd62d33991 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) ), @@ -393,7 +392,7 @@ export async function setupFsCheck(opts: { dynamicRoutes, nextDataRoutes, - interceptionRoutes: undefined as + exportPathMapRoutes: undefined as | undefined | ReturnType>[], 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 e84beb96c512b..37d55112535e4 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -358,9 +358,12 @@ export function getResolveRoutes( } if (params) { - if (fsChecker.interceptionRoutes && route.name === 'before_files_end') { - for (const interceptionRoute of fsChecker.interceptionRoutes) { - const result = await handleRoute(interceptionRoute) + if ( + fsChecker.exportPathMapRoutes && + route.name === 'before_files_end' + ) { + for (const exportPathMapRoute of fsChecker.exportPathMapRoutes) { + const result = await handleRoute(exportPathMapRoute) if (result) { return result 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 bce38c0d6cfc8..f035e1ba404dc 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 @@ -68,7 +68,10 @@ import { getRouteMatcher } from '../../../shared/lib/router/utils/route-matcher' import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' import { createClientRouterFilter } from '../../../lib/create-client-router-filter' import { absolutePathToPage } from '../../../shared/lib/page-path/absolute-path-to-page' -import { generateInterceptionRoutesRewrites } from '../../../lib/generate-interception-routes-rewrites' +import { + generateInterceptionRoutesRewrites, + isInterceptionRouteRewrite, +} from '../../../lib/generate-interception-routes-rewrites' import { store as consoleStore } from '../../../build/output/store' import { @@ -87,6 +90,7 @@ import { REACT_LOADABLE_MANIFEST, MIDDLEWARE_REACT_LOADABLE_MANIFEST, MIDDLEWARE_BUILD_MANIFEST, + INTERCEPTION_ROUTE_REWRITE_MANIFEST, } from '../../../shared/lib/constants' import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher' @@ -868,15 +872,32 @@ async function startWatcher(opts: SetupOpts) { 'server', `${MIDDLEWARE_BUILD_MANIFEST}.js` ) + const interceptionRewriteManifestPath = path.join( + distDir, + 'server', + `${INTERCEPTION_ROUTE_REWRITE_MANIFEST}.js` + ) deleteCache(buildManifestPath) deleteCache(middlewareBuildManifestPath) + deleteCache(interceptionRewriteManifestPath) await writeFileAtomic( buildManifestPath, JSON.stringify(buildManifest, null, 2) ) await writeFileAtomic( middlewareBuildManifestPath, - `self.__BUILD_MANIFEST=${JSON.stringify(buildManifest)}` + `self.__BUILD_MANIFEST=${JSON.stringify(buildManifest)};` + ) + + const interceptionRewrites = JSON.stringify( + rewrites.beforeFiles.filter(isInterceptionRouteRewrite) + ) + + await writeFileAtomic( + interceptionRewriteManifestPath, + `self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST=${JSON.stringify( + interceptionRewrites + )};` ) const content: ClientBuildManifest = { @@ -2340,18 +2361,19 @@ 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.rewrites.beforeFiles.push(...interceptionRoutes) const exportPathMap = (typeof nextConfig.exportPathMap === 'function' && @@ -2367,19 +2389,22 @@ async function startWatcher(opts: SetupOpts) { ))) || {} - for (const [key, value] of Object.entries(exportPathMap || {})) { - opts.fsChecker.interceptionRoutes.push( - buildCustomRoute( - 'before_files_rewrite', - { - source: key, - destination: `${value.page}${ - value.query ? '?' : '' - }${qs.stringify(value.query)}`, - }, - opts.nextConfig.basePath, - opts.nextConfig.experimental.caseSensitiveRoutes - ) + const exportPathMapEntries = Object.entries(exportPathMap || {}) + + if (exportPathMapEntries.length > 0) { + opts.fsChecker.exportPathMapRoutes = exportPathMapEntries.map( + ([key, value]) => + buildCustomRoute( + 'before_files_rewrite', + { + source: key, + destination: `${value.page}${ + value.query ? '?' : '' + }${qs.stringify(value.query)}`, + }, + opts.nextConfig.basePath, + opts.nextConfig.experimental.caseSensitiveRoutes + ) ) } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 2b62c6b07697a..78e812cdc6187 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -373,6 +373,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 3539ebda3d0ef..38569833a6ae7 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 f0e667feb1379..c3b3ed8563cc4 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 0000000000000..9a4b0ee3ec326 --- /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 b830f64f4a9e5..8579b284fcfb9 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')), + }, + }) + ) + }) +})