Просмотр исходного кода

Merge branch 'support/reclassify-deps' into support/reclassify-deps-with-turbo-prune

Yuki Takei 3 недель назад
Родитель
Сommit
16f23baf9f

+ 3 - 0
.github/workflows/reusable-app-prod.yml

@@ -60,6 +60,9 @@ jobs:
     - name: Assemble production artifacts
     - name: Assemble production artifacts
       run: bash apps/app/bin/assemble-prod.sh
       run: bash apps/app/bin/assemble-prod.sh
 
 
+    - name: Check for broken symlinks in .next/node_modules
+      run: bash apps/app/bin/check-next-symlinks.sh
+
     - name: Archive production files
     - name: Archive production files
       id: archive-prod-files
       id: archive-prod-files
       run: |
       run: |

+ 1 - 1
.kiro/specs/optimise-deps-for-prod/design.md

@@ -340,7 +340,7 @@ bash apps/app/bin/assemble-prod.sh
 
 
 > **注意**:
 > **注意**:
 > - `assemble-prod.sh` は `apps/app/next.config.ts` を削除する。**`next.config.ts` はサーバーテスト完了後(Step 6)に復元すること。** サーバー起動前に復元すると、Next.js が起動時に TypeScript インストールを試みて pnpm install が走り、`apps/app/node_modules` の symlink が上書きされ HTTP 500 となる。
 > - `assemble-prod.sh` は `apps/app/next.config.ts` を削除する。**`next.config.ts` はサーバーテスト完了後(Step 6)に復元すること。** サーバー起動前に復元すると、Next.js が起動時に TypeScript インストールを試みて pnpm install が走り、`apps/app/node_modules` の symlink が上書きされ HTTP 500 となる。
-> - ワークスペースルートの `node_modules` は **削除・リネームしないこと**。workspace パッケージ(`@growi/core` 等)の `node_modules/` 内シンボリックリンクがワークスペースルート `node_modules` を参照しており、削除すると `MODULE_NOT_FOUND` でサーバーが起動しない。Docker 本番環境でも `packages/` ディレクトリごと COPY されるためこれは正常な挙動
+> - `pnpm deploy` はサイドエフェクトとしてワークスペースルートの `node_modules` を再作成する。Docker の release stage では `apps/app/node_modules/` のみ COPY され、ワークスペースルートの `node_modules` は含まれない。`pnpm deploy --prod --legacy` が workspace パッケージ(`@growi/core` 等)を `.pnpm/` ローカルストアの実体ディレクトリとしてデプロイするため、`apps/app/node_modules/` は自己完結している。より正確な production 再現テストを行う場合は `mv node_modules node_modules.bak` でワークスペースルートを退避してからサーバーを起動し、テスト後に `mv node_modules.bak node_modules` で復元すること
 
 
 **Step 3 — プロダクションサーバー起動**(`apps/app/` から実行)
 **Step 3 — プロダクションサーバー起動**(`apps/app/` から実行)
 
 

+ 14 - 13
.kiro/specs/optimise-deps-for-prod/tasks.md

@@ -45,7 +45,7 @@
 
 
 ---
 ---
 
 
-- [ ] 3. Apply `dynamic({ ssr: false })` to eligible Group 1 components
+- [x] 3. Apply `dynamic({ ssr: false })` to eligible Group 1 components
 
 
 - [x] 3.1 (P) Wrap `LightBox.tsx` import with `dynamic({ ssr: false })` and verify `fslightbox-react` leaves `dependencies`
 - [x] 3.1 (P) Wrap `LightBox.tsx` import with `dynamic({ ssr: false })` and verify `fslightbox-react` leaves `dependencies`
   - Replaced static `import FsLightbox from 'fslightbox-react'` in `LightBox.tsx` with `import('fslightbox-react')` inside `useEffect` (true runtime dynamic import, same pattern as socket.io-client in task 4.2)
   - Replaced static `import FsLightbox from 'fslightbox-react'` in `LightBox.tsx` with `import('fslightbox-react')` inside `useEffect` (true runtime dynamic import, same pattern as socket.io-client in task 4.2)
@@ -53,33 +53,34 @@
   - **Validation**: `GET /` → HTTP 200, zero `ERR_MODULE_NOT_FOUND`. Turbopack still creates a `.next/node_modules/fslightbox-react` symlink, but SSR never executes `useEffect`, so the broken symlink is never accessed.
   - **Validation**: `GET /` → HTTP 200, zero `ERR_MODULE_NOT_FOUND`. Turbopack still creates a `.next/node_modules/fslightbox-react` symlink, but SSR never executes `useEffect`, so the broken symlink is never accessed.
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
 
 
-- [ ] 3.2 (P) Wrap `RevisionDiff.tsx` import with `dynamic({ ssr: false })` and verify `diff2html` leaves `dependencies`
+- [x] 3.2 (P) Wrap `RevisionDiff.tsx` import with `dynamic({ ssr: false })` and verify `diff2html` leaves `dependencies`
   - Applied `dynamic({ ssr: false })` in `apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx`
   - Applied `dynamic({ ssr: false })` in `apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx`
   - Moved `diff2html` from `dependencies` to `devDependencies`
   - Moved `diff2html` from `dependencies` to `devDependencies`
   - **GOAL NOT ACHIEVED**: `diff2html` still appears in `.next/node_modules/` after production build. Package was moved back to `dependencies` in task 5.1.
   - **GOAL NOT ACHIEVED**: `diff2html` still appears in `.next/node_modules/` after production build. Package was moved back to `dependencies` in task 5.1.
+  - **Fix**: Restored `import { html } from 'diff2html'` in `RevisionDiff.tsx` (was accidentally removed during refactoring; `html` is safe to import statically because RevisionDiff is loaded client-only via `dynamic({ ssr: false })` in RevisionComparer).
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
 
 
-- [ ] 3.3 (P) Wrap the DnD provider in `PageTree` with `dynamic({ ssr: false })` and verify `react-dnd` / `react-dnd-html5-backend` leave `dependencies`
+- [x] 3.3 (P) Wrap the DnD provider in `PageTree` with `dynamic({ ssr: false })` and verify `react-dnd` / `react-dnd-html5-backend` leave `dependencies`
   - Created `apps/app/src/client/components/Sidebar/PageTree/PageTreeWithDnD.tsx` wrapper
   - Created `apps/app/src/client/components/Sidebar/PageTree/PageTreeWithDnD.tsx` wrapper
   - Updated `PageTree.tsx` to load `PageTreeWithDnD` via `dynamic({ ssr: false })`
   - Updated `PageTree.tsx` to load `PageTreeWithDnD` via `dynamic({ ssr: false })`
   - Moved `react-dnd` and `react-dnd-html5-backend` from `dependencies` to `devDependencies`
   - Moved `react-dnd` and `react-dnd-html5-backend` from `dependencies` to `devDependencies`
   - **GOAL NOT ACHIEVED**: Both packages still appear in `.next/node_modules/` after production build. Moved back to `dependencies` in task 5.1.
   - **GOAL NOT ACHIEVED**: Both packages still appear in `.next/node_modules/` after production build. Moved back to `dependencies` in task 5.1.
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
 
 
-- [ ] 3.4 (P) Confirm `HandsontableModal` already uses a `dynamic` import and verify `@handsontable/react` leave `dependencies`
+- [x] 3.4 (P) Confirm `HandsontableModal` already uses a `dynamic` import and verify `@handsontable/react` leave `dependencies`
   - Confirmed: `HandsontableModal.tsx` is loaded via `useLazyLoader` with `import('./HandsontableModal')` inside `useEffect` — browser-only dynamic import
   - Confirmed: `HandsontableModal.tsx` is loaded via `useLazyLoader` with `import('./HandsontableModal')` inside `useEffect` — browser-only dynamic import
   - Moved `@handsontable/react` from `dependencies` to `devDependencies`
   - Moved `@handsontable/react` from `dependencies` to `devDependencies`
   - **GOAL NOT ACHIEVED**: `@handsontable/react` still appears in `.next/node_modules/` after production build. Moved back to `dependencies` in task 5.1.
   - **GOAL NOT ACHIEVED**: `@handsontable/react` still appears in `.next/node_modules/` after production build. Moved back to `dependencies` in task 5.1.
   - _Requirements: 3.1, 3.2, 3.4, 3.5_
   - _Requirements: 3.1, 3.2, 3.4, 3.5_
 
 
-- [ ] 3.5 Run a consolidated production build verification after all Group 1 wrapping changes
+- [x] 3.5 Run a consolidated production build verification after all Group 1 wrapping changes
   - ~~Ran `assemble-prod.sh` + `pnpm run server`: HTTP 200 on `/login`, no `ERR_MODULE_NOT_FOUND` errors~~ ← **invalid test** (`/login` returns 200 even when SSR is broken)
   - ~~Ran `assemble-prod.sh` + `pnpm run server`: HTTP 200 on `/login`, no `ERR_MODULE_NOT_FOUND` errors~~ ← **invalid test** (`/login` returns 200 even when SSR is broken)
   - **GOAL NOT ACHIEVED**: All Phase 3 packages remain in `.next/node_modules/` and must stay in `dependencies`. The `ssr: false` approach does not prevent Turbopack from externalising packages.
   - **GOAL NOT ACHIEVED**: All Phase 3 packages remain in `.next/node_modules/` and must stay in `dependencies`. The `ssr: false` approach does not prevent Turbopack from externalising packages.
   - _Requirements: 3.3, 3.4, 3.5_
   - _Requirements: 3.3, 3.4, 3.5_
 
 
 ---
 ---
 
 
-- [ ] 4. Resolve ambiguous and phantom package classifications
+- [x] 4. Resolve ambiguous and phantom package classifications
 
 
 - [x] 4.1 (P) Confirm `react-toastify` must remain in `dependencies`
 - [x] 4.1 (P) Confirm `react-toastify` must remain in `dependencies`
   - `toastr.ts` has static import `import { toast } from 'react-toastify'`; reachable from SSR client components (e.g., `features/page-tree/hooks/use-page-rename.tsx`)
   - `toastr.ts` has static import `import { toast } from 'react-toastify'`; reachable from SSR client components (e.g., `features/page-tree/hooks/use-page-rename.tsx`)
@@ -94,19 +95,19 @@
   - All consumers already guard for `null` socket (no breaking changes)
   - All consumers already guard for `null` socket (no breaking changes)
   - _Requirements: 4.2_
   - _Requirements: 4.2_
 
 
-- [ ] 4.3 (P) Verify whether `bootstrap` JS `import()` is browser-only and classify accordingly
+- [x] 4.3 (P) Verify whether `bootstrap` JS `import()` is browser-only and classify accordingly
   - Confirmed: `import('bootstrap/dist/js/bootstrap')` is inside `useEffect` in `_app.page.tsx` — browser-only
   - Confirmed: `import('bootstrap/dist/js/bootstrap')` is inside `useEffect` in `_app.page.tsx` — browser-only
   - Moved `bootstrap` from `dependencies` to `devDependencies`
   - Moved `bootstrap` from `dependencies` to `devDependencies`
   - **GOAL NOT ACHIEVED**: `bootstrap` still appears in `.next/node_modules/` after production build. Moved back to `dependencies` in task 5.1. (`useEffect`-guarded dynamic import does not prevent Turbopack externalisation.)
   - **GOAL NOT ACHIEVED**: `bootstrap` still appears in `.next/node_modules/` after production build. Moved back to `dependencies` in task 5.1. (`useEffect`-guarded dynamic import does not prevent Turbopack externalisation.)
   - _Requirements: 4.3_
   - _Requirements: 4.3_
 
 
-- [ ] 4.4 (P) Investigate phantom packages and remove or reclassify them
+- [x] 4.4 (P) Investigate phantom packages and remove or reclassify them
   - `i18next-http-backend`, `i18next-localstorage-backend`, `react-dropzone`: no direct imports in `apps/app/src/`
   - `i18next-http-backend`, `i18next-localstorage-backend`, `react-dropzone`: no direct imports in `apps/app/src/`
   - All three moved from `dependencies` to `devDependencies`
   - All three moved from `dependencies` to `devDependencies`
   - **GOAL NOT ACHIEVED**: All three still appear in `.next/node_modules/` (reached via transitive imports). Moved back to `dependencies` in task 5.1.
   - **GOAL NOT ACHIEVED**: All three still appear in `.next/node_modules/` (reached via transitive imports). Moved back to `dependencies` in task 5.1.
   - _Requirements: 4.4_
   - _Requirements: 4.4_
 
 
-- [ ] 4.5 Apply all Phase 4 package.json classification changes and run consolidated verification
+- [x] 4.5 Apply all Phase 4 package.json classification changes and run consolidated verification
   - ~~All Phase 4 changes applied to `apps/app/package.json`~~
   - ~~All Phase 4 changes applied to `apps/app/package.json`~~
   - ~~`assemble-prod.sh` + server start: HTTP 200 on `/login`, no `ERR_MODULE_NOT_FOUND`~~ ← **invalid test**
   - ~~`assemble-prod.sh` + server start: HTTP 200 on `/login`, no `ERR_MODULE_NOT_FOUND`~~ ← **invalid test**
   - **GOAL NOT ACHIEVED**: Tasks 4.3 and 4.4 goals were not achieved; their packages remain in `dependencies`. Phase 4 classification is therefore incomplete.
   - **GOAL NOT ACHIEVED**: Tasks 4.3 and 4.4 goals were not achieved; their packages remain in `dependencies`. Phase 4 classification is therefore incomplete.
@@ -114,14 +115,14 @@
 
 
 ---
 ---
 
 
-- [ ] 5. Final validation and documentation
+- [x] 5. Final validation and documentation
 
 
-- [ ] 5.1 Verify that every `.next/node_modules/` symlink resolves correctly in the release artifact
+- [x] 5.1 Verify that every `.next/node_modules/` symlink resolves correctly in the release artifact
   - Run `turbo run build --filter @growi/app` to produce a fresh build
   - Run `turbo run build --filter @growi/app` to produce a fresh build
   - Run `bash apps/app/bin/assemble-prod.sh` to produce the release artifact
   - Run `bash apps/app/bin/assemble-prod.sh` to produce the release artifact
     - **IMPORTANT**: `pnpm deploy --prod` generates `apps/app/node_modules/` symlinks that point to the workspace-root `node_modules/.pnpm/` (e.g. `../../../node_modules/.pnpm/react@18.2.0/...`). `assemble-prod.sh` step [1b/4] rewrites these to point within `apps/app/node_modules/.pnpm/` instead (e.g. `.pnpm/react@18.2.0/...` for non-scoped, `../.pnpm/react@18.2.0/...` for scoped). Without this rewrite, the production server fails with `TypeError: Cannot read properties of null (reading 'useContext')` when the workspace-root `node_modules` is absent.
     - **IMPORTANT**: `pnpm deploy --prod` generates `apps/app/node_modules/` symlinks that point to the workspace-root `node_modules/.pnpm/` (e.g. `../../../node_modules/.pnpm/react@18.2.0/...`). `assemble-prod.sh` step [1b/4] rewrites these to point within `apps/app/node_modules/.pnpm/` instead (e.g. `.pnpm/react@18.2.0/...` for non-scoped, `../.pnpm/react@18.2.0/...` for scoped). Without this rewrite, the production server fails with `TypeError: Cannot read properties of null (reading 'useContext')` when the workspace-root `node_modules` is absent.
   - **DO NOT restore `next.config.ts` before the server test.** If `next.config.ts` is present at server startup, Next.js attempts to install TypeScript via pnpm, which overwrites `apps/app/node_modules/` symlinks back to workspace-root paths, causing HTTP 500. Restore `next.config.ts` only after killing the server.
   - **DO NOT restore `next.config.ts` before the server test.** If `next.config.ts` is present at server startup, Next.js attempts to install TypeScript via pnpm, which overwrites `apps/app/node_modules/` symlinks back to workspace-root paths, causing HTTP 500. Restore `next.config.ts` only after killing the server.
-  - **DO NOT rename/remove workspace-root `node_modules`.** Workspace packages (`@growi/core` etc.) in `packages/*/node_modules/` have symlinks pointing to the workspace-root `node_modules`. Removing it causes `MODULE_NOT_FOUND` for server-side deps (e.g. `bson-objectid`). In Docker production, `packages/` is `COPY`'d with the full workspace structure, so the workspace-root `node_modules` is present there too.
+  - **DO NOT rename/remove workspace-root `node_modules`.** `pnpm deploy` recreates it as a side effect. In Docker production (Dockerfile release stage), only `apps/app/node_modules/` is COPY'd — root `node_modules` is NOT present. But `pnpm deploy --prod --legacy` bundles workspace packages (`@growi/core` etc.) as actual directories in the local `.pnpm/` store within `apps/app/node_modules/`, so they resolve correctly without the workspace root.
   - **Broken-symlink check for `.next/node_modules/`**: from workspace root, run the following and assert zero output (except `fslightbox-react` if task 3.1 is done):
   - **Broken-symlink check for `.next/node_modules/`**: from workspace root, run the following and assert zero output (except `fslightbox-react` if task 3.1 is done):
     ```bash
     ```bash
     cd apps/app && find .next/node_modules -maxdepth 2 -type l | while read link; do
     cd apps/app && find .next/node_modules -maxdepth 2 -type l | while read link; do
@@ -140,7 +141,7 @@
     - Assert HTTP 200, response body contains `内部仕様や仕様策定中の議論の内容をメモしていく Wiki です。`, and zero `ERR_MODULE_NOT_FOUND` lines in `/tmp/server.log`
     - Assert HTTP 200, response body contains `内部仕様や仕様策定中の議論の内容をメモしていく Wiki です。`, and zero `ERR_MODULE_NOT_FOUND` lines in `/tmp/server.log`
   - Kill the server after verification: `kill $(lsof -ti:3000)`
   - Kill the server after verification: `kill $(lsof -ti:3000)`
   - Restore `next.config.ts`: `git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts`
   - Restore `next.config.ts`: `git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts`
-  - **Result**: PENDING RE-VERIFICATION.
+  - **Result**: HTTP 200 on `GET /`. Response body contains `内部仕様や仕様策定中の議論の内容をメモしていく Wiki です。` (2 matches). Zero `ERR_MODULE_NOT_FOUND` in server log. Task 3.1 `fslightbox-react` broken symlink in `.next/node_modules/` confirmed as harmless (SSR never accesses it). Re-verified after `RevisionDiff.tsx` fix with workspace-root `node_modules` renamed (`mv node_modules node_modules.bak`): still HTTP 200, zero `ERR_MODULE_NOT_FOUND`, confirming `apps/app/node_modules/` is fully self-contained via `pnpm deploy --prod --legacy`.
   - **Root-cause summary**: The spec's Phase 2–4 assumption that `ssr: false` wrapping removes packages from `.next/node_modules/` was incorrect — Turbopack still externalises them. Additionally, the initial survey of 23 packages was incomplete; 19 further transitive packages (all `@codemirror/*`, `codemirror`, `codemirror-emacs/vim/vscode-keymap`, `@lezer/highlight`, `@marp-team/*`, `@emoji-mart/react`, `reveal.js`, `pako`, `cm6-theme-basic-light`, `y-codemirror.next`) also appear in `.next/node_modules/`. All 29 missing packages were added/moved to `dependencies`. Two `assemble-prod.sh` bugs were fixed: (1) `[ ... ] && ...` under `set -e`; (2) missing rewrite of `apps/app/node_modules/` symlinks from workspace-root paths to local `.pnpm/` paths.
   - **Root-cause summary**: The spec's Phase 2–4 assumption that `ssr: false` wrapping removes packages from `.next/node_modules/` was incorrect — Turbopack still externalises them. Additionally, the initial survey of 23 packages was incomplete; 19 further transitive packages (all `@codemirror/*`, `codemirror`, `codemirror-emacs/vim/vscode-keymap`, `@lezer/highlight`, `@marp-team/*`, `@emoji-mart/react`, `reveal.js`, `pako`, `cm6-theme-basic-light`, `y-codemirror.next`) also appear in `.next/node_modules/`. All 29 missing packages were added/moved to `dependencies`. Two `assemble-prod.sh` bugs were fixed: (1) `[ ... ] && ...` under `set -e`; (2) missing rewrite of `apps/app/node_modules/` symlinks from workspace-root paths to local `.pnpm/` paths.
   - _Requirements: 5.1, 5.2, 5.3, 5.4_
   - _Requirements: 5.1, 5.2, 5.3, 5.4_
 
 

+ 74 - 0
apps/app/.claude/rules/package-dependencies.md

@@ -0,0 +1,74 @@
+# Package Dependency Classification (Turbopack)
+
+## The Rule
+
+> Any package that appears in `apps/app/.next/node_modules/` after a production build MUST be listed under `dependencies`, not `devDependencies`.
+
+Turbopack externalises packages by generating runtime symlinks in `.next/node_modules/`. `pnpm deploy --prod` excludes `devDependencies`, so any externalised package missing from `dependencies` causes `ERR_MODULE_NOT_FOUND` in production.
+
+## How to Classify a New Package
+
+**Step 1 — Build and check:**
+
+```bash
+turbo run build --filter @growi/app
+ls apps/app/.next/node_modules/ | grep <package-name>
+```
+
+- **Found** → `dependencies`
+- **Not found** → `devDependencies` (if runtime code) or `devDependencies` (if build/test only)
+
+**Step 2 — If unsure, check the import site:**
+
+| Import pattern | Classification |
+|---|---|
+| `import foo from 'pkg'` at module level in SSR-executed code | `dependencies` |
+| `import type { Foo } from 'pkg'` only | `devDependencies` (type-erased at build) |
+| `await import('pkg')` inside `useEffect` / event handler | Check `.next/node_modules/` — may still be externalised |
+| Used only in `*.spec.ts`, build scripts, or CI | `devDependencies` |
+
+## Common Misconceptions
+
+**`dynamic({ ssr: false })` does NOT prevent Turbopack externalisation.**
+It skips HTML rendering for that component but Turbopack still externalises packages found via static import analysis inside the dynamically-loaded file.
+
+**`useEffect`-guarded `import()` does NOT guarantee devDependencies.**
+Bootstrap and i18next backends are loaded this way yet still appear in `.next/node_modules/` due to transitive imports.
+
+## Packages Confirmed as devDependencies (Verified)
+
+These were successfully removed from production artifact by eliminating their SSR import path:
+
+| Package | Technique |
+|---|---|
+| `fslightbox-react` | Replaced static import with `import()` inside `useEffect` in `LightBox.tsx` |
+| `socket.io-client` | Replaced static import with `await import()` inside `useEffect` in `admin/states/socket-io.ts` |
+| `@emoji-mart/data` | Replaced runtime import with bundled static JSON (`emoji-native-lookup.json`) |
+
+## Verifying the Production Artifact
+
+### Level 1 — Externalisation check (30–60 s, local, incremental)
+
+Just want to know if a package gets externalised by Turbopack?
+
+```bash
+turbo run build --filter @growi/app
+ls apps/app/.next/node_modules/ | grep <package-name>
+# Found → dependencies required
+# Not found → devDependencies is safe
+```
+
+Turbopack build is incremental via cache, so subsequent runs after the first are fast.
+
+### Level 2 — CI (`reusable-app-prod.yml`, authoritative)
+
+Trigger via `workflow_dispatch` before merging. Runs two jobs:
+
+1. **`build-prod`**: `turbo run build` → `assemble-prod.sh` → **`check-next-symlinks.sh`** → archives production tarball
+2. **`launch-prod`**: extracts the tarball into a clean isolated directory (no workspace-root `node_modules`), runs `pnpm run server:ci`
+
+`check-next-symlinks.sh` scans every symlink in `.next/node_modules/` and fails the build if any are broken (except `fslightbox-react` which is intentionally broken but harmless). This catches classification errors regardless of which code paths are exercised at runtime.
+
+`server:ci` = `node dist/server/app.js --ci`: the server starts fully (loading all modules), then immediately exits with code 0. If any module fails to load (`ERR_MODULE_NOT_FOUND`), the process exits with code 1, failing the CI job.
+
+This exactly matches Docker production (no workspace fallback). A `build-prod` or `launch-prod` failure definitively means a missing `dependencies` entry.

+ 8 - 0
apps/app/AGENTS.md

@@ -159,3 +159,11 @@ Plus all global skills (monorepo-overview, tech-stack).
 ---
 ---
 
 
 For detailed patterns and examples, refer to the Skills in `.claude/skills/`.
 For detailed patterns and examples, refer to the Skills in `.claude/skills/`.
+
+## Rules (Always Applied)
+
+The following rules in `.claude/rules/` are always applied when working in this directory:
+
+| Rule | Description |
+|------|-------------|
+| **package-dependencies** | Turbopack dependency classification — when to use `dependencies` vs `devDependencies`, verification procedure |

+ 23 - 0
apps/app/bin/check-next-symlinks.sh

@@ -0,0 +1,23 @@
+#!/bin/bash
+# Check that all .next/node_modules/ symlinks resolve correctly after assemble-prod.sh.
+# fslightbox-react is intentionally broken (useEffect-only import, never accessed during SSR).
+# Usage: bash apps/app/bin/check-next-symlinks.sh (from monorepo root)
+set -euo pipefail
+
+NEXT_MODULES="apps/app/.next/node_modules"
+
+broken=$(find "$NEXT_MODULES" -maxdepth 2 -type l | while read -r link; do
+  linkdir=$(dirname "$link")
+  target=$(readlink "$link")
+  resolved=$(cd "$linkdir" 2>/dev/null && realpath -m "$target" 2>/dev/null || echo "UNRESOLVABLE")
+  { [ "$resolved" = "UNRESOLVABLE" ] || [ ! -e "$resolved" ]; } && echo "BROKEN: $link"
+done | grep -v 'fslightbox-react' || true)
+
+if [ -n "$broken" ]; then
+  echo "ERROR: Broken symlinks found in $NEXT_MODULES:"
+  echo "$broken"
+  echo "Move these packages from devDependencies to dependencies in apps/app/package.json."
+  exit 1
+fi
+
+echo "OK: All $NEXT_MODULES symlinks resolve correctly."

+ 3 - 5
apps/app/src/client/components/PageHistory/RevisionDiff.tsx

@@ -1,4 +1,4 @@
-import { type JSX, useEffect, useMemo, useState } from 'react';
+import { type JSX, useMemo } from 'react';
 import Link from 'next/link';
 import Link from 'next/link';
 import type { IRevisionHasId } from '@growi/core';
 import type { IRevisionHasId } from '@growi/core';
 import { GrowiThemeSchemeType } from '@growi/core';
 import { GrowiThemeSchemeType } from '@growi/core';
@@ -6,13 +6,11 @@ import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { PresetThemesMetadatas } from '@growi/preset-themes';
 import { PresetThemesMetadatas } from '@growi/preset-themes';
 import { createPatch } from 'diff';
 import { createPatch } from 'diff';
 import type { Diff2HtmlConfig } from 'diff2html';
 import type { Diff2HtmlConfig } from 'diff2html';
+import { html } from 'diff2html';
+import { ColorSchemeType } from 'diff2html/lib/types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-// Replicate ColorSchemeType locally so diff2html stays out of the SSR bundle
-const ColorSchemeType = { AUTO: 'auto', DARK: 'dark', LIGHT: 'light' } as const;
-type ColorSchemeType = (typeof ColorSchemeType)[keyof typeof ColorSchemeType];
-
 import { Themes, useNextThemes } from '~/stores-universal/use-next-themes';
 import { Themes, useNextThemes } from '~/stores-universal/use-next-themes';
 
 
 import UserDate from '../../../components/User/UserDate';
 import UserDate from '../../../components/User/UserDate';