upgrade: Next.js 16 + React 19, codemod transforms (async APIs, middleware→proxy, ESLint flat config)

This commit is contained in:
parsa aghaei 2026-06-28 11:41:00 +03:30
parent c496863180
commit 7533f8c97c
27 changed files with 2423 additions and 1257 deletions

View File

@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

12
eslint.config.mjs Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from "eslint/config";
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig([{
extends: [...nextCoreWebVitals, ...nextTypescript],
}]);

View File

@ -5,6 +5,7 @@ import withBundleAnalyzer from '@next/bundle-analyzer';
const nextConfig = {
output: 'standalone',
reactStrictMode: false,
turbopack: {},
images: {
remotePatterns: [
{

3517
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"dev": "start cmd.exe /k \"C:/laragon/bin/mysql/mysql-8.0.30-winx64/bin/mysqld.exe\" && start cmd.exe /k \"cd C:/Users/p.aghaei/Desktop/Work/parsa/parsaaghayi/backend && php artisan serve\" && start cmd.exe /k \"cd C:/Users/p.aghaei/Desktop/Work/parsa/parsaaghayi/frontend && next dev --port 3001\"",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"format": "prettier --write .",
"analyze": "cross-env ANALYZE=true next build",
"test": "vitest run",
@ -15,33 +15,37 @@
},
"dependencies": {
"cookies-next": "^4.3.0",
"next": "14.2.11",
"next": "16.2.9",
"nextjs-toploader": "^3.7.15",
"react": "^18",
"react-dom": "^18",
"react": "19.2.7",
"react-dom": "19.2.7",
"react-loading-skeleton": "^3.5.0",
"sharp": "^0.33.5",
"tailwindcss": "^0.0.0-insiders.e8614a2"
},
"devDependencies": {
"@next/bundle-analyzer": "^14.2.13",
"@next/bundle-analyzer": "16.2.9",
"@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"cross-env": "^7.0.3",
"cypress": "^15.17.0",
"eslint": "^8",
"eslint": "^9",
"jsdom": "^29.1.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@vitejs/plugin-react": "^6.0.3",
"vitest": "^4.1.9",
"eslint-config-next": "14.2.11",
"eslint-config-next": "16.2.9",
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"typescript": "^5",
"webpack-bundle-analyzer": "^4.10.2"
},
"overrides": {
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3"
}
}

View File

@ -22,7 +22,7 @@ export const metadata: Metadata = {
};
export default async function Home() {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
const socialLinks = [
{ href: "#", src: "/images/icons/facebook.svg", alt: "facebook" },
{ href: "#", src: "/images/icons/twitter.svg", alt: "twitter" },

View File

@ -2,12 +2,13 @@ import Image from "next/image";
import Link from "next/link";
interface PropsType {
params: {
params: Promise<{
notFound: string[];
};
}>;
}
export default function NotFound({ params }: PropsType) {
export default async function NotFound(props: PropsType) {
const params = await props.params;
const imageUrl = `/images/404/${Math.floor(Math.random() * 5) + 1}.jpg`;
return (

View File

@ -6,7 +6,7 @@ import "react-loading-skeleton/dist/skeleton.css";
export default async function AboutMe() {
const aboutMeData: AboutMeDataType = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/website/about-me/1/translation/${cookies().get("selectedLocale")?.value ? cookies().get("selectedLocale")?.value : cookies().get("defaultLocale")?.value}`,
`${process.env.NEXT_PUBLIC_API_URL}/website/about-me/1/translation/${(await cookies()).get("selectedLocale")?.value ? (await cookies()).get("selectedLocale")?.value : (await cookies()).get("defaultLocale")?.value}`,
).then((res) => res.json());
return (

View File

@ -5,7 +5,7 @@ import { getDefaultLocale } from "@/app/lib/functions/utils";
import Link from "next/link";
export default async function Footer() {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="flex w-full flex-col">
<div className="flex flex-col items-center gap-[50px] bg-[#F8F8F8] py-[50px]">

View File

@ -7,7 +7,7 @@ import { getDictionary } from "@/app/lib/functions/dictionaries";
import { getDefaultLocale } from "@/app/lib/functions/utils";
export default async function Header() {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div
@ -66,8 +66,8 @@ export default async function Header() {
{dict.website.header.Blog}
</Link>
<SelectLanguage
selectedLocale={cookies().get("selectedLocale")}
defaultLocale={cookies().get("defaultLocale")}
selectedLocale={(await cookies()).get("selectedLocale")}
defaultLocale={(await cookies()).get("defaultLocale")}
/>
</ul>
<div className="flex gap-1">

View File

@ -2,12 +2,13 @@ import Image from "next/image";
import Link from "next/link";
interface PropsType {
params: {
params: Promise<{
notFound: string[];
};
}>;
}
export default function NotFound({ params }: PropsType) {
export default async function NotFound(props: PropsType) {
const params = await props.params;
const imageUrl = `/images/404/${Math.floor(Math.random() * 5) + 1}.jpg`;
return (

View File

@ -26,9 +26,8 @@ async function fetchCategoryData(
return res.json();
}
export async function generateMetadata({
params,
}: categorySlugPropsType): Promise<Metadata> {
export async function generateMetadata(props: categorySlugPropsType): Promise<Metadata> {
const params = await props.params;
const categoryData: CategoryDataType = await fetchCategoryData(
params.categorySlug,
new URLSearchParams(),
@ -45,11 +44,10 @@ export async function generateMetadata({
};
}
export default async function SingleCategory({
params,
searchParams,
}: categorySlugPropsType) {
const dict = await getDictionary(getDefaultLocale());
export default async function SingleCategory(props: categorySlugPropsType) {
const searchParams = await props.searchParams;
const params = await props.params;
const dict = await getDictionary(await getDefaultLocale());
const categoryData: CategoryDataType = await fetchCategoryData(
params.categorySlug,

View File

@ -25,7 +25,8 @@ export const metadata: Metadata = {
},
};
export default async function Categories({ searchParams }: categoryPropsType) {
export default async function Categories(props: categoryPropsType) {
const searchParams = await props.searchParams;
const CategoriesData: CategoriesListType = await fetch(
`${process.env.NEXT_PUBLIC_BLOG_API_URL}/categories?per_page=${searchParams.per_page ? searchParams.per_page : process.env.NEXT_PUBLIC_CATEGORIES_PER_CATEGORY_PAGE}${searchParams.page ? `&page=${searchParams.page}` : ""}`,
{
@ -33,7 +34,7 @@ export default async function Categories({ searchParams }: categoryPropsType) {
},
).then((res) => res.json());
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="my-10 flex w-full max-w-[1280px] flex-wrap justify-center">

View File

@ -5,7 +5,7 @@ import Image from "next/image";
import React from "react";
export default async function Footer() {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="absolute bottom-0 flex w-full flex-wrap items-center justify-around gap-5 bg-white px-1 py-5">
<Image

View File

@ -10,7 +10,7 @@ import { getDictionary } from "@/app/lib/functions/dictionaries";
import { getDefaultLocale } from "@/app/lib/functions/utils";
export default async function Header() {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="fixed z-50 flex w-full items-center justify-center bg-blog-background py-10">
@ -29,8 +29,8 @@ export default async function Header() {
<NavLinks className="hidden md:flex" dict={dict} />
<SelectLanguage
className="hidden md:flex"
selectedLocale={cookies().get("selectedLocale")}
defaultLocale={cookies().get("defaultLocale")}
selectedLocale={(await cookies()).get("selectedLocale")}
defaultLocale={(await cookies()).get("defaultLocale")}
/>
</div>
<div className="flex items-center justify-center gap-1 sm:gap-5">

View File

@ -9,7 +9,7 @@ interface CategoriesProps {
}
export default async function Categories({ data }: CategoriesProps) {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="mb-5 flex w-full max-w-[95%] flex-wrap gap-10">
<div className="flex w-full items-center justify-between">

View File

@ -9,7 +9,7 @@ interface FilteredPostsProps {
}
export default async function FilteredPosts({ data }: FilteredPostsProps) {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="mb-5 flex max-w-[95%] flex-wrap gap-10">
<div className="flex w-full items-center justify-between">

View File

@ -9,7 +9,7 @@ interface NewProps {
}
export default async function New({ data }: NewProps) {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="flex w-full max-w-[95%] flex-wrap justify-between gap-5">
<div className="relative flex h-[300px] w-full flex-col justify-center overflow-hidden rounded-md border sm:h-[400px] md:h-auto md:w-[60%]">

View File

@ -9,7 +9,7 @@ interface RandomProps {
}
export default async function Random({ data }: RandomProps) {
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
return (
<div className="mb-10 flex w-full max-w-[90%] flex-wrap gap-10">
<div className="flex w-full items-center justify-between">

View File

@ -33,7 +33,7 @@ const Pagination: React.FC<PaginationProps> = ({ meta, baseUrl, dict }) => {
return `${baseUrl}?${params.toString()}`; // استفاده از baseUrl
};
const renderPageNumbers = (): (JSX.Element | string)[] => {
const renderPageNumbers = (): (React.JSX.Element | string)[] => {
const delta = 1; // تعداد صفحات اطراف صفحه فعلی که نمایش داده شوند
const range: number[] = [];
const rangeWithDots: (number | string)[] = [];

View File

@ -23,13 +23,12 @@ export const metadata: Metadata = {
},
};
export default async function FilteredPosts({
params,
searchParams,
}: FilteredPostsPagepropsType) {
export default async function FilteredPosts(props: FilteredPostsPagepropsType) {
const searchParams = await props.searchParams;
const params = await props.params;
let filteredPostApiUrl: string;
const dict = await getDictionary(getDefaultLocale());
const dict = await getDictionary(await getDefaultLocale());
switch (params.filter.replaceAll(/%20|-|_/g, " ").toLowerCase()) {
case "new":

View File

@ -24,9 +24,8 @@ async function fetchPostData(postSlug: string): Promise<PostSlugPagePostType> {
return res.json();
}
export async function generateMetadata({
params,
}: PostSlugPagepropsType): Promise<Metadata> {
export async function generateMetadata(props: PostSlugPagepropsType): Promise<Metadata> {
const params = await props.params;
const post: PostSlugPagePostType = await fetchPostData(params.postSlug);
return {
@ -40,8 +39,9 @@ export async function generateMetadata({
};
}
export default async function SinglePost({ params }: PostSlugPagepropsType) {
const dict = await getDictionary(getDefaultLocale());
export default async function SinglePost(props: PostSlugPagepropsType) {
const params = await props.params;
const dict = await getDictionary(await getDefaultLocale());
const postData: PostSlugPagePostType = await fetchPostData(params.postSlug);
return (

View File

@ -1,7 +1,8 @@
type propsType = {
params: { slug: string };
params: Promise<{ slug: string }>;
};
export default function SingleTag({ params }: propsType) {
export default async function SingleTag(props: propsType) {
const params = await props.params;
return <div>{params.slug}</div>;
}

View File

@ -8,14 +8,15 @@ const mikhak = localFont({
weight: "100 900",
});
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const defaultLocale = await getDefaultLocale();
return (
<html lang={getDefaultLocale()} dir={getDefaultLocale() === "fa" ? "rtl" : "ltr"}>
<html lang={defaultLocale} dir={defaultLocale === "fa" ? "rtl" : "ltr"}>
<body
suppressHydrationWarning={true}
className={`${mikhak.variable} relative flex h-full w-full flex-col items-center justify-between antialiased`}

View File

@ -1,7 +1,8 @@
import { cookies } from "next/headers";
export const getDefaultLocale = () => {
const defaultLocaleCookie = cookies().get("defaultLocale");
export const getDefaultLocale = async () => {
const cookieStore = await cookies();
const defaultLocaleCookie = cookieStore.get("defaultLocale");
const defaultLocale: string = defaultLocaleCookie
? defaultLocaleCookie.value
: "en";

View File

@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
export function proxy(request: NextRequest) {
const response = NextResponse.next();
const acceptLanguage = request.headers.get("accept-language");

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": [
{
@ -19,9 +23,21 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "tailwind.config.ts", "src/middleware.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"tailwind.config.ts",
"src/proxy.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}