feat: upgrade to Next.js 16 (#8615)

* feat: upgrade to Next.js 16

* chore: deps

* fix

* fix

* fix

* fix: workaround zod 4 for now

* fix

* fix: copy button

* fix: update apps/v4/hooks/use-is-mac.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix

* fix: remove

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
shadcn
2025-10-29 13:37:41 +04:00
committed by GitHub
parent c0329c86b9
commit 02d5ce85ec
26 changed files with 9625 additions and 5686 deletions

View File

@@ -27,10 +27,10 @@ jobs:
with:
version: 9.0.6
- name: Use Node.js 18
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
cache: "pnpm"
- name: Install NPM Dependencies

View File

@@ -23,11 +23,11 @@ jobs:
with:
version: 9.0.6
- name: Use Node.js 18
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
version: 9.0.6
node-version: 18
node-version: 20
cache: "pnpm"
- name: Install NPM Dependencies

View File

@@ -19,7 +19,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm

View File

@@ -6,7 +6,9 @@ import {
IconArrowRight,
IconArrowUpRight,
} from "@tabler/icons-react"
import { findNeighbour } from "fumadocs-core/server"
import fm from "front-matter"
import { findNeighbour } from "fumadocs-core/page-tree"
import z from "zod"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
@@ -25,7 +27,7 @@ export function generateStaticParams() {
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>
params: Promise<{ slug: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
@@ -73,7 +75,7 @@ export async function generateMetadata(props: {
}
export default async function Page(props: {
params: Promise<{ slug?: string[] }>
params: Promise<{ slug: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
@@ -82,12 +84,21 @@ export default async function Page(props: {
}
const doc = page.data
// @ts-expect-error - revisit fumadocs types.
const MDX = doc.body
const neighbours = await findNeighbour(source.pageTree, page.url)
const neighbours = findNeighbour(source.pageTree, page.url)
// @ts-expect-error - revisit fumadocs types.
const links = doc.links
const raw = await page.data.getText("raw")
const { attributes } = fm(raw)
const { links } = z
.object({
links: z
.object({
doc: z.string().optional(),
api: z.string().optional(),
})
.optional(),
})
.parse(attributes)
return (
<div
@@ -104,11 +115,7 @@ export default async function Page(props: {
{doc.title}
</h1>
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
<DocsCopyPage
// @ts-expect-error - revisit fumadocs types.
page={doc.content}
url={absoluteUrl(page.url)}
/>
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
{neighbours.previous && (
<Button
variant="secondary"
@@ -195,10 +202,8 @@ export default async function Page(props: {
</div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0" />
{/* @ts-expect-error - revisit fumadocs types. */}
{doc.toc?.length ? (
<div className="no-scrollbar overflow-y-auto px-8">
{/* @ts-expect-error - revisit fumadocs types. */}
<DocsTableOfContents toc={doc.toc} />
<div className="h-12" />
</div>

View File

@@ -8,7 +8,7 @@ export const revalidate = false
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const slug = (await params).slug
const page = source.getPage(slug)
@@ -17,8 +17,7 @@ export async function GET(
notFound()
}
// @ts-expect-error - revisit fumadocs types.
const processedContent = processMdxForLLMs(page.data.content)
const processedContent = processMdxForLLMs(await page.data.getText("raw"))
return new NextResponse(processedContent, {
headers: {

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/static-components */
import * as React from "react"
import { Metadata } from "next"
import { notFound } from "next/navigation"

View File

@@ -13,7 +13,6 @@ import { showMcpDocs } from "@/lib/flags"
import { source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { useIsMac } from "@/hooks/use-is-mac"
import { useMutationObserver } from "@/hooks/use-mutation-observer"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -50,7 +49,6 @@ export function CommandMenu({
navItems?: { href: string; label: string }[]
}) {
const router = useRouter()
const isMac = useIsMac()
const [config] = useConfig()
const [open, setOpen] = React.useState(false)
const [selectedType, setSelectedType] = React.useState<
@@ -206,7 +204,7 @@ export function CommandMenu({
<span className="inline-flex lg:hidden">Search...</span>
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex">
<KbdGroup>
<Kbd className="border">{isMac ? "⌘" : "Ctrl"}</Kbd>
<Kbd className="border"></Kbd>
<Kbd className="border">K</Kbd>
</KbdGroup>
</div>
@@ -404,7 +402,7 @@ export function CommandMenu({
<>
<Separator orientation="vertical" className="!h-4" />
<div className="flex items-center gap-1">
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
<CommandMenuKbd></CommandMenuKbd>
<CommandMenuKbd>C</CommandMenuKbd>
{copyPayload}
</div>

View File

@@ -4,7 +4,7 @@ import { Fragment } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useBreadcrumb } from "fumadocs-core/breadcrumb"
import type { PageTree } from "fumadocs-core/server"
import type { Root } from "fumadocs-core/page-tree"
import {
Breadcrumb,
@@ -19,7 +19,7 @@ export function DocsBreadcrumb({
tree,
className,
}: {
tree: PageTree.Root
tree: Root
className?: string
}) {
const pathname = usePathname()

View File

@@ -1,21 +1,25 @@
import { dirname } from "path"
import { fileURLToPath } from "url"
import { FlatCompat } from "@eslint/eslintrc"
import { defineConfig, globalIgnores } from "eslint/config"
import nextVitals from "eslint-config-next/core-web-vitals"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
const eslintConfig = [
...compat.config({
extends: ["next/core-web-vitals", "next/typescript"],
const eslintConfig = defineConfig([
...nextVitals,
globalIgnores([
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
".source/**",
]),
{
rules: {
"@next/next/no-duplicate-head": "off",
"react-hooks/incompatible-library": "off",
"react-hooks/purity": "off",
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unused-vars": "off",
},
}),
]
},
])
export default eslintConfig

View File

@@ -1,11 +0,0 @@
import { useEffect, useState } from "react"
export function useIsMac() {
const [isMac, setIsMac] = useState(true)
useEffect(() => {
setIsMac(navigator.platform.toUpperCase().includes("MAC"))
}, [])
return isMac
}

View File

@@ -61,7 +61,10 @@ const Layout = ({
}
})
const attrs = !value ? ["layout-fixed", "layout-full"] : Object.values(value)
const attrs = React.useMemo(
() => (!value ? ["layout-fixed", "layout-full"] : Object.values(value)),
[value]
)
const applyLayout = React.useCallback(
(layout: Layout) => {

View File

@@ -11,7 +11,7 @@ export function useIsMobile(mobileBreakpoint = 768) {
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < mobileBreakpoint)
return () => mql.removeEventListener("change", onChange)
}, [])
}, [mobileBreakpoint])
return !!isMobile
}

View File

@@ -4,7 +4,7 @@ import { Index } from "@/registry/__index__"
export function processMdxForLLMs(content: string) {
const componentPreviewRegex =
/<ComponentPreview\s+[^>]*name="([^"]+)"[^>]*\/>/g
/<ComponentPreview[\s\S]*?name="([^"]+)"[\s\S]*?\/>/g
return content.replace(componentPreviewRegex, (match, name) => {
try {

View File

@@ -1,7 +1,7 @@
import { docs } from "@/.source"
import { loader } from "fumadocs-core/source"
export const source: ReturnType<typeof loader> = loader({
export const source = loader({
baseUrl: "/docs",
source: docs.toFumadocsSource(),
})

View File

@@ -128,7 +128,6 @@ export const mdxComponents = {
/>
),
img: ({ className, alt, ...props }: React.ComponentProps<"img">) => (
// eslint-disable-next-line @next/next/no-img-element
<img className={cn("rounded-md", className)} alt={alt} {...props} />
),
hr: ({ ...props }: React.ComponentProps<"hr">) => (
@@ -281,7 +280,7 @@ export const mdxComponents = {
}: React.ComponentProps<"img">) => (
<Image
className={cn("mt-6 rounded-md border", className)}
src={src || ""}
src={(src as string) || ""}
width={Number(width)}
height={Number(height)}
alt={alt || ""}

View File

@@ -25,6 +25,9 @@ const nextConfig = {
},
],
},
experimental: {
turbopackFileSystemCacheForDev: true,
},
redirects() {
return [
{

View File

@@ -7,8 +7,8 @@
"dev": "next dev --turbopack --port 4000",
"build": "pnpm --filter=shadcn build && next build",
"start": "next start --port 4000",
"lint": "next lint",
"lint:fix": "next lint --fix",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"typecheck": "tsc --noEmit",
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
@@ -67,22 +67,23 @@
"date-fns": "^4.1.0",
"embla-carousel-autoplay": "8.5.2",
"embla-carousel-react": "8.5.2",
"fumadocs-core": "15.3.1",
"front-matter": "^4.0.2",
"fumadocs-core": "16.0.5",
"fumadocs-docgen": "2.0.0",
"fumadocs-mdx": "11.6.3",
"fumadocs-ui": "15.3.1",
"fumadocs-mdx": "13.0.2",
"fumadocs-ui": "16.0.5",
"input-otp": "^1.4.2",
"jotai": "^2.1.0",
"little-date": "^1.0.0",
"lodash": "^4.17.21",
"lucide-react": "0.474.0",
"motion": "^12.12.1",
"next": "15.3.1",
"next": "16.0.0",
"next-themes": "0.4.6",
"postcss": "^8.5.1",
"react": "19.1.0",
"react": "19.2.0",
"react-day-picker": "^9.7.0",
"react-dom": "19.1.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.62.0",
"react-resizable-panels": "^2.1.7",
"react-textarea-autosize": "^8.5.9",
@@ -95,20 +96,19 @@
"tailwind-merge": "^3.0.1",
"ts-morph": "18.0.0",
"vaul": "1.1.2",
"zod": "^3.24.1"
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@tailwindcss/postcss": "^4",
"@types/lodash": "^4.17.7",
"@types/mdx": "^2.0.13",
"@types/node": "^20",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@typescript-eslint/parser": "^8.31.0",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"eslint-config-next": "16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.1.11",

File diff suppressed because it is too large Load Diff

View File

@@ -100,7 +100,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
)
}
function Tree({ item }: { item: string | any[] }) {
type TreeItem = string | TreeItem[]
function Tree({ item }: { item: TreeItem }) {
const [name, ...items] = Array.isArray(item) ? item : [item]
if (!items.length) {

View File

@@ -3,7 +3,7 @@
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { CheckIcon } from "lucide-react"
import { Controller, useForm } from "react-hook-form"
import { Controller, useForm, useWatch } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
@@ -83,7 +83,10 @@ export default function FormRhfPassword() {
},
})
const password = form.watch("password")
const password = useWatch({
control: form.control,
name: "password",
})
// Calculate password strength.
const metRequirements = passwordRequirements.filter((req) =>

View File

@@ -30,7 +30,8 @@ export default function SheetSide() {
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
Make changes to your profile here. Click save when you&apos;re
done.
</SheetDescription>
</SheetHeader>
<div className="grid gap-4 py-4">

View File

@@ -5,10 +5,10 @@ export default function TypographyTable() {
<thead>
<tr className="even:bg-muted m-0 border-t p-0">
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
King's Treasury
King&apos;s Treasury
</th>
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
People's happiness
People&apos;s happiness
</th>
</tr>
</thead>

View File

@@ -1,10 +1,5 @@
import {
defineConfig,
defineDocs,
frontmatterSchema,
} from "fumadocs-mdx/config"
import { defineConfig, defineDocs } from "fumadocs-mdx/config"
import rehypePrettyCode from "rehype-pretty-code"
import { z } from "zod"
import { transformers } from "@/lib/highlight-code"
@@ -13,8 +8,6 @@ export default defineConfig({
rehypePlugins: (plugins) => {
plugins.shift()
plugins.push([
// TODO: fix the type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rehypePrettyCode as any,
{
theme: {
@@ -32,14 +25,15 @@ export default defineConfig({
export const docs = defineDocs({
dir: "content/docs",
docs: {
schema: frontmatterSchema.extend({
links: z
.object({
doc: z.string().optional(),
api: z.string().optional(),
})
.optional(),
}),
},
// TODO: Fix this when we upgrade to zod v4.
// docs: {
// schema: frontmatterSchema.extend({
// links: z.optional(
// z.object({
// doc: z.string().optional(),
// api: z.string().optional(),
// })
// ),
// }),
// },
})

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -20,8 +24,12 @@
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"react": ["./node_modules/@types/react"]
"@/*": [
"./*"
],
"react": [
"./node_modules/@types/react"
]
}
},
"include": [
@@ -30,7 +38,10 @@
"**/*.tsx",
".next/types/**/*.ts",
"scripts/build-registry.mts",
"next.config.mjs"
"next.config.mjs",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules"
]
}

View File

@@ -89,5 +89,11 @@
"@types/react-dom": "^18.2.22",
"start-server-and-test": "^2.0.12",
"typescript": "^5.5.3"
},
"pnpm": {
"overrides": {
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2"
}
}
}

3981
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff