Skip to content

Commit 16cfe37

Browse files
authored
Merge pull request #212 from ut-code/copilot/update-nextjs-and-eslint-config
Next.js 16への移行
2 parents 9a6fd21 + e7e0cd7 commit 16cfe37

File tree

17 files changed

+354
-278
lines changed

17 files changed

+354
-278
lines changed

app/(docs)/@chat/chat/[chatId]/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from "@/lib/chatHistory";
77
import { getMarkdownSections, getPagesList } from "@/lib/docs";
88
import { ChatAreaContainer, ChatAreaContent } from "./chatArea";
9-
import { unstable_cacheLife, unstable_cacheTag } from "next/cache";
9+
import { cacheLife, cacheTag } from "next/cache";
1010
import { isCloudflare } from "@/lib/detectCloudflare";
1111

1212
export default async function ChatPage({
@@ -56,8 +56,8 @@ export default async function ChatPage({
5656

5757
async function getChatOneFromCache(chatId: string, userId?: string) {
5858
"use cache";
59-
unstable_cacheLife("days");
60-
unstable_cacheTag(cacheKeyForChat(chatId));
59+
cacheLife("days");
60+
cacheTag(cacheKeyForChat(chatId));
6161

6262
if (!userId) {
6363
return null;

app/(docs)/@docs/[lang]/[pageId]/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
PagePath,
1515
PageSlug,
1616
} from "@/lib/docs";
17-
import { unstable_cacheLife, unstable_cacheTag } from "next/cache";
17+
import { cacheLife, cacheTag } from "next/cache";
1818
import { isCloudflare } from "@/lib/detectCloudflare";
1919
import { DocsAutoRedirect } from "./autoRedirect";
2020

@@ -83,12 +83,12 @@ async function getChatFromCache(path: PagePath, userId?: string) {
8383
// 一方、use cacheの関数内でheaders()にはアクセスできない。
8484
// したがって、外でheaders()を使ってuserIdを取得した後、関数の中で再度drizzleを初期化しないといけない。
8585
"use cache";
86-
unstable_cacheLife("days");
86+
cacheLife("days");
8787

8888
if (!userId) {
8989
return [];
9090
}
91-
unstable_cacheTag(cacheKeyForPage(path, userId));
91+
cacheTag(cacheKeyForPage(path, userId));
9292

9393
if (isCloudflare()) {
9494
const cache = await caches.open("chatHistory");

app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
3+
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
44
import { ChatForm } from "./chatForm";
55
import { StyledMarkdown } from "@/markdown/markdown";
66
import { useSidebarMdContext } from "@/sidebar";
@@ -32,11 +32,41 @@ export function PageContent(props: PageContentProps) {
3232
const { setSidebarMdContent } = useSidebarMdContext();
3333
const { splitMdContent, pageEntry, path, chatHistories } = props;
3434

35-
const initDynamicMdContent = useCallback(() => {
35+
const [sectionInView, setSectionInView] = useState<boolean[]>([]);
36+
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
37+
useEffect(() => {
38+
const handleScroll = () => {
39+
setSectionInView((sectionInView) => {
40+
sectionInView = sectionInView.slice(); // Reactの変更検知のために新しい配列を作成
41+
for (
42+
let i = 0;
43+
i < sectionRefs.current.length || i < sectionInView.length;
44+
i++
45+
) {
46+
if (sectionRefs.current.at(i)) {
47+
const rect = sectionRefs.current.at(i)!.getBoundingClientRect();
48+
sectionInView[i] =
49+
rect.top < window.innerHeight * 0.9 &&
50+
rect.bottom >= window.innerHeight * 0.1;
51+
} else {
52+
sectionInView[i] = false;
53+
}
54+
}
55+
return sectionInView;
56+
});
57+
};
58+
window.addEventListener("scroll", handleScroll);
59+
handleScroll();
60+
return () => {
61+
window.removeEventListener("scroll", handleScroll);
62+
};
63+
}, []);
64+
65+
const dynamicMdContent = useMemo(() => {
3666
const newContent: DynamicMarkdownSection[] = splitMdContent.map(
37-
(section) => ({
67+
(section, i) => ({
3868
...section,
39-
inView: false,
69+
inView: sectionInView[i],
4070
replacedContent: section.rawContent,
4171
replacedRange: [],
4272
})
@@ -94,54 +124,13 @@ export function PageContent(props: PageContentProps) {
94124
}
95125

96126
return newContent;
97-
}, [splitMdContent, chatHistories]);
98-
99-
// SSR用のローカルstate
100-
const [dynamicMdContent, setDynamicMdContent] = useState<
101-
DynamicMarkdownSection[]
102-
>(() => initDynamicMdContent());
127+
}, [splitMdContent, chatHistories, sectionInView]);
103128

104129
useEffect(() => {
105130
// props.splitMdContentが変わったとき, チャットのdiffが変わった時に
106-
// ローカルstateとcontextの両方を更新
107-
const newContent = initDynamicMdContent();
108-
setDynamicMdContent(newContent);
109-
setSidebarMdContent(path, newContent);
110-
}, [initDynamicMdContent, path, setSidebarMdContent]);
111-
112-
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
113-
// sectionRefsの長さをsplitMdContentに合わせる
114-
while (sectionRefs.current.length < props.splitMdContent.length) {
115-
sectionRefs.current.push(null);
116-
}
117-
118-
useEffect(() => {
119-
const handleScroll = () => {
120-
const updateContent = (
121-
prevDynamicMdContent: DynamicMarkdownSection[]
122-
) => {
123-
const dynMdContent = prevDynamicMdContent.slice(); // Reactの変更検知のために新しい配列を作成
124-
for (let i = 0; i < sectionRefs.current.length; i++) {
125-
if (sectionRefs.current.at(i) && dynMdContent.at(i)) {
126-
const rect = sectionRefs.current.at(i)!.getBoundingClientRect();
127-
dynMdContent.at(i)!.inView =
128-
rect.top < window.innerHeight * 0.9 &&
129-
rect.bottom >= window.innerHeight * 0.1;
130-
}
131-
}
132-
return dynMdContent;
133-
};
134-
135-
// ローカルstateとcontextの両方を更新
136-
setDynamicMdContent(updateContent);
137-
setSidebarMdContent(path, updateContent);
138-
};
139-
window.addEventListener("scroll", handleScroll);
140-
handleScroll();
141-
return () => {
142-
window.removeEventListener("scroll", handleScroll);
143-
};
144-
}, [setSidebarMdContent, path]);
131+
// sidebarのcontextを更新
132+
setSidebarMdContent(path, dynamicMdContent);
133+
}, [dynamicMdContent, path, setSidebarMdContent]);
145134

146135
const [isFormVisible, setIsFormVisible] = useState(false);
147136

app/lib/chatHistory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getDrizzle } from "./drizzle";
44
import { chat, diff, message, section } from "@/schema/chat";
55
import { and, asc, eq, exists } from "drizzle-orm";
66
import { Auth } from "better-auth";
7-
import { revalidateTag } from "next/cache";
7+
import { updateTag } from "next/cache";
88
import { isCloudflare } from "./detectCloudflare";
99
import { getPagesList, LangId, PagePath, PageSlug, SectionId } from "./docs";
1010

@@ -31,7 +31,6 @@ export function cacheKeyForChat(chatId: string) {
3131
// nextjsのキャッシュのrevalidateはRouteHandlerではなくServerActionから呼ばないと正しく動作しないらしい。
3232
// https://github.com/vercel/next.js/issues/69064
3333
// そのためlib/以下の関数では直接revalidateChatを呼ばず、ServerActionの関数から呼ぶようにする。
34-
// Nextjs 16 に更新したらこれをupdateTag()で置き換える。
3534
export async function revalidateChat(
3635
chatId: string,
3736
userId: string,
@@ -41,8 +40,8 @@ export async function revalidateChat(
4140
const [lang, page] = pagePath.split("/") as [LangId, PageSlug];
4241
pagePath = { lang, page };
4342
}
44-
revalidateTag(cacheKeyForChat(chatId));
45-
revalidateTag(cacheKeyForPage(pagePath, userId));
43+
updateTag(cacheKeyForChat(chatId));
44+
updateTag(cacheKeyForPage(pagePath, userId));
4645
if (isCloudflare()) {
4746
const cache = await caches.open("chatHistory");
4847
await cache.delete(cacheKeyForChat(chatId));
@@ -283,7 +282,7 @@ export async function migrateChatUser(oldUserId: string, newUserId: string) {
283282
const pagesList = await getPagesList();
284283
for (const lang of pagesList) {
285284
for (const page of lang.pages) {
286-
revalidateTag(
285+
updateTag(
287286
cacheKeyForPage({ lang: lang.id, page: page.slug }, newUserId)
288287
);
289288
}

app/lib/clientOnly.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use client";
2+
3+
import { useSyncExternalStore } from "react";
4+
5+
// --- SSR無効化のためのカスタムフック準備 ---
6+
const subscribe = () => () => {};
7+
const getSnapshot = () => true; // クライアントでは true
8+
const getServerSnapshot = () => false; // サーバーでは false
9+
10+
export function useIsClient() {
11+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
12+
}

app/markdown/styledSyntaxHighlighter.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {
55
tomorrow,
66
tomorrowNight,
77
} from "react-syntax-highlighter/dist/esm/styles/hljs";
8-
import { lazy, Suspense, useEffect, useState } from "react";
8+
import { lazy, Suspense } from "react";
99
import { LangConstants } from "@my-code/runtime/languages";
1010
import clsx from "clsx";
11+
import { useIsClient } from "@/lib/clientOnly";
1112

1213
// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
1314
const SyntaxHighlighter = lazy(() => {
@@ -24,11 +25,8 @@ export function StyledSyntaxHighlighter(props: {
2425
}) {
2526
const theme = useChangeTheme();
2627
const codetheme = theme === "tomorrow" ? tomorrow : tomorrowNight;
27-
const [initHighlighter, setInitHighlighter] = useState(false);
28-
useEffect(() => {
29-
setInitHighlighter(true);
30-
}, []);
31-
return initHighlighter ? (
28+
const isClient = useIsClient();
29+
return isClient ? (
3230
<Suspense fallback={<FallbackPre>{props.children}</FallbackPre>}>
3331
<SyntaxHighlighter
3432
language={props.language.rsh}

app/sidebar.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
ReactNode,
1616
useCallback,
1717
useContext,
18-
useEffect,
1918
useState,
2019
} from "react";
2120
import clsx from "clsx";
@@ -100,22 +99,17 @@ export function Sidebar({ pagesList }: { pagesList: LanguageEntry[] }) {
10099

101100
// 目次の開閉状態
102101
const [detailsOpen, setDetailsOpen] = useState<boolean[]>([]);
103-
const currentLangIndex = pagesList.findIndex(
104-
(group) => currentLang === group.id
105-
);
106-
useEffect(() => {
107-
// 表示しているグループが変わったときに現在のグループのdetailsを開く
108-
if (currentLangIndex !== -1) {
109-
setDetailsOpen((detailsOpen) => {
110-
const newDetailsOpen = [...detailsOpen];
111-
while (newDetailsOpen.length <= currentLangIndex) {
112-
newDetailsOpen.push(false);
113-
}
114-
newDetailsOpen[currentLangIndex] = true;
115-
return newDetailsOpen;
116-
});
117-
}
118-
}, [currentLangIndex]);
102+
const [prevLangIndex, setPrevLangIndex] = useState<number>(-1);
103+
const langIndex = pagesList.findIndex((group) => currentLang === group.id);
104+
// 表示しているグループが変わったときに現在のグループのdetailsを開く
105+
if (prevLangIndex !== langIndex) {
106+
setPrevLangIndex(langIndex);
107+
setDetailsOpen((detailsOpen) => {
108+
const newDetailsOpen = [...detailsOpen];
109+
newDetailsOpen[langIndex] = true;
110+
return newDetailsOpen;
111+
});
112+
}
119113

120114
return (
121115
<div className="bg-base-200 h-full w-sidebar flex flex-col relative">

app/terminal/embedContext.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
ReactNode,
88
useCallback,
99
useContext,
10-
useEffect,
1110
useState,
1211
} from "react";
1312

@@ -58,7 +57,7 @@ export function useEmbedContext() {
5857
export function EmbedContextProvider({ children }: { children: ReactNode }) {
5958
const pathname = usePathname();
6059

61-
const [currentPathname, setCurrentPathname] = useState<PagePathname>("");
60+
const [prevPathname, setPrevPathname] = useState<PagePathname>("");
6261
const [files, setFiles] = useState<
6362
Record<PagePathname, Record<Filename, string>>
6463
>({});
@@ -72,15 +71,12 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
7271
const [execResults, setExecResults] = useState<
7372
Record<Filename, ReplOutput[]>
7473
>({});
75-
// pathnameが変わったらデータを初期化
76-
useEffect(() => {
77-
if (pathname && pathname !== currentPathname) {
78-
setCurrentPathname(pathname);
79-
setReplOutputs({});
80-
setCommandIdCounters({});
81-
setExecResults({});
82-
}
83-
}, [pathname, currentPathname]);
74+
if (pathname && pathname !== prevPathname) {
75+
setPrevPathname(pathname);
76+
setReplOutputs({});
77+
setCommandIdCounters({});
78+
setExecResults({});
79+
}
8480

8581
const writeFile = useCallback(
8682
(updatedFiles: Record<Filename, string>) => {

app/terminal/exec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ export function ExecFile(props: ExecProps) {
7777
>("idle");
7878
useEffect(() => {
7979
if (executionState === "triggered" && ready) {
80-
setExecutionState("executing");
8180
(async () => {
81+
setExecutionState("executing");
8282
clearTerminal(terminalInstanceRef.current!);
8383
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
8484
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる

app/terminal/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Heading } from "@/markdown/heading";
44
import "mocha/mocha.css";
5-
import { Fragment, useEffect, useState } from "react";
5+
import { Fragment, useEffect, useRef, useState } from "react";
66
import { langConstants, RuntimeLang } from "@my-code/runtime/languages";
77
import { ReplTerminal } from "./repl";
88
import { EditorComponent } from "./editor";
@@ -202,7 +202,11 @@ function AnsiColorSample() {
202202
}
203203

204204
function MochaTest() {
205-
const runtimeRef = useRuntimeAll();
205+
const runtimeAll = useRuntimeAll();
206+
const runtimeRef = useRef(runtimeAll);
207+
for (const lang of Object.keys(runtimeAll) as RuntimeLang[]) {
208+
runtimeRef.current[lang] = runtimeAll[lang];
209+
}
206210

207211
const [searchParams, setSearchParams] = useState<string>("");
208212
useEffect(() => {

0 commit comments

Comments
 (0)