Browse Source

remove spec for optimisation

Yuki Takei 2 weeks ago
parent
commit
5d4f78f532

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

@@ -1,459 +0,0 @@
-# Design Document: optimise-deps-for-prod
-
-## Overview
-
-This feature corrects the `devDependencies` / `dependencies` classification in `apps/app/package.json` for packages that Turbopack externalises at SSR runtime. When webpack was the bundler, all imports were inlined into self-contained chunks, making `devDependencies` sufficient. Turbopack instead loads certain packages at runtime via `.next/node_modules/` symlinks; `pnpm deploy --prod` excludes `devDependencies`, breaking the production server.
-
-**Purpose**: Restore a working production build and then minimise the `dependencies` set to only what is genuinely required at runtime.
-
-**Users**: Release engineers and developers maintaining the production deployment pipeline.
-
-**Impact**: Modifies `apps/app/package.json` and up to four source files; no changes to user-facing features or API contracts.
-
-### Goals
-
-- `pnpm deploy --prod` produces a complete, self-contained production artifact (Req 1)
-- Minimise `dependencies` by reverting packages where technically safe (Req 2, 3, 4)
-- Document the Turbopack externalisation rule to prevent future misclassification (Req 5)
-
-### Non-Goals
-
-- Changes to Turbopack configuration or build pipeline beyond `assemble-prod.sh`
-- Refactoring of feature logic or component APIs unrelated to SSR behaviour
-- Migration from Pages Router to App Router
-
----
-
-## Requirements Traceability
-
-| Requirement | Summary | Components | Flows |
-|-------------|---------|------------|-------|
-| 1.1–1.5 | Move all 23 packages to `dependencies`; verify no missing-module errors | `package.json` | Phase 1 |
-| 2.1–2.4 | Investigate and optionally remove `@emoji-mart/data` server-side import | `emoji.ts` | Phase 2 |
-| 3.1–3.5 | Apply `dynamic({ ssr: false })` to eligible Group 1 components | Targeted component files | Phase 3 |
-| 4.1–4.5 | Resolve `react-toastify`, `socket.io-client`, `bootstrap`, phantom packages | `admin/states/socket-io.ts`, `_app.page.tsx` | Phase 4 |
-| 5.1–5.5 | Validate final state; add Turbopack externalisation rule documentation | `package.json`, steering doc | Phase 5 |
-
----
-
-## Architecture
-
-### Current Architecture Analysis
-
-The production assembly pipeline:
-
-```
-turbo run build
-  └─ Turbopack build → .next/ (with .next/node_modules/ symlinks → ../../../../node_modules/.pnpm/)
-
-assemble-prod.sh (current — no symlink rewriting):
-  [1]   pnpm deploy out --prod --legacy --filter @growi/app
-        └─ out/node_modules/<pkg> → .pnpm/<pkg>/...  (self-contained in pnpm v10 ✓)
-  [mv]  rm -rf node_modules && mv out/node_modules node_modules
-        └─ workspace-root node_modules/ is now prod-only ✓
-  [ln]  rm -rf apps/app/node_modules && ln -sfn ../../node_modules apps/app/node_modules
-        └─ apps/app/node_modules → ../../node_modules ✓
-  [3]   rm -rf .next/cache
-  [4]   rm -f next.config.ts
-
-cp -a node_modules /tmp/release/   → workspace-root prod node_modules
-cp -a apps/app/.next ...           → preserves pnpm symlinks intact
-COPY --from=builder /tmp/release/ → release image
-```
-
-The `.next/node_modules/` symlinks (pointing `../../../../node_modules/.pnpm/`) resolve naturally to the workspace-root `node_modules/` — no symlink rewriting is needed. In pnpm v10, `--legacy` produces self-contained `.pnpm/` symlinks within the deploy output (step [1b] is unnecessary); placing the output at workspace root means Turbopack's original symlink targets already resolve (step [2] is unnecessary).
-
-### Architecture Pattern & Boundary Map
-
-```mermaid
-graph TB
-    subgraph DockerBuilder
-        PrunedRoot[Pruned workspace root]
-        FullDepsNM[node_modules full deps]
-        Build[turbo run build]
-        NextOut[apps/app/.next node_modules symlinks to 4-levels-up]
-        Deploy[pnpm deploy out --prod --legacy]
-        ProdNM[out/node_modules prod-only self-contained]
-        Replace[rm node_modules mv out/node_modules node_modules]
-        Symlink[ln -sfn apps/app/node_modules]
-        Clean[rm cache next.config.ts]
-        Stage[Stage to tmp/release]
-    end
-
-    subgraph ReleaseImage
-        WsRoot[workspace root node_modules prod-only]
-        AppNext[apps/app/.next symlinks resolve naturally]
-        AppNMLink[apps/app/node_modules symlink to ws root]
-        Server[pnpm run server]
-    end
-
-    PrunedRoot --> FullDepsNM
-    FullDepsNM --> Build
-    Build --> NextOut
-    Build --> Deploy
-    Deploy --> ProdNM
-    ProdNM --> Replace
-    Replace --> Symlink
-    Symlink --> Clean
-    Clean --> Stage
-    Stage --> WsRoot
-    Stage --> AppNext
-    Stage --> AppNMLink
-    WsRoot --> AppNext
-    AppNMLink --> WsRoot
-    WsRoot --> Server
-```
-
-**Key decisions**:
-- Workspace-root staging (not `apps/app/` staging): Turbopack's `.next/node_modules/` symlinks point `../../../../node_modules/.pnpm/` (workspace root). Placing the deploy output at workspace root means these symlinks resolve naturally — no step [2] rewriting needed (see `research.md` — Session 3: Decision: workspace-root staging).
-- Keep `--legacy` in `pnpm deploy`: In pnpm v10, `--legacy` produces self-contained `.pnpm/` symlinks within the deploy output — step [1b] rewriting is no longer needed (see `research.md` — Session 3).
-- `apps/app/node_modules` as a symlink to `../../node_modules`: satisfies `migrate-mongo` script path and Node.js `require()` traversal without duplicating the `.pnpm/` store.
-- `pnpm deploy --prod` (not `--dev`) is the correct scope; only runtime packages belong in the artifact.
-
-### Technology Stack
-
-| Layer | Choice | Role | Notes |
-|-------|--------|------|-------|
-| Package manifest | `apps/app/package.json` | Declares runtime vs build-time deps | 23 entries move from `devDependencies` to `dependencies` |
-| Build assembly | `apps/app/bin/assemble-prod.sh` | Produces self-contained release artifact | No symlink rewriting; workspace-root staging + `apps/app/node_modules` symlink (pnpm v10) |
-| Bundler | Turbopack (Next.js 16) | Externalises packages to `.next/node_modules/` | Externalisation heuristic: static module-level imports in SSR code paths |
-| Package manager | pnpm v10 with `--legacy` deploy | Produces pnpm-native `node_modules` with `.pnpm/` virtual store | `inject-workspace-packages` not required with `--legacy` |
-
----
-
-## System Flows
-
-### Phased Execution Flow
-
-```mermaid
-graph TB
-    P1[Phase 1: Move all 23 to dependencies]
-    P1Check{Production server OK?}
-    P2[Phase 2: Investigate @emoji-mart/data server import]
-    P2Result{Removal viable without breaking emoji?}
-    P2Revert[Move @emoji-mart/data back to devDependencies]
-    P2Keep[Document as justified production dep]
-    P3[Phase 3: Apply ssr:false to eligible Group 1 components]
-    P4[Phase 4: Resolve ambiguous packages]
-    P5[Phase 5: Final validation and documentation]
-
-    P1 --> P1Check
-    P1Check -->|No| P1
-    P1Check -->|Yes| P2
-    P2 --> P2Result
-    P2Result -->|Yes| P2Revert
-    P2Result -->|No| P2Keep
-    P2Revert --> P3
-    P2Keep --> P3
-    P3 --> P4
-    P4 --> P5
-```
-
-Each phase gate requires: production server starts without errors + `GET /` returns HTTP 200 with expected content (body contains `内部仕様や仕様策定中の議論の内容をメモしていく Wiki です。`) + zero `ERR_MODULE_NOT_FOUND` in server log. **Do NOT use `/login` as the smoke test URL** — it returns HTTP 200 even when SSR is broken because the login page does not render editor components.
-
----
-
-## Components and Interfaces
-
-### Summary Table
-
-| Component | Domain | Intent | Req Coverage | Contracts |
-|-----------|--------|--------|--------------|-----------|
-| `package.json` | Build Config | Dependency manifest | 1.1, 2.3, 2.4, 3.4, 3.5, 4.1–4.4, 5.3 | State |
-| `emoji.ts` (remark plugin) | Server Renderer | Emoji shortcode → native emoji lookup | 2.1, 2.2, 2.3 | Service |
-| Admin socket atom | Client State | Socket.IO connection for admin panel | 4.2 | State |
-| Group 1 component wrappers | UI | `dynamic({ ssr: false })` wrapping for eligible components | 3.1–3.5 | — |
-
----
-
-### Build Config
-
-#### `apps/app/package.json`
-
-| Field | Detail |
-|-------|--------|
-| Intent | Central manifest governing which packages are included in `pnpm deploy --prod` output |
-| Requirements | 1.1, 2.3, 2.4, 3.4, 3.5, 4.1–4.4, 5.3 |
-
-**Responsibilities & Constraints**
-- Determines the complete set of packages in the production artifact.
-- Any package appearing in `.next/node_modules/` after a production build must be in `dependencies`, not `devDependencies`.
-- Changes propagate to all consumers of the monorepo lock file; `pnpm install --frozen-lockfile` must remain valid.
-
-**Final classification** (all packages in `dependencies`; 3 successfully reverted to `devDependencies`):
-
-| Package | Final classification | Rationale |
-|---------|---------------------|-----------|
-| `@codemirror/state` + 18 others (`codemirror`, `codemirror-emacs/vim/vscode-keymap`, `@lezer/highlight`, `@marp-team/*`, `@emoji-mart/react`, `reveal.js`, `pako`, `cm6-theme-basic-light`, `y-codemirror.next`) | `dependencies` | Found in `.next/node_modules/` via transitive imports; not in initial 23 — discovered during implementation |
-| `@handsontable/react` | `dependencies` | Still externalised despite `useEffect` dynamic import; `ssr: false` does not prevent Turbopack externalisation |
-| `@headless-tree/core`, `@headless-tree/react` | `dependencies` | Used in PageTree hooks (SSR'd) |
-| `@tanstack/react-virtual` | `dependencies` | Used in ItemsTree (layout-critical, SSR'd) |
-| `bootstrap` | `dependencies` | Still externalised despite `useEffect`-guarded `import()`; transitive imports cause externalisation |
-| `diff2html` | `dependencies` | Still externalised despite `ssr: false` on RevisionDiff; static import analysis reaches it |
-| `downshift` | `dependencies` | Used in SearchModal (SSR'd) |
-| `fastest-levenshtein` | `dependencies` | Used in openai client service (SSR'd) |
-| `i18next-http-backend`, `i18next-localstorage-backend`, `react-dropzone` | `dependencies` | Still in `.next/node_modules/` via transitive imports despite no direct `src/` imports |
-| `pretty-bytes` | `dependencies` | Used in RichAttachment (SSR'd) |
-| `react-copy-to-clipboard` | `dependencies` | Used in multiple inline components (SSR'd) |
-| `react-dnd`, `react-dnd-html5-backend` | `dependencies` | Still externalised despite DnD provider wrapped with `ssr: false` |
-| `react-hook-form` | `dependencies` | Used in forms across app (SSR'd) |
-| `react-input-autosize` | `dependencies` | Used in form inputs (SSR'd) |
-| `react-toastify` | `dependencies` | Static `{ toast }` import in `toastr.ts`; async refactor would change API surface |
-| `simplebar-react` | `dependencies` | Used in Sidebar, AiAssistant (layout-critical, SSR'd) |
-| **`@emoji-mart/data`** | **`devDependencies`** | Replaced with bundled `emoji-native-lookup.json`; no runtime package needed |
-| **`fslightbox-react`** | **`devDependencies`** | `import()` inside `useEffect`; broken symlink in `.next/node_modules/` is harmless (SSR never accesses it) |
-| **`socket.io-client`** | **`devDependencies`** | `await import()` inside `useEffect` in `admin/states/socket-io.ts`; no static SSR import path |
-
-**Contracts**: State [x]
-
-**Implementation Notes**
-- Integration: Edit `apps/app/package.json` directly; run `pnpm install --frozen-lockfile` to verify lock file integrity after changes.
-- Validation: After each phase, run `assemble-prod.sh` locally and start `pnpm run server`; confirm no `ERR_MODULE_NOT_FOUND` in logs and HTTP 200 on `/login`.
-- Risks: Moving packages from `devDependencies` to `dependencies` may increase Docker image size; acceptable trade-off for Phase 1.
-
----
-
-### Server Renderer
-
-#### `apps/app/src/services/renderer/remark-plugins/emoji.ts`
-
-| Field | Detail |
-|-------|--------|
-| Intent | Resolve `:emoji-name:` shortcodes to native emoji characters during Markdown SSR |
-| Requirements | 2.1, 2.2, 2.3 |
-
-**Responsibilities & Constraints**
-- Processes Markdown AST server-side via `findAndReplace`.
-- Uses `@emoji-mart/data/sets/15/native.json` only to look up `emojiData.emojis[name]?.skins[0].native`.
-- Must produce identical output before and after any refactor (Req 2.2).
-
-**Dependencies**
-- External: `@emoji-mart/data` — static emoji data (P1, Phase 2 investigation target)
-
-**Contracts**: Service [x]
-
-**Phase 2 investigation**: Determine whether `@emoji-mart/data` can be replaced with a repo-bundled static lookup file. The required data structure is:
-
-```typescript
-interface EmojiNativeLookup {
-  emojis: Record<string, { skins: [{ native: string }] }>;
-}
-```
-
-If a static extraction script (run at package update time) can produce this file, `@emoji-mart/data` can revert to `devDependencies`. If not, document as justified production dependency per Req 2.4.
-
-**Implementation Notes**
-- Integration: Any replacement file must be a static JSON import; no runtime fetch.
-- Validation: Render a Markdown document containing known emoji shortcodes (`:+1:`, `:tada:`, etc.) and verify the native characters appear in the output.
-- Risks: Static extraction requires a maintenance step when `@emoji-mart/data` is upgraded.
-
----
-
-### Client State
-
-#### `apps/app/src/features/admin/states/socket-io.ts`
-
-| Field | Detail |
-|-------|--------|
-| Intent | Jotai atom managing Socket.IO connection for the admin panel |
-| Requirements | 4.2 |
-
-**Responsibilities & Constraints**
-- Provides an `io` Socket.IO client instance to admin panel components.
-- The static `import io from 'socket.io-client'` at module level causes Turbopack to externalise `socket.io-client` for SSR.
-
-**Dependencies**
-- External: `socket.io-client` — WebSocket client (P1 if static; P2 after dynamic import refactor)
-
-**Contracts**: State [x]
-
-**Phase 4 refactor target**: Replace static import with dynamic import to match the pattern in `states/socket-io/global-socket.ts`:
-
-```typescript
-// Before (causes SSR externalisation)
-import io from 'socket.io-client';
-
-// After (browser-only, matches global-socket.ts pattern)
-const io = (await import('socket.io-client')).default;
-```
-
-**Implementation Notes**
-- Integration: The atom must be an async atom or use `atomWithLazy` to defer the import.
-- Validation: Verify admin socket connects in browser; verify `socket.io-client` no longer appears in `.next/node_modules/` after a production build.
-- Risks: Admin socket consumers must handle the async initialisation; if synchronous access is required at page load, the refactor may not be feasible.
-
----
-
-### UI — `dynamic({ ssr: false })` Wrapper Points
-
-These are not new components; they are targeted wrapping of existing imports using Next.js `dynamic`.
-
-**Phase 3 evaluation criteria**:
-1. Component renders interactive content only (no meaningful text for SEO)
-2. Initial HTML without the component does not cause visible layout shift
-3. No hydration mismatch after applying `ssr: false`
-
-| Component | Package | `ssr: false` Feasibility | Notes |
-|-----------|---------|-------------------------|-------|
-| `LightBox.tsx` | `fslightbox-react` | High — renders only after user interaction | No SSR content |
-| `RevisionDiff.tsx` | `diff2html` | High — interactive diff viewer, no SEO content | Loaded on user action |
-| `PageTree.tsx` drag-drop | `react-dnd`, `react-dnd-html5-backend` | Medium — DnD provider wraps tree; tree content is SSR'd | Wrap DnD provider only, not content |
-| `HandsontableModal.tsx` | `@handsontable/react` | High — modal, not in initial HTML | Verify existing dynamic import pattern |
-| `SearchModal.tsx` | `downshift` | Low — search input in sidebar, part of hydration | Risk of layout shift |
-| openai fuzzy matching | `fastest-levenshtein` | Medium — algorithm utility; depends on call site | May be callable lazily |
-
-**Contracts**: None (pure wrapping changes, no new interfaces)
-
-**Implementation Notes**
-- Apply `dynamic` wrapping to the specific consuming component file, not to the package entry point.
-- Validation per component: (a) build with package removed from `dependencies`, (b) confirm it disappears from `.next/node_modules/`, (c) run Production Server Startup Procedure and assert `GET /` returns HTTP 200 with expected content, (d) confirm no hydration warnings in browser console.
-- Risks: Wrapping components that render visible content may cause flash of missing content (FOMC); test on slow connections.
-
----
-
-## Testing Strategy
-
-### Production Server Startup Procedure
-
-以下の手順でプロダクションサーバーを起動する。devcontainer 環境での検証を想定している。
-
-**Step 1 — クリーンビルド**(ワークスペースルートから実行)
-
-```bash
-turbo run build --filter @growi/app
-```
-
-**Step 2 — プロダクション用アセンブル**(ワークスペースルートから実行)
-
-```bash
-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` は `rm -rf node_modules && mv out/node_modules node_modules` を実行するため、ワークスペースルートの `node_modules/` が prod-only に置き換わる。**`mv node_modules node_modules.bak` は不要**(新アプローチではワークスペースルートが自動的に prod-only になる)。テスト後は `pnpm install` で開発環境を復元すること。
-> - `apps/app/node_modules` は `../../node_modules` へのシンボリックリンクになる。Docker release image でもこの構造が再現される。
-
-**Step 3 — プロダクションサーバー起動**(`apps/app/` から実行)
-
-```bash
-cd apps/app && pnpm run server > /tmp/server.log 2>&1 &
-```
-
-起動完了を待つ:
-
-```bash
-timeout 60 bash -c 'until grep -q "Express server is listening" /tmp/server.log; do sleep 2; done'
-```
-
-> **注意**: `preserver` スクリプトが `pnpm run migrate` を実行するため、起動に数十秒かかる。**Do NOT use mongosh/mongo** for DB connectivity checks — check server logs instead.
-
-**Step 4 — 検証**
-
-```bash
-# HTTP ステータスとコンテンツ確認
-HTTP_CODE=$(curl -s -o /tmp/response.html -w "%{http_code}" http://localhost:3000/)
-echo "HTTP: $HTTP_CODE"  # → 200 であること
-grep -c "内部仕様や仕様策定中の議論の内容をメモしていく Wiki です。" /tmp/response.html  # → 1 以上であること
-
-# ERR_MODULE_NOT_FOUND がないことを確認
-grep -c "ERR_MODULE_NOT_FOUND" /tmp/server.log  # → 0 であること
-```
-
-> **検証 URL は `/` を使うこと。`/login` は不可。** `/login` は SSR が壊れていても HTTP 200 を返すため正常動作の確認にならない。`/` はエディタ関連コンポーネントを SSR するため、パッケージが欠損すると HTTP 500 になる。
-
-**Step 5 — サーバー停止**
-
-```bash
-kill $(lsof -ti:3000)
-```
-
-**Step 6 — 開発環境の復元**(検証後)
-
-```bash
-# next.config.ts を復元
-git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts
-```
-
----
-
-### Server Rendering Verification
-
-プロダクションサーバー起動後、以下のコマンドで SSR の正常動作を確認する。
-
-**検証コマンド(Production Server Startup Procedure の Step 5 参照)**
-
-```bash
-# URL は / を使うこと(/login は不可)
-HTTP_CODE=$(curl -s -o /tmp/response.html -w "%{http_code}" http://localhost:3000/)
-echo "HTTP: $HTTP_CODE"  # → 200
-grep -c "内部仕様や仕様策定中の議論の内容をメモしていく Wiki です。" /tmp/response.html  # → 1 以上
-grep -c "ERR_MODULE_NOT_FOUND" /tmp/server.log  # → 0
-```
-
-**破損シンボリックリンクの確認**
-
-`assemble-prod.sh` 実行後、`.next/node_modules/` および `apps/app/node_modules/` 内のシンボリックリンクがすべて解決可能であることを確認する。
-
-```bash
-# .next/node_modules/ の確認
-cd apps/app && find .next/node_modules -maxdepth 2 -type l | while read 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
-
-# apps/app/node_modules/ の確認(ワークスペースルートの node_modules をリネーム後に実行)
-find apps/app/node_modules -maxdepth 2 -type l | while read 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
-```
-
-> **注意**: `@growi/*` パッケージは `../../../../packages/` を指すシンボリックリンクだが、`packages/` はワークスペースルート直下に存在するため問題ない。
-
-**devDependencies 逆入り確認**
-
-```bash
-# devDependencies に列挙されているパッケージが .next/node_modules/ に現れないことを確認
-# (あれば Classification regression)
-```
-
-**devcontainer における再現性について**
-
-`assemble-prod.sh` は `rm -rf node_modules && mv out/node_modules node_modules` を実行するため、ワークスペースルートの `node_modules/` が prod-only の deploy 出力に置き換わる。**`mv node_modules node_modules.bak` は不要**(以前のアプローチでは必要だったが、現アプローチでは `assemble-prod.sh` がワークスペースルートを自動的に prod-only にする)。テスト完了後は `pnpm install` で開発環境を復元すること。
-
----
-
-### Phase 1 — Smoke Test (Req 1.3, 1.4, 1.5)
-
-- 上記「Production Server Startup Procedure」に従いサーバーを起動し、stdout に `ERR_MODULE_NOT_FOUND` が出力されないことを確認する。
-- **HTTP GET `/`** (not `/login`): assert HTTP 200, body contains `内部仕様や仕様策定中の議論の内容をメモしていく Wiki です。`, zero `ERR_MODULE_NOT_FOUND` in server log.
-- Run `launch-prod` CI job: assert job passes against MongoDB 6.0 and 8.0.
-
-### Phase 2 — Emoji Rendering (Req 2.2)
-
-- Unit test: render Markdown string containing `:+1:`, `:tada:`, `:rocket:` through the remark plugin; assert native emoji characters in output.
-- If static file replacement applied: run same test against replacement; assert identical output.
-
-### Phase 3 — Hydration Integrity (Req 3.3)
-
-- Per-package smoke test: for each package moved to devDependencies, run Production Server Startup Procedure and assert `GET /` HTTP 200 with expected content.
-- Per-component browser test: load page containing the wrapped component; assert no React hydration warnings in browser console.
-- Visual regression: screenshot comparison of affected pages before and after `ssr: false` wrapping.
-
-### Phase 4 — Admin Socket and Bootstrap (Req 4.2, 4.3)
-
-- Per-package smoke test: for each package moved to devDependencies, run Production Server Startup Procedure and assert `GET /` HTTP 200 with expected content.
-- Admin socket: open admin panel in browser; assert Socket.IO connection established (WebSocket upgrade in browser DevTools Network tab).
-- Bootstrap: assert Bootstrap dropdown/modal JavaScript functions correctly in browser after confirming `import()` placement.
-
-### Phase 5 — Final Coverage Check (Req 5.1, 5.3)
-
-- After deploy, assert that every symlink in `apps/app/.next/node_modules/` AND `apps/app/node_modules/` resolves to an existing file (zero broken symlinks; `apps/app/node_modules` is a symlink to `../../node_modules` which is the prod-only workspace-root `node_modules/`).
-- Assert no package listed in `devDependencies` appears in `apps/app/.next/node_modules/` after a production build.
-- Run Production Server Startup Procedure and assert `GET /` HTTP 200 with expected content.
-
----
-

+ 0 - 49
.kiro/specs/optimise-deps-for-prod/requirements.md

@@ -1,49 +0,0 @@
-# Requirements Document
-
-## Introduction
-
-GROWI migrated its production bundler from webpack to Turbopack. Unlike webpack, which inlines all imported modules into self-contained bundle chunks, Turbopack externalises certain packages at SSR runtime via `.next/node_modules/` symlinks. This means packages that were historically classified as `devDependencies` (on the assumption that they were only needed at build time) are now required at production runtime.
-
-The current `pnpm deploy --prod` step excludes `devDependencies`, causing missing modules when the production server starts. This specification defines a phased approach: first restore a working production build by moving all affected packages to `dependencies`, then systematically minimise the `dependencies` set by eliminating unnecessary runtime exposure where technically feasible.
-
----
-
-## Requirements
-
-### Requirement 1: Production Build Baseline Restoration
-
-**Objective:** As a release engineer, I want `pnpm deploy --prod` to produce a complete, self-contained production artifact, so that the production server starts without missing-module errors.
-
-**Summary**: All packages appearing in `.next/node_modules/` after a Turbopack production build must be in `dependencies`. The production server must start without `ERR_MODULE_NOT_FOUND` errors and pass the `launch-prod` CI job against MongoDB 6.0 and 8.0.
-
----
-
-### Requirement 2: Elimination of Server-Side Imports for Group 2 Packages
-
-**Objective:** As a developer, I want to remove direct server-side imports of packages that do not need to run on the server, so that those packages can revert to `devDependencies` and be excluded from the production artifact.
-
-**Summary**: Investigate whether `@emoji-mart/data`'s server-side import in `emoji.ts` can be replaced with a server-safe alternative. If successful, emoji rendering must produce identical output and the package must revert to `devDependencies`. If removal is not viable, document as a justified production dependency.
-
----
-
-### Requirement 3: SSR Opt-out for Group 1 Client-Only Components
-
-**Objective:** As a developer, I want to wrap client-only UI components with `dynamic(() => import(...), { ssr: false })` where appropriate, so that their dependencies are excluded from Turbopack's SSR externalisation and can revert to `devDependencies`.
-
-**Summary**: Evaluate Group 1 components for `ssr: false` safety (no SEO impact, no hydration mismatch, no visible layout shift). Successfully converted components must no longer appear in `.next/node_modules/`; otherwise, the package stays in `dependencies` with documentation.
-
----
-
-### Requirement 4: Classification of Unresolved Packages
-
-**Objective:** As a developer, I want to determine the correct final classification for packages with unclear or mixed usage patterns, so that every entry in `dependencies` and `devDependencies` has a documented and verified rationale.
-
-**Summary**: Resolve `react-toastify`, `socket.io-client`, `bootstrap`, and phantom packages (`i18next-http-backend`, `i18next-localstorage-backend`, `react-dropzone`) through code analysis and build verification. Refactor to dynamic imports where feasible; otherwise document as justified production dependencies. CI must pass after all reclassifications.
-
----
-
-### Requirement 5: Final State Validation and Documentation
-
-**Objective:** As a release engineer, I want a verified and documented final state of `dependencies` vs `devDependencies`, so that future package additions follow the correct classification rules.
-
-**Summary**: Every symlink in `.next/node_modules/` must resolve correctly in the release artifact (no broken symlinks). No `devDependencies` package may appear in `.next/node_modules/` after a production build. The codebase must include documentation of the Turbopack externalisation rule to prevent future misclassification.

+ 0 - 252
.kiro/specs/optimise-deps-for-prod/research.md

@@ -1,252 +0,0 @@
-# Research & Design Decisions
-
----
-**Purpose**: Capture discovery findings and rationale for the `optimise-deps-for-prod` specification.
-
-**Usage**: Background investigation notes referenced by `design.md`.
-
----
-
-## Summary
-
-- **Feature**: `optimise-deps-for-prod`
-- **Discovery Scope**: Extension — modifying `apps/app/package.json` and targeted source files
-- **Key Findings**:
-  - All 23 packages confirmed present in `apps/app/.next/node_modules/` after a Turbopack production build, verifying they are Turbopack SSR externals.
-  - `pnpm deploy --prod --legacy` produces a pnpm-native isolated structure (`node_modules/.pnpm/` + symlinks), NOT a flat/hoisted layout; transitive deps are only in `.pnpm/`, not hoisted to top-level.
-  - Three packages (`i18next-http-backend`, `i18next-localstorage-backend`, `react-dropzone`) have no direct imports in `apps/app/src/` and may be transitive phantom entries or unused.
-
----
-
-## Research Log
-
-### Turbopack SSR Externalisation Mechanism
-
-- **Context**: Why does Turbopack create `.next/node_modules/` at all, and what determines which packages end up there?
-- **Findings**:
-  - Turbopack (Next.js 16) externalises packages for SSR when they are imported in code that runs server-side, instead of inlining them into the SSR bundle as webpack does.
-  - In Next.js Pages Router, every page component is SSR'd for initial HTML regardless of `"use client"` directives. Only `dynamic(() => import('...'), { ssr: false })` prevents server-side execution.
-  - The resulting symlinks in `.next/node_modules/` point to `../../../../node_modules/.pnpm/...` (workspace root) and must be redirected to `../../node_modules/.pnpm/...` (deploy output) via the `assemble-prod.sh` rewrite step.
-- **Implications**: Any package imported at module-level in an SSR-rendered code path (component, hook, utility, server service) will be externalised and must be in `dependencies`.
-
-### pnpm deploy --legacy output structure
-
-- **Context**: Does `--legacy` produce a flat (hoisted) or pnpm-native node_modules?
-- **Findings**:
-  - `pnpm deploy --legacy` in pnpm v10 still produces a pnpm-native structure with `node_modules/.pnpm/` and symlinks. The `--legacy` flag only bypasses the `inject-workspace-packages` gate; it does NOT force hoisting.
-  - Transitive deps (e.g., `use-sync-external-store`, `dequal` from `swr`) are in `.pnpm/` but NOT at the top-level. The `cp -rL` approach failed because physical copies in `.next/node_modules/` lose the `.pnpm/` sibling resolution context.
-  - The correct fix is symlink rewriting in `assemble-prod.sh`: `../../../../node_modules/.pnpm/` → `../../node_modules/.pnpm/`.
-- **Implications**: The symlink rewrite in `assemble-prod.sh` is essential and must not be replaced with `cp -rL`.
-
-### @emoji-mart/data — server-side import analysis
-
-- **Context**: Can the server-side import in `emoji.ts` be removed or replaced?
-- **Sources Consulted**: `apps/app/src/services/renderer/remark-plugins/emoji.ts`
-- **Findings**:
-  - The plugin performs `import emojiData from '@emoji-mart/data/sets/15/native.json'` and accesses only `emojiData.emojis[$1]?.skins[0].native`.
-  - The data structure needed is: `{ emojis: { [name: string]: { skins: [{ native: string }] } } }`.
-  - This is a static JSON lookup — no runtime behaviour from the package, only data.
-  - Alternative: A minimal bundled static file (`emoji-native-lookup.json`) containing only the `name → native emoji` mapping could replace the full `@emoji-mart/data` package.
-  - Effort: moderate (requires a build-time extraction script or manual curation); risk: emoji data staleness.
-- **Implications**: Technically feasible to replace with a static file, but adds maintenance overhead. Acceptable to keep as `dependencies` for Phase 1; defer decision to Phase 2 investigation.
-
-### bootstrap — dynamic import analysis
-
-- **Context**: Why does `bootstrap` appear in `.next/node_modules/` when the import appears to be dynamic?
-- **Sources Consulted**: `apps/app/src/pages/_app.page.tsx:93`
-- **Findings**:
-  - `import('bootstrap/dist/js/bootstrap')` is a dynamic `import()` expression, not a static `import` statement.
-  - If called inside a `useEffect`, it would be browser-only and Turbopack should not externalise it for SSR.
-  - Needs verification: check whether Turbopack traces the `import()` call site (component level vs `useEffect`) and whether it appears in `.next/node_modules/` after a build without bootstrap in `dependencies`.
-- **Implications**: Bootstrap may be safely reverted to `devDependencies` if the `import()` is confirmed to be inside a browser-only lifecycle hook. Flag for Phase 4 investigation.
-
-### socket.io-client — mixed import pattern
-
-- **Context**: `global-socket.ts` uses `await import('socket.io-client')` (dynamic, browser-safe); `features/admin/states/socket-io.ts` uses `import io from 'socket.io-client'` (static, externalised by Turbopack).
-- **Findings**:
-  - The admin socket's static import is the cause of externalisation.
-  - Refactoring to `const { default: io } = await import('socket.io-client')` (matching `global-socket.ts` pattern) would remove the static import from the SSR code path.
-  - Pattern precedent exists in `global-socket.ts` — low-risk refactor.
-- **Implications**: After refactoring admin socket to dynamic import, `socket.io-client` should no longer appear in `.next/node_modules/` and can revert to `devDependencies`.
-
-### react-toastify — partial ssr:false coverage
-
-- **Context**: `ToastContainer` is guarded by `dynamic({ ssr: false })` in `RawLayout.tsx`, but `toastr.ts` imports `{ toast }` and type imports statically.
-- **Findings**:
-  - Even if `ToastContainer` is client-only, the `toast` function import in `toastr.ts` is a static module-level import, causing Turbopack to externalise `react-toastify`.
-  - Refactoring `toast` calls to use `await import('react-toastify').then(m => m.toast(...))` would remove the static import. However, `toastr.ts` is a utility used broadly; async toast calls would change its API surface.
-  - Alternative: Accept `react-toastify` as a `dependencies` entry given its small size.
-- **Implications**: The type imports (`ToastContent`, `ToastOptions`) are erased at runtime and do not cause externalisation; only the value import `{ toast }` matters. Simplest path: keep in `dependencies`.
-
-### Packages with no src/ imports
-
-- **Context**: `i18next-http-backend`, `i18next-localstorage-backend`, `react-dropzone` — no imports found in `apps/app/src/`.
-- **Findings**:
-  - These may be imported in shared packages (`@growi/editor`, `@growi/core`) that are listed as workspace dependencies.
-  - Alternatively, they may be historical entries added but never used in `apps/app` directly.
-  - If they do NOT appear in `.next/node_modules/` after Phase 1 build, they are safe to remove from `devDependencies` entirely.
-- **Implications**: Investigate post-Phase 1. If not in `.next/node_modules/` and not in prod `node_modules`, remove from `devDependencies`.
-
----
-
-## Architecture Pattern Evaluation
-
-| Option | Description | Strengths | Risks | Notes |
-|--------|-------------|-----------|-------|-------|
-| Move-all-to-deps (flat fix) | Move all 23 packages to `dependencies` and stop | Immediate fix, zero code changes | Large production artifact, wrong semantics | Acceptable as Phase 1 baseline only |
-| Phased minimisation | Phase 1 fix + systematic revert via ssr:false / dynamic import / removal | Minimal production artifact, correct semantics | More effort, requires per-package verification | **Selected approach** |
-| cp -rL (.next/node_modules) | Resolve symlinks to physical files | Self-contained | Breaks transitive dep resolution (use-sync-external-store issue) | Rejected — symlink rewrite is correct approach |
-
----
-
-## Design Decisions
-
-### Decision: Symlink Rewrite over cp -rL
-
-- **Context**: `.next/node_modules/` symlinks point to workspace root `.pnpm/`; release image only has `apps/app/node_modules/.pnpm/`.
-- **Alternatives Considered**:
-  1. `cp -rL` — copy physical files, loses pnpm sibling resolution
-  2. Symlink rewrite (`../../../../ → ../../`) — updates symlink targets to deployed `.pnpm/`
-- **Selected Approach**: Symlink rewrite in `assemble-prod.sh`
-- **Rationale**: Preserves pnpm's isolated structure; transitive deps (e.g., `use-sync-external-store` for `swr`) remain resolvable via `.pnpm/` sibling pattern.
-- **Trade-offs**: Requires `find -maxdepth 2` to handle both depth-1 and depth-2 symlinks under `@scope/` directories.
-
-### Decision: Phased Revert Strategy
-
-- **Context**: Rather than leaving all 23 in `dependencies` permanently, systematically revert where safe.
-- **Selected Approach**: Phase 1 (all to deps) → Phase 2 (server-side import removal) → Phase 3 (ssr:false) → Phase 4 (ambiguous) → Phase 5 (validate & document)
-- **Rationale**: Each phase is independently verifiable; Phase 1 unblocks CI immediately.
-
-### Decision: @emoji-mart/data — Accept as Production Dependency (Phase 1)
-
-- **Context**: Static JSON import used for server-side emoji processing.
-- **Selected Approach**: Move to `dependencies`; defer extraction of a static lookup file to Phase 2 investigation.
-- **Rationale**: The data is genuinely needed server-side; extraction adds maintenance overhead that may not be worth the artifact size saving.
-
----
-
-## Risks & Mitigations
-
-- **Phase 3 ssr:false causes hydration mismatch** — Mitigation: test each wrapped component in browser before reverting the package; use React hydration warnings as signal.
-- **Phase 4 admin socket refactor breaks admin panel** — Mitigation: existing `global-socket.ts` dynamic pattern serves as verified template; unit test admin socket atom.
-- **Turbopack version change alters externalisation heuristics** — Mitigation: `assemble-prod.sh` includes a post-rewrite check via production server smoke test; Req 5.3 enforces no devDeps in `.next/node_modules/`.
-- **Phantom packages (i18next-*, react-dropzone) are transitive** — Mitigation: verify by checking `.next/node_modules/` contents post-Phase-1; remove only after confirming absence.
-
----
-
-## References
-
-- pnpm deploy documentation — `pnpm deploy` flags and node-linker behaviour
-- Next.js Pages Router SSR — all pages render server-side by default; `dynamic({ ssr: false })` is the only opt-out
-- Turbopack externalisation — packages in `.next/node_modules/` are loaded at runtime, not bundled
-
----
-
-## Session 2: Production Implementation Discoveries
-
-### Finding: `ssr: false` does NOT prevent Turbopack externalisation
-
-**Pre-implementation assumption**: Wrapping a component with `dynamic({ ssr: false })` would remove its package dependencies from `.next/node_modules/`.
-
-**Reality**: Turbopack performs static import analysis on the dynamically-loaded file and still externalises packages found there. `ssr: false` only skips HTML rendering — it does not affect which packages are added to `.next/node_modules/`. This invalidated the entire Phase 3 plan (wrapping `diff2html`, `react-dnd`, `@handsontable/react` with `ssr: false`).
-
-**Only two techniques actually remove a package from `.next/node_modules/`**:
-1. Replace the static import with `import()` inside `useEffect` and ensure no other static import path exists in the SSR code graph (e.g., `socket.io-client` in `admin/states/socket-io.ts`).
-2. Replace the runtime package with a bundled static alternative (e.g., `@emoji-mart/data` → `emoji-native-lookup.json`).
-
-**Exception**: `fslightbox-react` remains in `.next/node_modules/` as a broken symlink but is harmless — `useEffect` never runs during SSR, so the broken symlink is never accessed.
-
----
-
-### Finding: Initial survey of 23 packages was incomplete
-
-The design identified 23 packages to move from `devDependencies` to `dependencies`. During implementation, 19 additional packages were found in `.next/node_modules/`:
-
-- `@codemirror/*` (multiple packages), `codemirror`, `codemirror-emacs`, `codemirror-vim`, `codemirror-vscode-keymap`
-- `@lezer/highlight`
-- `@marp-team/marp-core`, `@marp-team/marpit`
-- `@emoji-mart/react`
-- `reveal.js`, `pako`, `cm6-theme-basic-light`, `y-codemirror.next`
-
-All 42 packages (23 + 19) were moved to `dependencies`. Lesson: always run the Level 1 check (`ls apps/app/.next/node_modules/`) after a production build to get the authoritative list.
-
----
-
-### Finding: `assemble-prod.sh` had two bugs
-
-1. **`set -e` + `[ ... ] && ...` pattern**: Under `set -e`, a `[ condition ] && command` expression exits the script with failure when the condition is false (exit code 1). Fixed by wrapping in `if/then`.
-2. **`.next/node_modules/` symlink rewrite was missing**: The script rewrote `apps/app/node_modules/` symlinks but did not rewrite `.next/node_modules/` symlinks. These still pointed to `../../../../node_modules/.pnpm/` (workspace root), which does not exist in production. Both rewrites are now present.
-
----
-
-### Finding: Packages successfully reverted to `devDependencies`
-
-Only 3 of the 23 originally moved packages were successfully reverted:
-
-| Package | Technique |
-|---------|-----------|
-| `@emoji-mart/data` | Replaced with bundled `emoji-native-lookup.json` extracted at build time |
-| `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` |
-
-All other packages remain in `dependencies` because either `ssr: false` wrapping failed to remove them from `.next/node_modules/` or they are genuinely needed at SSR runtime.
-
----
-
-### Finding: CI symlink integrity check added
-
-`check-next-symlinks.sh` was added to the `build-prod` CI job (runs after `assemble-prod.sh`) to detect broken symlinks in `.next/node_modules/` automatically. This prevents future classification regressions regardless of which code paths are exercised at runtime by `server:ci`. The `fslightbox-react` exception is hardcoded in the script.
-
----
-
-## Session 3: Assembly Pipeline Simplification
-
-### Finding: pnpm v10 `--legacy` creates self-contained symlinks (step [1b] eliminated)
-
-**Verification** — `pnpm deploy out --prod --legacy --filter @growi/app` in pnpm v10.32.1:
-- `out/node_modules/react` → `.pnpm/react@18.2.0/node_modules/react` (self-contained ✓)
-- `out/node_modules/@codemirror/state` → `../.pnpm/@codemirror+state@6.5.4/node_modules/@codemirror/state` ✓
-
-**NOT** `../../../node_modules/.pnpm/...` (workspace-root-pointing) as in pre-v10 pnpm.
-
-**Implication**: Step [1b] is no longer needed. The root cause of step [1b] was placing the deploy output at `apps/app/` (not `--legacy` itself). Removing `--legacy` would require `inject-workspace-packages=true` in `.npmrc` with no practical benefit — keeping `--legacy` is the simpler choice.
-
-**pnpm version sensitivity**: If downgrading below pnpm v10, `--legacy` may again produce workspace-root-pointing symlinks requiring step [1b] to be reinstated. Verify with `readlink out/node_modules/react` — must start with `.pnpm/`.
-
----
-
-### Finding: `.next/node_modules/` symlink path analysis (step [2] eliminated)
-
-**Analysis of actual `.next/node_modules/` contents** (Turbopack original output, before any rewrite):
-- Non-scoped packages: `../../../../node_modules/.pnpm/<pkg>/...` (4 levels up from `.next/node_modules/` = Docker workspace root)
-- Scoped packages (`@scope/pkg`): `../../../../../node_modules/.pnpm/<pkg>/...`
-
-Step [2] in the old `assemble-prod.sh` rewrote these from `../../../../` to `../../` to point to `apps/app/node_modules/`. If the workspace-root `node_modules/` contains the prod deps instead, the original Turbopack symlinks resolve correctly without any rewriting.
-
-**Implication**: Placing the deploy output at workspace root eliminates step [2] entirely.
-
----
-
-### Design Decisions
-
-#### Decision: Workspace-root staging (eliminates step [2])
-
-- **Alternatives**:
-  1. Keep `apps/app/node_modules/` placement, apply step [2] rewrite (old approach)
-  2. `pnpm install --prod` post-build — workspace packages remain as symlinks, requiring `packages/` in release image
-  3. Place deploy output at workspace root, no rewrite needed (selected)
-- **Selected**: `rm -rf node_modules && mv out/node_modules node_modules` + `ln -sfn ../../node_modules apps/app/node_modules`
-- **Rationale**: Turbopack's original symlink targets (`../../../../node_modules/.pnpm/`) already point to workspace root. Preserving this structure requires no rewriting.
-- **Trade-off**: `rm -rf node_modules` destroys the full-deps workspace root `node_modules/` locally — developers must run `pnpm install` to restore after local testing.
-
-#### Decision: Keep `--legacy` in `pnpm deploy` (eliminates step [1b])
-
-- **Context**: In pnpm v10, `--legacy` produces self-contained `.pnpm/` symlinks regardless. The `--legacy` flag is now a pnpm v10 gate-bypass (skips `inject-workspace-packages` check), not a linker-mode selector.
-- **Alternatives**: Remove `--legacy` — requires `inject-workspace-packages=true` in `.npmrc`; same symlink output but adds config dependency.
-- **Selected**: Keep `--legacy` (no `.npmrc` changes required).
-
-#### Decision: `apps/app/node_modules` as a symlink to `../../node_modules`
-
-- **Context**: The `migrate` script in `apps/app/package.json` uses path `node_modules/migrate-mongo/...` relative to `apps/app/`. With deploy output at workspace root, this direct path would fail without the symlink.
-- **Selected**: `ln -sfn ../../node_modules apps/app/node_modules`
-- **Rationale**: Satisfies migration script path + Node.js `require()` traversal without duplicating the `.pnpm/` store. `cp -a` and Docker BuildKit `COPY` both preserve symlinks correctly.

+ 0 - 23
.kiro/specs/optimise-deps-for-prod/spec.json

@@ -1,23 +0,0 @@
-{
-  "feature_name": "optimise-deps-for-prod",
-  "created_at": "2026-03-12T05:00:00Z",
-  "updated_at": "2026-03-17T00:00:00Z",
-  "language": "en",
-  "phase": "implementation-complete",
-  "cleanup_completed": true,
-  "approvals": {
-    "requirements": {
-      "generated": true,
-      "approved": true
-    },
-    "design": {
-      "generated": true,
-      "approved": true
-    },
-    "tasks": {
-      "generated": true,
-      "approved": true
-    }
-  },
-  "ready_for_implementation": true
-}

+ 0 - 151
.kiro/specs/optimise-deps-for-prod/tasks.md

@@ -1,151 +0,0 @@
-# Implementation Plan
-
-## Task Overview
-
-| Phase | Major Task | Sub-tasks | Requirements |
-|-------|-----------|-----------|--------------|
-| 1 | Baseline fix — move 23 packages | 1.1, 1.2 | 1.1–1.5 |
-| 2 | Eliminate `@emoji-mart/data` server import | 2.1, 2.2 | 2.1–2.4 |
-| 3 | Apply `ssr: false` to Group 1 components | 3.1–3.5 | 3.1–3.5 |
-| 4 | Resolve ambiguous packages | 4.1–4.5 | 4.1–4.5 |
-| 5 | Final validation and documentation | 5.1, 5.2 | 5.1–5.5 |
-
----
-
-- [x] 1. Move all 23 Turbopack-externalised packages from `devDependencies` to `dependencies`
-
-- [x] 1.1 Edit `apps/app/package.json` to reclassify all 23 packages
-  - Move the following entries from the `devDependencies` section to the `dependencies` section, preserving alphabetical order within each section: `@codemirror/state`, `@emoji-mart/data`, `@handsontable/react`, `@headless-tree/core`, `@headless-tree/react`, `@tanstack/react-virtual`, `bootstrap`, `diff2html`, `downshift`, `fastest-levenshtein`, `fslightbox-react`, `i18next-http-backend`, `i18next-localstorage-backend`, `pretty-bytes`, `react-copy-to-clipboard`, `react-dnd`, `react-dnd-html5-backend`, `react-dropzone`, `react-hook-form`, `react-input-autosize`, `react-toastify`, `simplebar-react`, `socket.io-client`
-  - Run `pnpm install --frozen-lockfile` from the monorepo root after editing to verify the lock file remains valid; if it fails (lock file mismatch), run `pnpm install` to regenerate it
-  - _Requirements: 1.1_
-
-- [x] 1.2 Verify the production server starts cleanly after the package reclassification
-  - Run `bash apps/app/bin/assemble-prod.sh` from the monorepo root to produce the release artifact
-  - Start the production server with `pnpm run server` and confirm no `ERR_MODULE_NOT_FOUND` or `Failed to load external module` errors appear in stdout
-  - Send a GET request to `/login` and assert HTTP 200; confirm the server logs show no SSR errors for the login page
-  - **Result**: HTTP 200 on `/login`, no `ERR_MODULE_NOT_FOUND` errors. Server starts cleanly.
-  - **Note**: `assemble-prod.sh` deletes `next.config.ts`; restore it after with `git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts`
-  - _Requirements: 1.2, 1.3, 1.4, 1.5_
-
----
-
-- [x] 2. Replace the `@emoji-mart/data` runtime dependency with a bundled static lookup file
-
-- [x] 2.1 Extract the minimal emoji lookup data from `@emoji-mart/data` into a static JSON file
-  - Confirmed: only fields consumed are `emojiData.emojis[name]?.skins[0].native`
-  - Created `apps/app/bin/extract-emoji-data.cjs` extraction script
-  - Generated `apps/app/src/services/renderer/remark-plugins/emoji-native-lookup.json` (1870 entries)
-  - _Requirements: 2.1_
-
-- [x] 2.2 Refactor `emoji.ts` to use the static lookup file and revert `@emoji-mart/data` to `devDependencies`
-  - Replaced `import emojiData from '@emoji-mart/data/sets/15/native.json'` with `import emojiNativeLookup from './emoji-native-lookup.json'`
-  - Moved `@emoji-mart/data` from `dependencies` back to `devDependencies`
-  - Build confirmed green (TypeScript type cast fixed with `as unknown as Record<...>`)
-  - _Requirements: 2.2, 2.3, 2.4_
-
----
-
-- [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`
-  - 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)
-  - Moved `fslightbox-react` from `dependencies` to `devDependencies`
-  - **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_
-
-- [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`
-  - 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.
-  - **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_
-
-- [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
-  - Updated `PageTree.tsx` to load `PageTreeWithDnD` via `dynamic({ ssr: false })`
-  - 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.
-  - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
-
-- [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
-  - 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.
-  - _Requirements: 3.1, 3.2, 3.4, 3.5_
-
-- [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)
-  - **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_
-
----
-
-- [x] 4. Resolve ambiguous and phantom package classifications
-
-- [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`)
-  - `react-toastify` justified as production dependency; documented in `.kiro/steering/tech.md`
-  - _Requirements: 4.1_
-
-- [x] 4.2 (P) Refactor `admin/states/socket-io.ts` to use a dynamic import and verify `socket.io-client` leaves `dependencies`
-  - Replaced static `import io from 'socket.io-client'` with `const { default: io } = await import('socket.io-client')` inside `useEffect`
-  - Adopted `atom<Socket | null>(null)` + `useSetupAdminSocket` hook pattern (matching `global-socket.ts`)
-  - Added `useSetupAdminSocket()` call to `AdminLayout.tsx`
-  - Moved `socket.io-client` from `dependencies` to `devDependencies`
-  - All consumers already guard for `null` socket (no breaking changes)
-  - _Requirements: 4.2_
-
-- [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
-  - 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.)
-  - _Requirements: 4.3_
-
-- [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/`
-  - 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.
-  - _Requirements: 4.4_
-
-- [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`~~
-  - ~~`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.
-  - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
-
----
-
-- [x] 5. Final validation and documentation
-
-- [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 `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.
-  - **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`.** `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):
-    ```bash
-    cd apps/app && find .next/node_modules -maxdepth 2 -type l | while read 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
-    ```
-  - Assert that no package listed in `devDependencies` in `apps/app/package.json` appears in `apps/app/.next/node_modules/` (no classification regression)
-  - Start the production server in background: `cd apps/app && pnpm run server > /tmp/server.log 2>&1 &`
-    - **Do NOT run mongosh/mongo for DB connectivity checks** — the server will connect automatically; check logs instead
-    - Wait for log line: `Express server is listening on port 3000`
-  - **HTTP check — use root URL, NOT /login**: `curl -s -o /tmp/response.html -w "%{http_code}" http://localhost:3000/`
-    - `/login` is not a valid smoke test: it returns HTTP 200 even when SSR is broken
-    - The root page `/` triggers SSR of editor-related components and fails with HTTP 500 when packages are missing
-    - 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)`
-  - Restore `next.config.ts`: `git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts`
-  - **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.
-  - _Requirements: 5.1, 5.2, 5.3, 5.4_
-
-- [x] 5.2 Add Turbopack externalisation rule documentation
-  - Added "Turbopack Externalisation Rule" section to `.kiro/steering/tech.md` under "Import Optimization Principles"
-  - Documents: which packages must be in `dependencies`, how to make a package devDep-eligible, list of justified production dependencies
-  - _Requirements: 5.5_