Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow custom app routes for metadata conventions #71153

Merged
merged 7 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
11 changes: 9 additions & 2 deletions packages/next/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ import {
isInternalComponent,
isNonRoutePagesPage,
} from '../lib/is-internal-component'
import { isMetadataRoute } from '../lib/metadata/is-metadata-route'
import {
isMetadataRoute,
isMetadataRouteFile,
} from '../lib/metadata/is-metadata-route'
import { RouteKind } from '../server/route-kind'
import { encodeToBase64 } from './webpack/loaders/utils'
import { normalizeCatchAllRoutes } from './normalize-catchall-routes'
Expand Down Expand Up @@ -267,7 +270,11 @@ export async function createPagesMapping({

let route = pagesType === 'app' ? normalizeMetadataRoute(pageKey) : pageKey

if (isMetadataRoute(route) && pagesType === 'app') {
if (
isMetadataRoute(route) &&
pagesType === 'app' &&
isMetadataRouteFile(pagePath, pageExtensions, true)
huozhi marked this conversation as resolved.
Show resolved Hide resolved
) {
const filePath = join(appDir!, pagePath)
const staticInfo = await getPageStaticInfo({
nextConfig: {},
Expand Down
14 changes: 11 additions & 3 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { generateEncryptionKeyBase64 } from '../app-render/encryption-utils-serv
import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-definition'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { getNodeDebugType } from '../lib/utils'
import { isMetadataRouteFile } from '../../lib/metadata/is-metadata-route'
// import { getSupportedBrowsers } from '../../build/utils'

const wsServer = new ws.Server({ noServer: true })
Expand Down Expand Up @@ -936,10 +937,17 @@ export async function createHotReloaderTurbopack(
}

const isInsideAppDir = routeDef.bundlePath.startsWith('app/')
const normalizedAppPage = normalizedPageToTurbopackStructureRoute(
page,
extname(routeDef.filename)
const isEntryMetadataRouteFile = isMetadataRouteFile(
routeDef.filename.replace(opts.appDir || '', ''),
nextConfig.pageExtensions,
true
)
const normalizedAppPage = isEntryMetadataRouteFile
? normalizedPageToTurbopackStructureRoute(
page,
extname(routeDef.filename)
)
: page

const route = isInsideAppDir
? currentEntrypoints.app.get(normalizedAppPage)
Expand Down
48 changes: 24 additions & 24 deletions packages/next/src/server/dev/turbopack/manifest-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ type TurbopackMiddlewareManifest = MiddlewareManifest & {
instrumentation?: InstrumentationDefinition
}

const getManifestPath = (page: string, distDir: string, name: string, type: string) => {
let manifestPath = posix.join(
distDir,
`server`,
type,
type === 'middleware' || type === 'instrumentation'
? ''
: type === 'app'
? page
: getAssetPathFromRoute(page),
name
)
return manifestPath
}

async function readPartialManifest<T>(
distDir: string,
name:
Expand All @@ -70,34 +85,19 @@ async function readPartialManifest<T>(
pageName: string,
type: 'pages' | 'app' | 'middleware' | 'instrumentation' = 'pages'
): Promise<T> {
const page = pageName.replace(/\/sitemap\/route$/, '/sitemap.xml/route')
const page = pageName
const isSitemapRoute = /[\\/]sitemap(.xml)?\/route$/.test(page)
let manifestPath = getManifestPath(page, distDir, name, type)

let manifestPath = posix.join(
distDir,
`server`,
type,
type === 'middleware' || type === 'instrumentation'
? ''
: type === 'app'
? page
: getAssetPathFromRoute(page),
name
)
// Check the ambiguity of /sitemap and /sitemap.xml
if (isSitemapRoute && !existsSync(manifestPath)) {
manifestPath = getManifestPath(pageName.replace(/\/sitemap\/route$/, '/sitemap.xml/route'), distDir, name, type)
}
// existsSync is faster than using the async version
if(!existsSync(manifestPath) && page.endsWith('/route')) {
// TODO: Improve implementation of metadata routes, currently it requires this extra check for the variants of the files that can be written.
const metadataPage = addRouteSuffix(addMetadataIdToRoute(removeRouteSuffix(page.replace(/\/sitemap\.xml\/route$/, '/sitemap/route'))))
manifestPath = posix.join(
distDir,
`server`,
type,
type === 'middleware' || type === 'instrumentation'
? ''
: type === 'app'
? metadataPage
: getAssetPathFromRoute(metadataPage),
name
)
let metadataPage = addRouteSuffix(addMetadataIdToRoute(removeRouteSuffix(page)))
manifestPath = getManifestPath(metadataPage, distDir, name, type)
}
return JSON.parse(await readFile(posix.join(manifestPath), 'utf-8')) as T
}
Expand Down
15 changes: 13 additions & 2 deletions packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ import {
ModuleBuildError,
TurbopackInternalError,
} from '../../dev/turbopack-utils'
import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route'
import {
isMetadataRoute,
isMetadataRouteFile,
} from '../../../lib/metadata/is-metadata-route'
import { normalizeMetadataPageToRoute } from '../../../lib/metadata/get-metadata-route'
import { createEnvDefinitions } from '../experimental/create-env-definitions'
import { JsConfigPathsPlugin } from '../../../build/webpack/plugins/jsconfig-paths-plugin'
Expand Down Expand Up @@ -429,7 +432,15 @@ async function startWatcher(opts: SetupOpts) {
pagesType: isAppPath ? PAGE_TYPES.APP : PAGE_TYPES.PAGES,
})

if (isAppPath && isMetadataRoute(pageName)) {
if (
isAppPath &&
huozhi marked this conversation as resolved.
Show resolved Hide resolved
huozhi marked this conversation as resolved.
Show resolved Hide resolved
isMetadataRoute(pageName) &&
isMetadataRouteFile(
fileName.replace(appDir!, ''),
nextConfig.pageExtensions,
true
)
) {
const staticInfo = await getPageStaticInfo({
pageFilePath: fileName,
nextConfig: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import { isAppRouteRoute } from '../../../lib/is-app-route-route'
import { DevAppNormalizers } from '../../normalizers/built/app'
import {
isMetadataRoute,
isMetadataRouteFile,
isStaticMetadataRoute,
} from '../../../lib/metadata/is-metadata-route'
import { normalizeMetadataPageToRoute } from '../../../lib/metadata/get-metadata-route'
import path from '../../../shared/lib/isomorphic/path'

export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvider<AppRouteRouteMatcher> {
private readonly normalizers: {
page: Normalizer
pathname: Normalizer
bundlePath: Normalizer
}
private readonly appDir: string

constructor(
appDir: string,
Expand All @@ -25,6 +28,7 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid
) {
super(appDir, reader)

this.appDir = appDir
this.normalizers = new DevAppNormalizers(appDir, extensions)
}

Expand All @@ -43,8 +47,18 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid

const pathname = this.normalizers.pathname.normalize(filename)
const bundlePath = this.normalizers.bundlePath.normalize(filename)
const ext = path.extname(filename).slice(1)
const isEntryMetadataRouteFile = isMetadataRouteFile(
filename.replace(this.appDir, ''),
[ext],
true
)

if (isMetadataRoute(page) && !isStaticMetadataRoute(page)) {
if (
isMetadataRoute(page) &&
!isStaticMetadataRoute(page) &&
isEntryMetadataRouteFile
) {
// Matching dynamic metadata routes.
// Add 2 possibilities for both single and multiple routes:
{
Expand All @@ -54,12 +68,12 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid
// We'll map the filename before normalization:
// sitemap.ts -> sitemap.xml/route.ts
// icon.ts -> icon/route.ts
const metadataPage = normalizeMetadataPageToRoute(page, false) // this.normalizers.page.normalize(dummyFilename)
const metadataPathname = normalizeMetadataPageToRoute(pathname, false) // this.normalizers.pathname.normalize(dummyFilename)
const metadataPage = normalizeMetadataPageToRoute(page, false)
const metadataPathname = normalizeMetadataPageToRoute(pathname, false)
const metadataBundlePath = normalizeMetadataPageToRoute(
bundlePath,
false
) // this.normalizers.bundlePath.normalize(dummyFilename)
)

const matcher = new AppRouteRouteMatcher({
kind: RouteKind.APP_ROUTE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
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,20 @@
// custom /sitemap route, with xml content
export function GET() {
return new Response(
`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com</loc>
<lastmod>2021-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
`.trim(),
{
headers: {
'Content-Type': 'application/xml',
},
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { nextTestSetup } from 'e2e-utils'

describe('app-dir - metadata-non-standard-custom-routes', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('should work with custom sitemap route', async () => {
const res = await next.fetch('/sitemap')
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toBe('application/xml')
expect(await res.text()).toMatchInlineSnapshot(`
"<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com</loc>
<lastmod>2021-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
</urlset>"
`)
})
})
Loading