Skip to content

Commit

Permalink
error when output: export is used with intercepting routes (#75058)
Browse files Browse the repository at this point in the history
Intercepting routes are built on top of
[rewrites](https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites)
which is one of the listed [unsupported
features](https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features)
of `output: "export"`.

However, because the Next.js server injects the interception routes,
it's not caught by existing validation logic in the export flow.

This properly documents that intercepting routes are not currently
supported by `output: "export"` and hard errors in next dev/build if
detected. Previously route interception would just not have worked,
instead serving the non-intercepted page when built, leading to a
confusing experience.

We eventually want to support this with improved SPA/export features,
however that's out of scope for this PR: the goal here is to ensure
we're providing more immediate feedback about the fact that this is
unsupported.
  • Loading branch information
ztanner authored Jan 18, 2025
1 parent 6fcd42c commit 808d2ba
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
20 changes: 19 additions & 1 deletion packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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`
)
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? []
}

Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/actions/app-action-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'intercepted'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => null
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/interception-routes-output-export/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function Layout(props: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
<div id="children">{props.children}</div>
<div id="modal">{props.modal}</div>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Link from 'next/link'

export default function Page() {
return (
<div>
<Link href="/page">To /page</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -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)
}
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = { output: 'export' }

module.exports = nextConfig

0 comments on commit 808d2ba

Please sign in to comment.