Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/next/src/build/adapter/build-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,7 @@ export async function handleBuildComplete({
config.i18n?.locales || []
).pathname
const parentOutput =
pageOutputMap[normalizedSrcRoute] || appOutputMap[normalizedSrcRoute]
appOutputMap[srcRoute] || pageOutputMap[normalizedSrcRoute]

if (!parentOutput && !allowMissing) {
console.error({
Expand Down Expand Up @@ -2089,6 +2089,7 @@ export async function handleBuildComplete({
const dynamicRoutes: DynamicRouteItem[] = []
const dynamicDataRoutes: DynamicRouteItem[] = []
const dynamicSegmentRoutes: DynamicRouteItem[] = []
const appPathnames = new Set(appPageKeys?.map(normalizeAppPath) ?? [])

const getDestinationQuery = (routeKeys: Record<string, string>) => {
const items = Object.entries(routeKeys ?? {})
Expand Down Expand Up @@ -2131,7 +2132,9 @@ export async function handleBuildComplete({
route.page
)

const shouldLocalize = Boolean(config.i18n && !isAPIRoute(route.page))
const shouldLocalize = Boolean(
config.i18n && !isAPIRoute(route.page) && !appPathnames.has(route.page)
)

const routeRegex = getNamedRouteRegex(route.page, {
prefixRouteKeys: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { nextTestSetup } from 'e2e-utils'
import { load } from 'cheerio'

const deploymentHost =
process.env.NEXT_TEST_DEPLOYMENT_HOST ||
(process.env.VERCEL === '1' ? process.env.VERCEL_URL : undefined)

describe('i18n-app-pages-domain-routing', () => {
const { next } = nextTestSetup({
const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
// Adapter-generated routing metadata is covered separately. This suite
// verifies the built-in router and cannot run in deploy mode until the
// adapter-specific follow-up is applied.
skipDeployment: true,
// This layer includes the adapter routing fix, so exercise the shared
// regression fixture in deployment mode as well.
skipDeployment: false,
})

function fetchFromDomain(pathname: string, host = 'nl.example.local') {
function fetchFromDomain(
pathname: string,
host = deploymentHost || 'nl.example.local'
) {
if (isNextDeploy) {
// The deployed URL already uses the hostname configured through
// NEXT_TEST_DEPLOYMENT_HOST or the provider's deployment environment.
return next.fetch(pathname)
}

return next.fetch(pathname, {
headers: { host },
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const deploymentHost =
process.env.NEXT_TEST_DEPLOYMENT_HOST ||
(process.env.VERCEL === '1' ? process.env.VERCEL_URL : undefined)

const nextConfig = {
i18n: {
locales: ['en-US', 'nl-NL'],
Expand All @@ -12,7 +16,7 @@ const nextConfig = {
defaultLocale: 'en-US',
},
{
domain: 'nl.example.local',
domain: deploymentHost || 'nl.example.local',
defaultLocale: 'nl-NL',
},
],
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/app-dir/i18n-app-pages-domain-routing/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'

const deploymentHost =
process.env.NEXT_TEST_DEPLOYMENT_HOST ||
(process.env.VERCEL === '1' ? process.env.VERCEL_URL : undefined)

const localeByHost = new Map([
['en.example.local', 'en-US'],
['nl.example.local', 'nl-NL'],
])

if (deploymentHost) {
localeByHost.set(deploymentHost, 'nl-NL')
}

export function proxy(request: NextRequest) {
const hostname = request.headers.get('host')?.split(':', 1)[0] || ''
const locale = localeByHost.get(hostname) || 'en-US'
Expand Down
45 changes: 45 additions & 0 deletions test/production/adapter-config-i18n/adapter-config-i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,51 @@ describe('adapter config with i18n routes', () => {
)
})

it('keeps dynamic App routes in the literal namespace', async () => {
const { outputs, routing }: Parameters<NextAdapter['onBuildComplete']>[0] =
await next.readJSON('build-complete.json')

const appPageOutput = outputs.appPages.find(
(output) => output.pathname === '/[lang]'
)
const appRouteOutput = outputs.appRoutes.find(
(output) => output.pathname === '/[lang]/endpoint'
)
const appPageRoute = routing.dynamicRoutes.find(
(route) => route.source === '/[lang]'
)
const appRoute = routing.dynamicRoutes.find(
(route) => route.source === '/[lang]/endpoint'
)

expect(appPageOutput).toBeDefined()
expect(appRouteOutput).toBeDefined()

expect(appPageRoute).toBeDefined()
expect(appPageRoute?.sourceRegex).not.toContain('nextLocale')
expect(appPageRoute?.destination).toBe('/[lang]?nxtPlang=$nxtPlang')

expect(appRoute).toBeDefined()
expect(appRoute?.sourceRegex).not.toContain('nextLocale')
expect(appRoute?.destination).toBe('/[lang]/endpoint?nxtPlang=$nxtPlang')
})

it('links prerenders with locale-like App segments to their App output', async () => {
const { outputs }: Parameters<NextAdapter['onBuildComplete']>[0] =
await next.readJSON('build-complete.json')

const appPageOutput = outputs.appPages.find(
(output) => output.pathname === '/fr/static'
)
const prerenderOutput = outputs.prerenders.find(
(output) => output.pathname === '/fr/static'
)

expect(appPageOutput).toBeDefined()
expect(prerenderOutput).toBeDefined()
expect(prerenderOutput?.parentOutputId).toBe(appPageOutput?.id)
})

it('does not emit outputs multiple times for a given pathname', async () => {
const { outputs }: Parameters<NextAdapter['onBuildComplete']>[0] =
await next.readJSON('build-complete.json')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function GET(
_request: Request,
{ params }: { params: Promise<{ lang: string }> }
) {
return Response.json({ lang: (await params).lang })
}
7 changes: 7 additions & 0 deletions test/production/adapter-config-i18n/app/[lang]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default async function Page({
params,
}: {
params: Promise<{ lang: string }>
}) {
return <p>App page: {(await params).lang}</p>
}
3 changes: 3 additions & 0 deletions test/production/adapter-config-i18n/app/fr/static/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>static App page</p>
}
11 changes: 11 additions & 0 deletions test/production/adapter-config-i18n/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>{children}</body>
</html>
)
}
Loading