Skip to content

Commit

Permalink
fix: allow custom app routes for metadata conventions (#71153)
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi authored Oct 11, 2024
1 parent 2d068a9 commit d8c0539
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 36 deletions.
7 changes: 5 additions & 2 deletions packages/next/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import {
isInternalComponent,
isNonRoutePagesPage,
} from '../lib/is-internal-component'
import { isMetadataRoute } from '../lib/metadata/is-metadata-route'
import { 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 +267,10 @@ export async function createPagesMapping({

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

if (isMetadataRoute(route) && pagesType === 'app') {
if (
pagesType === 'app' &&
isMetadataRouteFile(pagePath, pageExtensions, true)
) {
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
12 changes: 10 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,7 @@ import {
ModuleBuildError,
TurbopackInternalError,
} from '../../dev/turbopack-utils'
import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route'
import { 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 +429,15 @@ async function startWatcher(opts: SetupOpts) {
pagesType: isAppPath ? PAGE_TYPES.APP : PAGE_TYPES.PAGES,
})

if (isAppPath && isMetadataRoute(pageName)) {
if (
isAppPath &&
appDir &&
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 @@ -6,17 +6,19 @@ import { FileCacheRouteMatcherProvider } from './file-cache-route-matcher-provid
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 +27,7 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid
) {
super(appDir, reader)

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

Expand All @@ -43,8 +46,14 @@ 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 (!isStaticMetadataRoute(page) && isEntryMetadataRouteFile) {
// Matching dynamic metadata routes.
// Add 2 possibilities for both single and multiple routes:
{
Expand All @@ -54,12 +63,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>"
`)
})
})

0 comments on commit d8c0539

Please sign in to comment.