diff --git a/docs/01-app/03-building-your-application/10-deploying/02-static-exports.mdx b/docs/01-app/03-building-your-application/10-deploying/02-static-exports.mdx index 2f2c375de57ce..e16c482900b06 100644 --- a/docs/01-app/03-building-your-application/10-deploying/02-static-exports.mdx +++ b/docs/01-app/03-building-your-application/10-deploying/02-static-exports.mdx @@ -288,6 +288,7 @@ Features that require a Node.js server, or dynamic logic that cannot be computed - [Image Optimization](/docs/app/building-your-application/optimizing/images) with the default `loader` - [Draft Mode](/docs/app/building-your-application/configuring/draft-mode) - [Server Actions](/docs/app/building-your-application/data-fetching/server-actions-and-mutations) +- [Intercepting Routes](/docs/app/building-your-application/routing/intercepting-routes) Attempting to use any of these features with `next dev` will result in an error, similar to setting the [`dynamic`](/docs/app/api-reference/file-conventions/route-segment-config#dynamic) option to `error` in the root layout. diff --git a/packages/next/errors.json b/packages/next/errors.json index f3a918ac233fe..fb9f2243e2468 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -622,5 +622,7 @@ "621": "Required root params (%s) were not provided in generateStaticParams for %s, please provide at least one value for each.", "622": "A required root parameter (%s) was not provided in generateStaticParams for %s, please provide at least one value.", "623": "Invalid quality prop (%s) on \\`next/image\\` does not match \\`images.qualities\\` configured in your \\`next.config.js\\`\\nSee more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities", - "624": "Internal Next.js Error: createMutableActionQueue was called more than once" + "624": "Internal Next.js Error: createMutableActionQueue was called more than once", + "625": "Server Actions are not supported with static export.\\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features", + "626": "Intercepting routes are not supported with static export.\\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features" } diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index e09daa276aa73..069691a42115e 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -32,6 +32,7 @@ import { SERVER_DIRECTORY, SERVER_REFERENCE_MANIFEST, APP_PATH_ROUTES_MANIFEST, + ROUTES_MANIFEST, } from '../shared/lib/constants' import loadConfig from '../server/config' import type { ExportPathMap } from '../server/config-shared' @@ -52,6 +53,7 @@ import { formatManifest } from '../build/manifests/formatter/format-manifest' import { TurborepoAccessTraceResult } from '../build/turborepo-access-trace' import { createProgress } from '../build/progress' import type { DeepReadonly } from '../shared/lib/deep-readonly' +import { isInterceptionRouteRewrite } from '../lib/generate-interception-routes-rewrites' export class ExportError extends Error { code = 'NEXT_EXPORT_ERROR' @@ -302,13 +304,29 @@ async function exportAppImpl( serverActionsManifest = require( join(distDir, SERVER_DIRECTORY, SERVER_REFERENCE_MANIFEST + '.json') ) + if (nextConfig.output === 'export') { + const routesManifest = require(join(distDir, ROUTES_MANIFEST)) + + // We already prevent rewrites earlier in the process, however Next.js will insert rewrites + // for interception routes so we need to check for that here. + if (routesManifest?.rewrites?.beforeFiles?.length > 0) { + const hasInterceptionRouteRewrite = + routesManifest.rewrites.beforeFiles.some(isInterceptionRouteRewrite) + + if (hasInterceptionRouteRewrite) { + throw new ExportError( + `Intercepting routes are not supported with static export.\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features` + ) + } + } + if ( Object.keys(serverActionsManifest.node).length > 0 || Object.keys(serverActionsManifest.edge).length > 0 ) { throw new ExportError( - `Server Actions are not supported with static export.` + `Server Actions are not supported with static export.\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features` ) } } diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index 9b0e543261517..6b7092c4252fe 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -604,6 +604,14 @@ export default class DevServer extends Server { this.nextConfig.basePath ).map((route) => new RegExp(buildCustomRoute('rewrite', route).regex)) + if (this.nextConfig.output === 'export' && rewrites.length > 0) { + Log.error( + 'Intercepting routes are not supported with static export.\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features' + ) + + process.exit(1) + } + return rewrites ?? [] } diff --git a/test/e2e/app-dir/actions/app-action-export.test.ts b/test/e2e/app-dir/actions/app-action-export.test.ts index 7223b0b5d6a60..66288756848cd 100644 --- a/test/e2e/app-dir/actions/app-action-export.test.ts +++ b/test/e2e/app-dir/actions/app-action-export.test.ts @@ -27,6 +27,8 @@ describe('app-dir action handling - next export', () => { } ` ) + // interception routes are also not supported with export + await next.remove('app/interception-routes') try { await next.start() } catch {} diff --git a/test/e2e/app-dir/interception-routes-output-export/app/@modal/(.)page/page.tsx b/test/e2e/app-dir/interception-routes-output-export/app/@modal/(.)page/page.tsx new file mode 100644 index 0000000000000..7c48f6584136b --- /dev/null +++ b/test/e2e/app-dir/interception-routes-output-export/app/@modal/(.)page/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'intercepted' +} diff --git a/test/e2e/app-dir/interception-routes-output-export/app/@modal/default.tsx b/test/e2e/app-dir/interception-routes-output-export/app/@modal/default.tsx new file mode 100644 index 0000000000000..ad4e17b5767f9 --- /dev/null +++ b/test/e2e/app-dir/interception-routes-output-export/app/@modal/default.tsx @@ -0,0 +1 @@ +export default () => null diff --git a/test/e2e/app-dir/interception-routes-output-export/app/default.tsx b/test/e2e/app-dir/interception-routes-output-export/app/default.tsx new file mode 100644 index 0000000000000..86b9e9a388129 --- /dev/null +++ b/test/e2e/app-dir/interception-routes-output-export/app/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null +} diff --git a/test/e2e/app-dir/interception-routes-output-export/app/layout.tsx b/test/e2e/app-dir/interception-routes-output-export/app/layout.tsx new file mode 100644 index 0000000000000..6a674f5ea764a --- /dev/null +++ b/test/e2e/app-dir/interception-routes-output-export/app/layout.tsx @@ -0,0 +1,13 @@ +export default function Layout(props: { + children: React.ReactNode + modal: React.ReactNode +}) { + return ( + +
+hello world
+} diff --git a/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts b/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts new file mode 100644 index 0000000000000..ca18ed020972b --- /dev/null +++ b/test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts @@ -0,0 +1,30 @@ +import { isNextDev, isNextStart } from 'e2e-utils' +import { findPort, killApp, launchApp, nextBuild, retry } from 'next-test-utils' + +describe('interception-routes-output-export', () => { + it('should error when using interception routes with static export', async () => { + if (isNextStart) { + const { code, stderr } = await nextBuild(__dirname, [], { stderr: true }) + expect(stderr).toContain( + 'Intercepting routes are not supported with static export.' + ) + expect(code).toBe(1) + } else if (isNextDev) { + let stderr = '' + const port = await findPort() + const app = await launchApp(__dirname, port, { + onStderr(msg) { + stderr += msg || '' + }, + }) + + await retry(async () => { + expect(stderr).toContain( + 'Intercepting routes are not supported with static export.' + ) + }) + + await killApp(app) + } + }) +}) diff --git a/test/e2e/app-dir/interception-routes-output-export/next.config.js b/test/e2e/app-dir/interception-routes-output-export/next.config.js new file mode 100644 index 0000000000000..db98d74fb2c31 --- /dev/null +++ b/test/e2e/app-dir/interception-routes-output-export/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { output: 'export' } + +module.exports = nextConfig