Browse Source

WIP: reclassify deps

Yuki Takei 3 weeks ago
parent
commit
ea9c1018ae

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@
 
 
 # dependencies
 # dependencies
 node_modules
 node_modules
+node_modules.*
 /.pnp
 /.pnp
 .pnp.js
 .pnp.js
 .pnpm-store
 .pnpm-store

+ 405 - 0
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/design.md

@@ -0,0 +1,405 @@
+# Design Document: optimise-deps-for-prod-with-turbo-prune
+
+## Overview
+
+The GROWI production Docker build uses a multi-stage Dockerfile: `pruner` → `deps` → `builder` → `release`. The `pruner` stage already runs `turbo prune @growi/app @growi/pdf-converter --docker` to create a minimal monorepo subset. However, the `builder` stage still calls `assemble-prod.sh`, which contains two fragile post-build symlink-rewriting steps using `sed` and `find`:
+
+- **Step [1b]**: Rewrites `apps/app/node_modules/` top-level symlinks from the workspace-root `.pnpm/` path (an artifact of `pnpm deploy --legacy`) to local `apps/app/node_modules/.pnpm/` paths.
+- **Step [2]**: Rewrites `.next/node_modules/` symlinks (generated by Turbopack) from `../../../../node_modules/.pnpm/` (workspace root in Docker) to `../../node_modules/.pnpm/` (`apps/app/node_modules/`).
+
+Both steps exist because of two coupled design decisions: (1) the `--legacy` flag in `pnpm deploy`, and (2) placing the deploy output at `apps/app/node_modules/` instead of the workspace root.
+
+This design removes both root causes: dropping `--legacy` makes `pnpm deploy` create self-contained relative symlinks (eliminating step [1b]); staging the deploy output at workspace root instead of `apps/app/` means Turbopack's original symlink targets already resolve correctly (eliminating step [2]).
+
+**Purpose**: Eliminate fragile bash symlink manipulation from the production assembly pipeline, replacing it with a structure that exploits the self-contained properties of `turbo prune` + `pnpm deploy` (without `--legacy`).
+
+**Users**: Release engineers and CI/CD pipelines maintaining the production Docker build.
+
+**Impact**: Modifies `apps/app/bin/assemble-prod.sh` (removes ~15 lines) and the `builder` stage artifact staging step in `apps/app/docker/Dockerfile`. No changes to source code, Next.js configuration, or dependency classifications.
+
+### Goals
+- Eliminate step [1b] and step [2] from `assemble-prod.sh` by fixing their root causes
+- Reduce `assemble-prod.sh` to two operations: remove `.next/cache` and remove `next.config.ts`
+- Maintain production server correctness (`GET /` → HTTP 200, zero `ERR_MODULE_NOT_FOUND`)
+- Preserve Docker layer caching behavior (deps layer cached when only source changes)
+
+### Non-Goals
+- Changes to dependency classification (`dependencies` vs `devDependencies`) — handled by `optimise-deps-for-prod` spec
+- Migration to Next.js standalone output mode
+- Changes to the `pruner` or `deps` Docker stages
+- Changes to the Express server, database migrations, or application logic
+
+---
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Flows |
+|-------------|---------|------------|-------|
+| 1.1–1.4 | Eliminate symlink rewrite steps | `assemble-prod.sh` | Assembly Flow |
+| 2.1–2.4 | `pnpm deploy --prod` (no `--legacy`) creates prod-only workspace-root `node_modules/` | `assemble-prod.sh` | Assembly Flow |
+| 3.1–3.4 | Release artifact has `node_modules/` at workspace root; `apps/app/node_modules` is a symlink | `assemble-prod.sh`, Dockerfile staging | Assembly Flow, Release Image |
+| 4.1–4.5 | Production server starts, `GET /` returns 200, no broken symlinks | All components | Validation |
+| 5.1–5.5 | `pruner` stage unchanged; Docker layer caching preserved | Dockerfile | Build Flow |
+| 6.1–6.4 | All `.next/node_modules/` packages are in `dependencies`; dep rule from `optimise-deps-for-prod` remains valid | (inherited, no new code) | — |
+
+---
+
+## Architecture
+
+### Existing Architecture Analysis
+
+The production assembly pipeline (current):
+
+```
+turbo run build --filter @growi/app
+  └─ Turbopack → apps/app/.next/
+       └─ .next/node_modules/<pkg> → ../../../../node_modules/.pnpm/<pkg>/...
+                                      (workspace root in Docker = /opt/)
+
+assemble-prod.sh (current):
+  [1]   pnpm deploy out --prod --legacy --filter @growi/app
+        └─ out/node_modules/<pkg> → ../../../node_modules/.pnpm/<pkg>/...  ← points to workspace root
+  [mv]  rm -rf apps/app/node_modules && mv out/node_modules apps/app/node_modules
+        └─ apps/app/node_modules/<pkg> → still points to workspace root ✗
+  [1b]  find + sed: rewrite apps/app/node_modules/ symlinks
+        └─ apps/app/node_modules/<pkg> → .pnpm/<pkg>/...  (local) ✓
+  [2]   find + sed: rewrite .next/node_modules/ symlinks
+        └─ ../../../../node_modules/.pnpm/ → ../../node_modules/.pnpm/
+        └─ .next/node_modules/<pkg> → ../../node_modules/.pnpm/...  (= apps/app/node_modules/) ✓
+  [3]   rm -rf .next/cache
+  [4]   rm -f next.config.ts
+```
+
+Root causes of steps [1b] and [2]:
+- **[1b] root cause**: `--legacy` linker creates top-level symlinks pointing to the workspace-root `.pnpm/` store, not the deploy output's local `.pnpm/`
+- **[2] root cause**: Deploy output is placed at `apps/app/`, but Turbopack builds `.next/node_modules/` symlinks relative to the workspace root (4 levels up)
+
+### 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 no 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**:
+- Drop `--legacy`: isolated linker creates self-contained `out/node_modules/` with relative `.pnpm/` symlinks — no step [1b] needed
+- Place deploy output at workspace root: Turbopack's `../../../../node_modules/.pnpm/` symlinks already point to workspace root — no step [2] needed
+- `apps/app/node_modules` → symlink `../../node_modules` for `migrate-mongo` script compatibility and Node.js `require()` traversal
+
+### Technology Stack
+
+| Layer | Choice | Role | Notes |
+|-------|--------|------|-------|
+| Build orchestration | Turborepo `turbo prune --docker` | Generates minimal monorepo subset for Docker | `pruner` stage — unchanged |
+| Package manager | pnpm (current version) | `pnpm deploy --prod` (no `--legacy`) | Removes `--legacy`; isolated linker |
+| Build assembly | `assemble-prod.sh` | Simplified: deploy → stage (2 ops remain) | Removes steps [1b] and [2] |
+| Container | Docker BuildKit `COPY` | Copies symlinks intact | Requires BuildKit (already in use) |
+
+---
+
+## System Flows
+
+### Assembly Flow: Current vs New
+
+```mermaid
+graph TB
+    subgraph Current
+        C1[pnpm deploy --prod --legacy]
+        C2[mv out/node_modules apps/app/node_modules]
+        C3[step 1b rewrite apps/app/node_modules symlinks]
+        C4[step 2 rewrite .next/node_modules symlinks]
+        C5[rm cache next.config.ts]
+        C1 --> C2 --> C3 --> C4 --> C5
+    end
+
+    subgraph New
+        N1[pnpm deploy --prod no legacy]
+        N2[rm node_modules mv out/node_modules node_modules]
+        N3[ln apps/app/node_modules to ws root]
+        N4[rm cache next.config.ts]
+        N1 --> N2 --> N3 --> N4
+    end
+```
+
+### Symlink Resolution in Release Image
+
+```mermaid
+graph LR
+    subgraph ReleaseImage at opt/growi
+        NM[node_modules from pnpm deploy]
+        PNPM[node_modules .pnpm react at 18.2.0]
+        AppNMLink[apps/app/node_modules symlink to dotdot/dotdot/node_modules]
+        NextNM[apps/app/.next/node_modules react-xxx symlink to 4-levels-up]
+        MigrateScript[migrate script node_modules/migrate-mongo]
+    end
+
+    NextNM -->|../../../../node_modules/.pnpm/| PNPM
+    AppNMLink -->|../../node_modules/| NM
+    NM --> PNPM
+    MigrateScript -->|via apps/app/node_modules symlink| NM
+```
+
+---
+
+## Components and Interfaces
+
+| Component | Domain | Intent | Req Coverage | Key Dependencies | Contracts |
+|-----------|--------|--------|--------------|------------------|-----------|
+| `assemble-prod.sh` | Build Assembly | Assemble prod artifact: deploy prod deps, stage symlinks, clean | 1.1–1.4, 2.1–2.4, 3.1–3.3 | pnpm, filesystem | Batch |
+| Dockerfile `builder` staging step | Container Build | Copy release artifact to `/tmp/release/` with correct structure | 3.1, 3.3, 5.1–5.3 | Docker BuildKit COPY | Batch |
+| Release image structure | Container Runtime | `node_modules/` at workspace root; `apps/app/node_modules` symlink | 3.2, 3.4, 4.1–4.4 | `assemble-prod.sh` output | State |
+
+### Build Assembly
+
+#### `apps/app/bin/assemble-prod.sh`
+
+| Field | Detail |
+|-------|--------|
+| Intent | Produce a production-ready artifact by deploying prod-only deps to workspace root and creating `apps/app/node_modules` compatibility symlink |
+| Requirements | 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3 |
+
+**Responsibilities & Constraints**
+- Run from workspace root (same CWD as current usage)
+- Deploy production dependencies using `pnpm deploy out --prod --filter @growi/app` (no `--legacy`)
+- Replace workspace root `node_modules/` (full deps) with deploy output (prod-only)
+- Create `apps/app/node_modules` as a symlink to `../../node_modules` for migration script and Node.js resolution compatibility
+- Remove `.next/cache` to reduce release image size
+- Remove `next.config.ts` to prevent Next.js from attempting TypeScript install at server startup
+
+**Constraints**:
+- Must run AFTER `pnpm deploy` output is created (pnpm requires workspace root `node_modules/` during deploy)
+- Must run AFTER `turbo run build` (`.next/` must exist for cache removal)
+- `rm -rf node_modules` destroys the workspace root's full-deps `node_modules/`; developers running this locally must run `pnpm install` to restore the development environment
+
+**Contracts**: Batch [x]
+
+##### Batch / Job Contract
+- Trigger: Called by Dockerfile `builder` stage via `RUN bash apps/app/bin/assemble-prod.sh`; also callable locally
+- Input: Workspace root with `node_modules/` (full deps) and `apps/app/.next/` (built)
+- Output:
+  - `node_modules/` at workspace root (prod-only, self-contained relative symlinks)
+  - `apps/app/node_modules` → symlink `../../node_modules`
+  - `apps/app/.next/` without `.next/cache/`
+  - `apps/app/next.config.ts` removed
+- Idempotency: Re-runnable; `rm -rf out` at start cleans previous output
+
+**Implementation Notes**
+- Integration: Replaces `pnpm deploy out --prod --legacy` with `pnpm deploy out --prod` and changes `mv out/node_modules apps/app/node_modules` to `rm -rf node_modules && mv out/node_modules node_modules && ln -sfn ../../node_modules apps/app/node_modules`; removes step [1b] and step [2] bash blocks entirely
+- Validation: Verify that `out/node_modules/react` symlink target starts with `.pnpm/` (not `../../../node_modules/.pnpm/`) after running `pnpm deploy out --prod --filter @growi/app`
+- Risks: pnpm version may affect isolated linker behavior — if `pnpm deploy` without `--legacy` still creates workspace-root-pointing symlinks, step [1b] would need to be reinstated. Document pnpm version requirement.
+
+---
+
+#### Dockerfile `builder` stage — artifact staging step
+
+| Field | Detail |
+|-------|--------|
+| Intent | Copy the production artifact (workspace root `node_modules/` + `apps/app/` contents) into `/tmp/release/` for the `COPY --from=builder` instruction in the `release` stage |
+| Requirements | 3.1, 3.3, 5.1, 5.2, 5.3 |
+
+**Responsibilities & Constraints**
+- Copy `node_modules/` from workspace root (prod-only, after `assemble-prod.sh`) to `/tmp/release/node_modules/`
+- Copy `apps/app/node_modules` **as a symlink** (not dereferenced) to `/tmp/release/apps/app/node_modules`
+- Use `cp -a` (includes `-d`/`-P` flag: no symlink dereferencing) for all copies
+- Docker BuildKit `COPY` preserves symlinks; verify with `RUN test -L apps/app/node_modules` if needed
+
+**Contracts**: Batch [x]
+
+##### Batch / Job Contract
+- Trigger: `RUN ...` step in Dockerfile `builder` stage, after `RUN bash apps/app/bin/assemble-prod.sh`
+- Input: `/opt/node_modules/` (prod-only) + `/opt/apps/app/` (with `.next/`, `dist/`, etc.) + `/opt/apps/app/node_modules` (symlink)
+- Output: `/tmp/release/` with:
+  ```
+  /tmp/release/
+  ├── package.json
+  ├── node_modules/              ← workspace-root prod node_modules
+  └── apps/app/
+      ├── .next/                 ← symlinks: ../../../../node_modules/.pnpm/ (resolvable in release)
+      ├── dist/
+      ├── config/
+      ├── public/
+      ├── resource/
+      ├── tmp/
+      ├── package.json
+      └── node_modules           ← symlink: ../../node_modules (preserved)
+  ```
+
+**Implementation Notes**
+- Integration: Change `cp -a apps/app/node_modules /tmp/release/apps/app/` to `cp -a node_modules /tmp/release/` and add `cp -a apps/app/node_modules /tmp/release/apps/app/` (symlink preserved by `-a`). Remove `apps/app/node_modules` from the existing long `cp -a` list that copies `.next`, `config`, `dist`, etc.
+- Validation: In Dockerfile, add `RUN test -L /tmp/release/apps/app/node_modules` to assert symlink preservation before the release stage
+- Risks: `cp -a` on a symlink-to-directory may dereference on some OS/busybox versions — test on the actual base image
+
+---
+
+### Release Image Structure
+
+The `release` stage copies `/tmp/release/` to `${appDir}` (e.g. `/opt/growi/`):
+
+```
+/opt/growi/
+├── package.json
+├── node_modules/              ← prod-only (pnpm deploy output, self-contained)
+│   ├── .pnpm/
+│   │   ├── react@18.2.0/
+│   │   │   └── node_modules/react/  ← physical package files
+│   │   └── @growi+core@.../
+│   │       └── node_modules/@growi/core/  ← physically injected by pnpm deploy
+│   └── react                 ← symlink: .pnpm/react@18.2.0/node_modules/react  ✓
+└── apps/
+    └── app/
+        ├── .next/
+        │   └── node_modules/
+        │       └── react-xxx  ← symlink: ../../../../node_modules/.pnpm/react@18.2.0/...  ✓
+        ├── dist/
+        ├── node_modules       ← symlink: ../../node_modules → /opt/growi/node_modules/  ✓
+        └── package.json
+```
+
+**Resolution chains**:
+- `.next/node_modules/react-xxx` → `../../../../node_modules/.pnpm/react@18.2.0/node_modules/react` → `/opt/growi/node_modules/.pnpm/react@18.2.0/node_modules/react` ✓
+- `apps/app/node_modules/migrate-mongo` → via symlink `../../node_modules` → `/opt/growi/node_modules/migrate-mongo` ✓
+- Node.js `require('express')` from `apps/app/dist/server/app.js` → traverses to `apps/app/node_modules/` (symlink) → `/opt/growi/node_modules/express` ✓
+
+---
+
+## Testing Strategy
+
+### Production Server Startup Procedure (updated)
+
+The procedure from `optimise-deps-for-prod/design.md` is simplified. With the new approach, after `assemble-prod.sh`, the workspace root's `node_modules/` IS already the prod-only deploy output. The `mv node_modules node_modules.bak` step is **no longer needed**.
+
+**Step 1 — Clean build**
+```bash
+turbo run build --filter @growi/app
+```
+
+**Step 2 — Production assembly**
+```bash
+bash apps/app/bin/assemble-prod.sh
+```
+
+> **Note**: `assemble-prod.sh` now does `rm -rf node_modules && mv out/node_modules node_modules`. After this, the workspace root's `node_modules/` is prod-only. `apps/app/node_modules` is a symlink. Run `pnpm install` to restore the development environment after testing.
+
+**Step 2b — Restore `next.config.ts`**
+```bash
+git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts
+```
+
+**Step 3 — Start production server** (from `apps/app/`)
+```bash
+cd apps/app && pnpm run server > /tmp/server.log 2>&1 &
+timeout 60 bash -c 'until grep -q "Express server is listening" /tmp/server.log; do sleep 2; done'
+```
+
+**Step 4 — Verify**
+```bash
+HTTP_CODE=$(curl -s -o /tmp/response.html -w "%{http_code}" http://localhost:3000/)
+echo "HTTP: $HTTP_CODE"  # → 200
+grep -c "ERR_MODULE_NOT_FOUND" /tmp/server.log  # → 0
+```
+
+**Step 5 — Stop and restore**
+```bash
+kill $(lsof -ti:3000)
+pnpm install  # restore full-deps node_modules for development
+```
+
+### Symlink Integrity Verification
+
+After `assemble-prod.sh`:
+```bash
+# Verify apps/app/node_modules is a symlink
+test -L apps/app/node_modules && echo "OK: symlink" || echo "FAIL: not a symlink"
+
+# Verify .next/node_modules symlinks resolve (no broken links)
+find apps/app/.next/node_modules -maxdepth 2 -type l | while read link; do
+  linkdir=$(dirname "$link"); target=$(readlink "$link")
+  resolved=$(cd "$linkdir" 2>/dev/null && realpath "$target" 2>/dev/null || echo "BROKEN")
+  [ "$resolved" = "BROKEN" ] && echo "BROKEN: $link"
+done
+# Expected: no output
+
+# Verify pnpm deploy without --legacy creates self-contained symlinks
+readlink out/node_modules/react 2>/dev/null  # Expected: .pnpm/react@.../node_modules/react (not ../../../...)
+```
+
+### Docker Build Verification
+
+After each change, verify Docker build produces a working release image:
+```bash
+docker build -f apps/app/docker/Dockerfile -t growi-test .
+docker run --rm growi-test ls /opt/growi/node_modules/.pnpm | head -5  # → packages listed
+docker run --rm growi-test test -L /opt/growi/apps/app/node_modules && echo "symlink OK"
+```
+
+---
+
+## Migration Strategy
+
+The change is non-breaking and phased:
+
+```mermaid
+graph LR
+    P1[Phase 1 verify pnpm deploy without legacy creates self-contained symlinks]
+    P2[Phase 2 update assemble-prod.sh remove legacy rm step 1b step 2 add new mv and ln]
+    P3[Phase 3 update Dockerfile staging add node_modules at workspace root]
+    P4[Phase 4 validate Docker build and production server startup]
+
+    P1 --> P2 --> P3 --> P4
+```
+
+**Rollback**: Each phase modifies only `assemble-prod.sh` and/or the Dockerfile staging step. Rolling back is a targeted revert of those two files. The production build pipeline (turbo prune, deps install, turbo build) is unchanged.
+
+**Phase 1 gate**: `readlink out/node_modules/react` must start with `.pnpm/` after `pnpm deploy out --prod --filter @growi/app` (without `--legacy`). If it starts with `../../../node_modules/`, the `--legacy` behavior is still present and the approach needs adjustment.
+
+---
+
+## Error Handling
+
+### Assembly Failures
+
+- **`pnpm deploy out --prod` fails**: The script exits with `set -euo pipefail`. Inspect pnpm output; check `pnpm-lock.yaml` is not dirty.
+- **`rm -rf node_modules` fails**: Unlikely; check filesystem permissions.
+- **`ln -sfn ../../node_modules apps/app/node_modules` fails**: Existing `apps/app/node_modules` is a real directory (from previous dev install). The `rm -rf apps/app/node_modules` step before `ln` handles this.
+- **Broken symlinks in release image**: Indicates a package from `.next/node_modules/` is missing from `dependencies` — classification regression from `optimise-deps-for-prod`. Run the devDependencies regression check:
+  ```bash
+  # verify no devDep appears in .next/node_modules/
+  ```
+
+### Production Server Failures
+- **`ERR_MODULE_NOT_FOUND`**: Package in `dependencies` was not included in `pnpm deploy` output, or `pnpm deploy` without `--legacy` behaves unexpectedly. Re-add `--legacy` as rollback.
+- **`TypeError: Cannot read properties of null (reading 'useContext')`**: Broken symlink in `node_modules/` (React resolved from wrong location). Verify `apps/app/node_modules` symlink integrity.

+ 101 - 0
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/requirements.md

@@ -0,0 +1,101 @@
+# Requirements Document
+
+## Project Description (Input)
+optimise-deps-for-prod-with-turbo-prune
+
+---
+
+## Introduction
+
+The GROWI monorepo's production Docker build already uses `turbo prune @growi/app --docker` in its `pruner` stage to create a minimal monorepo subset. However, the `builder` stage still calls `assemble-prod.sh`, which contains two complex, fragile symlink-rewriting steps:
+
+1. **Step [1b]**: Rewrites `apps/app/node_modules/` top-level symlinks from the workspace-root `.pnpm/` path (generated by `pnpm deploy --prod --legacy`) to the local `apps/app/node_modules/.pnpm/` path.
+2. **Step [2]**: Rewrites `.next/node_modules/` symlinks (generated by Turbopack during `next build`) from the pruned-root `.pnpm/` path to `apps/app/node_modules/.pnpm/`.
+
+These steps exist because `pnpm deploy --prod --legacy` creates top-level symlinks that point to the workspace-root `.pnpm/` store, and Turbopack creates `.next/node_modules/` symlinks pointing to the pruned-root `.pnpm/` store — neither of which exists in the release image.
+
+The key insight is that within a `turbo prune`-generated context, `pnpm install` creates a **self-contained** `node_modules/` where all top-level symlinks already point to the local `.pnpm/` store at the same workspace root. The `.next/node_modules/` symlinks generated by Turbopack during `next build` also point to that same workspace-root `.pnpm/`. If the release image preserves this relative directory structure (keeping `node_modules/` at the same level relative to `apps/app/`), **no symlink rewriting is needed**.
+
+This specification replaces the `pnpm deploy --prod` + symlink-rewrite assembly pattern with a `pnpm install --prod`-based approach that exploits the pruned monorepo context, eliminating the fragile bash symlink manipulation.
+
+---
+
+## Requirements
+
+### Requirement 1: Eliminate symlink rewrite steps from the production assembly pipeline
+
+**Objective:** As a release engineer, I want the production assembly pipeline to not require any post-build symlink rewriting, so that the assembly script is simple, reliable, and not sensitive to changes in pnpm or Turborepo internal symlink structures.
+
+#### Acceptance Criteria
+
+1. The build system shall not contain any `sed`-based symlink path rewriting for `apps/app/node_modules/` or `apps/app/.next/node_modules/` in the production assembly process.
+2. When the production assembly completes, every symlink in `apps/app/.next/node_modules/` shall resolve to an existing file without any path rewriting step having been executed.
+3. When the production assembly completes, every top-level symlink in the production `node_modules/` directory shall resolve to an existing file without any path rewriting step having been executed.
+4. The production assembly script (`assemble-prod.sh`) shall be reduced to only: removing `.next/cache` and removing `next.config.ts`.
+
+---
+
+### Requirement 2: Replace `pnpm deploy --prod` with `pnpm install --prod` in the pruned context
+
+**Objective:** As a release engineer, I want production dependency isolation to be achieved via `pnpm install --prod` within the `turbo prune`-generated workspace, so that the node_modules layout is naturally self-contained without requiring post-build fixups.
+
+#### Acceptance Criteria
+
+1. When the builder stage runs `pnpm install --prod --frozen-lockfile` after `next build`, the build system shall produce a `node_modules/` directory at the pruned workspace root that contains only production dependencies (those listed under `dependencies` in `apps/app/package.json`).
+2. After `pnpm install --prod`, the `node_modules/.pnpm/` store shall not contain any package that is listed exclusively in `devDependencies` in `apps/app/package.json`.
+3. The `node_modules/.pnpm/` store shall contain every package that appears in `apps/app/.next/node_modules/` after the production build.
+4. When the production `node_modules/` (pruned to prod-only) is placed at the workspace root level in the release image alongside `apps/app/`, all `.next/node_modules/` symlinks shall resolve correctly through the standard pnpm relative-path structure without modification.
+
+---
+
+### Requirement 3: Restructure the release artifact to include `node_modules/` at the workspace root level
+
+**Objective:** As a release engineer, I want the release artifact to preserve the pruned monorepo's directory structure (with `node_modules/` at the same level as `apps/app/`), so that Turbopack-generated `.next/node_modules/` symlinks resolve naturally.
+
+#### Acceptance Criteria
+
+1. The builder stage shall stage the release artifact with `node_modules/` at the workspace root level (alongside `apps/app/`), not inside `apps/app/node_modules/`.
+2. When the release image is launched, `apps/app/.next/node_modules/<package>` symlinks (which reference `../../../../node_modules/.pnpm/<package>/...`) shall resolve to files within the staged `node_modules/.pnpm/` directory.
+3. The release image shall not require `apps/app/node_modules/` to exist as a separate directory; Node.js module resolution for the Express server shall be satisfied by the workspace-root `node_modules/`.
+4. If `apps/app/node_modules/` is required for `pnpm run server` to resolve `apps/app` local dependencies (e.g. for `migrate-mongo`), the build system shall provide a mechanism to satisfy this without duplicating the `.pnpm/` store.
+
+---
+
+### Requirement 4: Maintain production server correctness
+
+**Objective:** As a release engineer, I want the production server to start and serve pages correctly after the assembly pipeline change, so that the simplification does not introduce any runtime regressions.
+
+#### Acceptance Criteria
+
+1. When the production server is started with `pnpm run server` in the release image, the build system shall not throw `ERR_MODULE_NOT_FOUND` or `Failed to load external module` errors.
+2. When a browser accesses the root page `/`, the GROWI server shall respond with HTTP 200 and render the page content without SSR errors in the server log.
+3. The GROWI server shall pass the existing `launch-prod` CI job (MongoDB 6.0 and 8.0) after this change.
+4. While the production server is running, zero broken symlinks shall exist in the `node_modules/` and `.next/node_modules/` directories (verified with workspace-root `node_modules` absent from the environment).
+5. The production server smoke test shall use `GET /` (not `/login`) as the verification URL, as `/login` returns HTTP 200 even when SSR is broken.
+
+---
+
+### Requirement 5: Preserve compatibility with the existing `turbo prune` Docker multi-stage build
+
+**Objective:** As a release engineer, I want the new assembly approach to integrate cleanly into the existing Dockerfile's multi-stage structure (`pruner` → `deps` → `builder` → `release`), so that Docker layer caching and build efficiency are maintained.
+
+#### Acceptance Criteria
+
+1. The `pruner` stage (`turbo prune @growi/app @growi/pdf-converter --docker`) shall remain unchanged; the simplification applies only to the `builder` stage and the `release` staging step.
+2. When only source files (not `package.json` or lockfile) change, the Docker layer cache for the dependency installation step shall remain valid (i.e., `pnpm install` shall not re-run unnecessarily).
+3. The `builder` stage shall produce the pruned, prod-only `node_modules/` without requiring any external tools beyond `pnpm` and standard Unix utilities.
+4. The `assemble-prod.sh` script shall remain as a thin wrapper callable from the `builder` stage, containing only the operations that cannot be expressed as a `RUN` step in the Dockerfile (e.g., `next.config.ts` removal and cache cleanup).
+5. If `assemble-prod.sh` is reduced to only two operations (cache removal and `next.config.ts` removal), those operations shall be inlinable directly into the Dockerfile as `RUN` steps, eliminating the need for `assemble-prod.sh` entirely.
+
+---
+
+### Requirement 6: Validate against the dep-classification baseline from `optimise-deps-for-prod`
+
+**Objective:** As a developer, I want the new assembly approach to be built on top of the correct `dependencies` vs `devDependencies` classification established in the `optimise-deps-for-prod` spec, so that the two workstreams are compatible and the correctness guarantees of that spec are preserved.
+
+#### Acceptance Criteria
+
+1. The build system shall verify that every package appearing in `apps/app/.next/node_modules/` after `next build` is listed under `dependencies` (not `devDependencies`) in `apps/app/package.json`.
+2. After `pnpm install --prod`, the `node_modules/.pnpm/` store shall contain all packages that were present in `.next/node_modules/` before the `--prod` pruning step.
+3. If any package appears in `.next/node_modules/` but is listed under `devDependencies`, the build system shall surface this as a detectable error (a broken symlink or `ERR_MODULE_NOT_FOUND`) that blocks the production server from starting.
+4. The dep-classification rule documented in `.kiro/steering/tech.md` (Turbopack Externalisation Rule) shall remain valid and apply to the new assembly approach: any package statically imported in SSR-executed code must be in `dependencies`.

+ 107 - 0
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/research.md

@@ -0,0 +1,107 @@
+# Research: optimise-deps-for-prod-with-turbo-prune
+
+## Summary
+- Feature: `optimise-deps-for-prod-with-turbo-prune`
+- Discovery Scope: Extension (modifying existing build pipeline)
+- Key Findings:
+  1. `pnpm deploy --prod --legacy` creates top-level symlinks pointing to the **workspace-root** `.pnpm/` store (not the deploy output's local `.pnpm/`). Removing `--legacy` creates self-contained relative symlinks within the deploy output directory.
+  2. Turbopack-generated `.next/node_modules/` symlinks for non-scoped packages use `../../node_modules/.pnpm/` path (2 levels up from `.next/node_modules/` = `apps/app/node_modules/`). After step [2] rewrite: same result. If `node_modules/` is placed at workspace root (4 levels up from `.next/node_modules/` = original Turbopack output `../../../../node_modules/.pnpm/`), no rewrite is needed.
+  3. The existing Dockerfile already uses `turbo prune @growi/app @growi/pdf-converter --docker` in the `pruner` stage. The `assemble-prod.sh` runs inside the Docker `builder` stage after `turbo run build`. The workspace root in Docker is `$OPT_DIR` (e.g. `/opt/`).
+
+## Research Log
+
+### Topic: `.next/node_modules/` symlink path analysis
+- Context: Determining why step [2] exists and what its target resolves to
+- Findings:
+  - Examined actual `.next/node_modules/@codemirror/` in devcontainer
+  - Non-scoped packages: `../../node_modules/.pnpm/<pkg>/...` (2 levels up from `.next/node_modules/` = `apps/app/node_modules/`)
+  - Scoped packages (inside `@scope/`): `../../../node_modules/.pnpm/<pkg>/...` (3 levels up from `@scope/` = `apps/app/node_modules/`)
+  - These are AFTER step [2] which rewrote from `../../../../` to `../../`
+  - Original Turbopack output (pre-rewrite): `../../../../node_modules/.pnpm/` (4 levels up from `.next/node_modules/` = Docker workspace root `/opt/node_modules/`)
+- Implications: If workspace root's `node_modules/` contains prod deps, the original Turbopack symlinks resolve correctly WITHOUT any rewriting
+
+### Topic: `pnpm deploy` symlink behavior with and without `--legacy`
+- Context: Understanding why step [1b] rewrites `apps/app/node_modules/` symlinks
+- Findings:
+  - With `--legacy`: creates "hoisted" node_modules where top-level symlinks reference the workspace root's `.pnpm/` store (not local). After `mv out/node_modules apps/app/node_modules`, these symlinks are broken in production.
+  - Without `--legacy`: pnpm's default isolated linker creates symlinks relative to the deploy output's local `.pnpm/` store. Verified: development `apps/app/node_modules/@codemirror/state` → `../.pnpm/@codemirror+state@6.5.4/node_modules/@codemirror/state` (relative, self-contained).
+  - `pnpm deploy` (with or without `--legacy`) physically INJECTS workspace packages (copies, not symlinks) into the deploy output.
+- Implications: Removing `--legacy` eliminates the need for step [1b] entirely.
+
+### Topic: `apps/app/node_modules` compatibility symlink for migration scripts
+- Context: The `migrate` script in `apps/app/package.json` uses path `node_modules/migrate-mongo/bin/migrate-mongo` (relative to `apps/app/`)
+- Findings:
+  - With the new structure (`node_modules/` at workspace root), the direct path `node_modules/migrate-mongo/...` from `apps/app/` would fail
+  - Creating `apps/app/node_modules` as a symlink to `../../node_modules` (workspace root) resolves this
+  - Node.js module resolution also follows symlinks, so `require()` calls work correctly
+  - `cp -a` preserves symlinks (uses `-d` flag, no dereferencing), so Docker COPY will preserve it
+- Implications: A single `ln -sfn ../../node_modules apps/app/node_modules` step replaces the complex `pnpm deploy + mv + symlink rewrite` chain
+
+### Topic: `turbo prune --docker` output structure (context7 official docs)
+- Sources: Turborepo official documentation via context7 (/vercel/turborepo)
+- Findings:
+  - `turbo prune <app> --docker` creates:
+    - `out/json/` – package.json files only (for Docker layer caching)
+    - `out/full/` – full source code needed for the target
+    - `out/pnpm-lock.yaml` – pruned lockfile
+  - After `pnpm install --frozen-lockfile` on the pruned `out/json/`, `node_modules/` is created at the workspace root of the pruned context with self-contained symlinks
+  - Official examples use Next.js standalone output mode (`output: 'standalone'`) which bundles needed modules into `.next/standalone/`, avoiding symlink issues entirely. GROWI does NOT use standalone mode.
+- Implications: GROWI's existing `turbo prune` usage in the Dockerfile is correct. The remaining issue is the `assemble-prod.sh` post-build assembly.
+
+### Topic: Release image directory structure requirements
+- Context: Determining what directory structure the release image needs for symlinks to resolve
+- Findings:
+  - Current release image: `${appDir}/apps/app/.next/` + `${appDir}/apps/app/node_modules/` (from pnpm deploy)
+  - `.next/node_modules/` symlinks (after step [2]): `../../node_modules/.pnpm/` → `apps/app/node_modules/.pnpm/` ✓
+  - Proposed release image: `${appDir}/node_modules/` + `${appDir}/apps/app/.next/`
+  - `.next/node_modules/` symlinks (NO rewrite, original Turbopack output): `../../../../node_modules/.pnpm/` → `${appDir}/node_modules/.pnpm/` ✓
+- Implications: Placing `node_modules/` (from pnpm deploy output) at workspace root level in release image, instead of `apps/app/node_modules/`, eliminates step [2] entirely.
+
+## Architecture Pattern Evaluation
+
+| Option | Description | Strengths | Risks / Limitations | Notes |
+|--------|-------------|-----------|---------------------|-------|
+| A: Remove `--legacy` only | Drop `--legacy` from `pnpm deploy`, keep `apps/app/node_modules/` placement | Minimal change; step [1b] eliminated | Step [2] still required | Partial improvement |
+| B: `pnpm install --prod` post-build | After build, run `pnpm install --prod --frozen-lockfile` in pruned context | Conceptually clean | Workspace packages remain as symlinks (need `packages/` in release) | Requires larger release image |
+| C: `pnpm deploy --prod` (no `--legacy`) + workspace-root staging | Remove `--legacy`, place deploy output at workspace root, create `apps/app/node_modules` symlink | Eliminates BOTH step [1b] AND step [2]; workspace packages still injected (no `packages/` needed) | Need to verify pnpm deploy without `--legacy` creates self-contained `.pnpm/` store | **Selected** |
+
+## Design Decisions
+
+### Decision: Remove `--legacy` from `pnpm deploy` (eliminates step [1b])
+- Context: `--legacy` linker creates symlinks pointing to workspace-root `.pnpm/`, requiring step [1b] rewriting
+- Alternatives:
+  1. Keep `--legacy`, rewrite symlinks (current approach)
+  2. Remove `--legacy`, symlinks become self-contained (selected)
+- Selected Approach: Remove `--legacy`
+- Rationale: Without `--legacy`, pnpm's default isolated linker creates relative symlinks within the deploy output. No rewriting needed after `mv`.
+- Trade-offs: Need to verify behavior with current pnpm version. `--legacy` was possibly added as a workaround for an older issue; removing it requires testing.
+- Follow-up: Verify `out/node_modules/react` symlink target after `pnpm deploy out --prod --filter @growi/app` in devcontainer
+
+### Decision: Place deploy output at workspace root, not `apps/app/` (eliminates step [2])
+- Context: Turbopack generates `.next/node_modules/` symlinks pointing to `../../../../node_modules/.pnpm/` (workspace root in Docker). Step [2] rewrites these to point to `apps/app/node_modules/.pnpm/`.
+- Alternatives:
+  1. Keep `apps/app/node_modules/`, apply step [2] rewrite (current)
+  2. Place deploy output at workspace root, no rewrite needed (selected)
+- Selected Approach: `mv out/node_modules node_modules` (replace workspace root `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 means no rewriting.
+- Trade-offs: Release image now includes `node_modules/` at workspace root (alongside `apps/app/`). The `package.json` at workspace root is already copied in current staging, so minimal structural change.
+- Follow-up: Verify Docker `COPY` preserves `apps/app/node_modules` symlink correctly
+
+### Decision: Use `pnpm deploy --prod` (not `pnpm install --prod`)
+- Context: Requirements described `pnpm install --prod` as the mechanism. Design evaluated both options.
+- Alternatives:
+  1. `pnpm install --prod --frozen-lockfile` — modifies existing workspace node_modules
+  2. `pnpm deploy out --prod` — creates clean deploy output (selected)
+- Selected Approach: `pnpm deploy out --prod --filter @growi/app` without `--legacy`
+- Rationale: `pnpm deploy` handles workspace package injection (physically copies `@growi/*` packages into deploy output, no `packages/` directory needed in release image). `pnpm install --prod` would leave `node_modules/@growi/core` as a symlink requiring `packages/core/` to exist in release.
+- Trade-offs: `pnpm deploy` takes slightly longer than `pnpm install --prod`. Both produce prod-only node_modules.
+
+## Risks & Mitigations
+- pnpm version compatibility: `pnpm deploy` without `--legacy` might behave differently across pnpm versions — Mitigation: pin pnpm version in Dockerfile; verify in CI before merging
+- `apps/app/node_modules` symlink + Docker COPY: Docker BuildKit COPY should preserve symlinks, but must be verified — Mitigation: add a verification step in CI that checks symlink integrity in release image
+- `rm -rf node_modules` in `assemble-prod.sh`: destroys workspace root `node_modules/` locally (dev workflow change) — Mitigation: document updated local testing procedure; developers must run `pnpm install` to restore after local testing
+
+## References
+- [turbo prune --docker official docs](https://github.com/vercel/turborepo/blob/main/apps/docs/content/docs/reference/prune.mdx) — `--docker` flag splits output into `json/` and `full/`
+- [Turborepo Docker guide](https://github.com/vercel/turborepo/blob/main/apps/docs/content/docs/guides/tools/docker.mdx) — multi-stage Dockerfile with prune
+- [pnpm deploy docs](https://pnpm.io/cli/deploy) — workspace package injection behavior

+ 22 - 0
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "optimise-deps-for-prod-with-turbo-prune",
+  "created_at": "2026-03-16T00:00:00.000Z",
+  "updated_at": "2026-03-16T00:00:00.000Z",
+  "language": "en",
+  "phase": "implementing",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": true,
+      "approved": true
+    }
+  },
+  "ready_for_implementation": true
+}

+ 87 - 0
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/tasks.md

@@ -0,0 +1,87 @@
+# Implementation Plan
+
+## Task Overview
+
+| Phase | Major Task | Sub-tasks | Requirements |
+|-------|-----------|-----------|--------------|
+| 1 | Verify `pnpm deploy` (without `--legacy`) symlink behavior | 1.1 | 2.1, 2.2, 2.3 |
+| 2 | Simplify `assemble-prod.sh` | 2.1, 2.2 | 1.1–1.4, 2.1, 2.4, 3.1–3.3, 5.4, 5.5 |
+| 3 | Update Dockerfile staging | — | 3.1, 3.3, 5.1–5.3 |
+| 4 | Validate end-to-end | 4.1, 4.2, 4.3 | 1.1–1.4, 2.3, 2.4, 3.2, 3.4, 4.1–4.5, 6.1–6.4 |
+
+---
+
+- [ ] 1. Verify `pnpm deploy` (without `--legacy`) creates self-contained symlinks
+
+- [ ] 1.1 Run `pnpm deploy out --prod --filter @growi/app` (without `--legacy`) and inspect top-level symlinks in `out/node_modules/`
+  - From workspace root, run `rm -rf out && pnpm deploy out --prod --filter @growi/app`
+  - Inspect a non-scoped top-level symlink: `readlink out/node_modules/react` — expected result starts with `.pnpm/react@` (e.g. `.pnpm/react@18.2.0/node_modules/react`), NOT `../../../node_modules/.pnpm/`
+  - Inspect a scoped top-level symlink: `readlink out/node_modules/@codemirror/state` — expected result starts with `../.pnpm/@codemirror`
+  - If either symlink points to `../../../node_modules/.pnpm/` or `../../../../node_modules/.pnpm/`, the isolated-linker assumption is wrong and the design requires revision before proceeding
+  - Verify `out/node_modules/.pnpm/` exists and contains physical package directories (not symlinks to the workspace-root `.pnpm/` store)
+  - Clean up: `rm -rf out`
+  - _Requirements: 2.1, 2.2, 2.3_
+
+---
+
+- [ ] 2. Simplify `assemble-prod.sh` by removing `--legacy`, both symlink-rewrite steps, and restructuring node_modules placement
+
+- [ ] 2.1 Remove `--legacy` from `pnpm deploy` and delete the step [1b] bash block
+  - In `apps/app/bin/assemble-prod.sh`, change `pnpm deploy out --prod --legacy --filter @growi/app` to `pnpm deploy out --prod --filter @growi/app` (drop `--legacy`)
+  - Delete the entire step [1b/4] block: the `echo "[1b/4]..."` line, the `find apps/app/node_modules -maxdepth 2 -type l | while read ...` loop, and the closing `echo "[1b/4] Done."` line
+  - Without `--legacy`, pnpm's isolated linker creates top-level symlinks that are self-contained relative to the deploy output directory; no rewriting is needed
+  - Update the progress step count in remaining echo messages to reflect removal of step [1b] (if desired; otherwise just delete the [1b] lines)
+  - _Requirements: 1.1, 1.3, 2.1_
+
+- [ ] 2.2 Replace node_modules placement with workspace-root staging, add compatibility symlink, and remove step [2] bash block
+  - Replace `rm -rf apps/app/node_modules` + `mv out/node_modules apps/app/node_modules` with the following sequence:
+    1. `rm -rf node_modules` — remove the workspace-root full-deps `node_modules/` (the pnpm install output)
+    2. `mv out/node_modules node_modules` — place the prod-only deploy output at workspace root
+    3. `rm -rf apps/app/node_modules` — remove any pre-existing `apps/app/node_modules/` (e.g. from a previous run)
+    4. `ln -sfn ../../node_modules apps/app/node_modules` — create a compatibility symlink for the `migrate-mongo` script path and Node.js `require()` traversal
+    5. `rm -rf out` — clean up remaining deploy output (source files copied by `pnpm deploy`)
+  - Delete the entire step [2/4] block: the `echo "[2/4]..."` line, the `if [ -d ... ]` block with the `find .next/node_modules ...` sed loop, and the closing `echo "[2/4] Done."` line
+  - The `.next/node_modules/` symlinks generated by Turbopack point to `../../../../node_modules/.pnpm/...` (workspace root). With `node_modules/` now at workspace root, those symlinks resolve naturally — no rewriting needed
+  - **Note**: After running this script locally, the workspace-root `node_modules/` is prod-only. Developers must run `pnpm install` from workspace root after local testing to restore the full development environment
+  - **Note on 5.4/5.5**: The remaining two operations (rm `.next/cache`, rm `next.config.ts`) could be inlined as `RUN` steps directly in the Dockerfile, eliminating `assemble-prod.sh` entirely — defer this to a follow-up if desired
+  - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.4, 3.1, 3.2, 3.3, 5.4, 5.5_
+
+---
+
+- [ ] 3. (P) Update Dockerfile `builder`-stage artifact staging to include workspace-root `node_modules/`
+  - In `apps/app/docker/Dockerfile`, in the `builder` stage `RUN mkdir -p /tmp/release/apps/app && cp ...` step, add `cp -a node_modules /tmp/release/ && \` immediately before the `cp -a apps/app/.next ...` line to copy the workspace-root prod `node_modules/` into the release artifact
+  - The existing `apps/app/node_modules` entry in the long `cp -a` list continues to be correct: `cp -a` uses `-d`/`-P` (no symlink dereferencing), so the symlink created by `assemble-prod.sh` is copied as a symlink, not dereferenced
+  - The resulting `/tmp/release/` structure will be: `node_modules/` (workspace root, prod-only) + `apps/app/.next/` + `apps/app/node_modules` (symlink to `../../node_modules`)
+  - This task modifies only the Dockerfile staging `RUN` step; no changes to the `pruner`, `deps`, or `release` stages
+  - Can be written concurrently with task 2 (different file); testing requires task 2 to be complete first
+  - _Requirements: 3.1, 3.3, 5.1, 5.2, 5.3_
+
+---
+
+- [ ] 4. Validate the simplified assembly pipeline end-to-end
+
+- [ ] 4.1 Run the updated `assemble-prod.sh` and verify symlink integrity
+  - From workspace root, run `turbo run build --filter @growi/app` to produce a fresh `.next/` build
+  - Run `bash apps/app/bin/assemble-prod.sh`
+  - Assert `apps/app/node_modules` is a symlink: `test -L apps/app/node_modules && echo OK`
+  - Assert the symlink resolves to workspace-root `node_modules/`: `readlink apps/app/node_modules` → `../../node_modules`
+  - Assert zero broken symlinks in `.next/node_modules/` using the broken-symlink check script from `optimise-deps-for-prod/design.md` (find + realpath loop, expect no output)
+  - Assert zero broken symlinks in workspace-root `node_modules/` (same check pattern)
+  - Assert no `devDependencies`-only package from `apps/app/package.json` appears in `apps/app/.next/node_modules/` (regression check from `optimise-deps-for-prod`)
+  - Restore `next.config.ts`: `git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts`
+  - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.3, 3.2, 6.1, 6.2, 6.3_
+
+- [ ] 4.2 Start the production server and verify HTTP 200 on root page
+  - Start server: `cd apps/app && pnpm run server > /tmp/server.log 2>&1 &`
+  - Wait for startup: `timeout 60 bash -c 'until grep -q "Express server is listening" /tmp/server.log; do sleep 2; done'`
+  - Verify HTTP 200: `HTTP_CODE=$(curl -s -o /tmp/response.html -w "%{http_code}" http://localhost:3000/); echo "HTTP: $HTTP_CODE"` → must be `200`
+  - Verify zero `ERR_MODULE_NOT_FOUND`: `grep -c "ERR_MODULE_NOT_FOUND" /tmp/server.log` → must be `0`
+  - Verify zero `Failed to load external module`: `grep -c "Failed to load external module" /tmp/server.log` → must be `0`
+  - **Use `GET /` (not `/login`) as the smoke-test URL**: `/login` returns HTTP 200 even when SSR is broken; `/` triggers SSR of editor components and will HTTP 500 if packages are missing
+  - Kill server after verification: `kill $(lsof -ti:3000)`
+  - _Requirements: 3.4, 4.1, 4.2, 4.3, 4.4, 4.5, 6.3, 6.4_
+
+- [ ] 4.3 Restore the development environment after local validation
+  - Run `pnpm install` from workspace root to restore the full-deps `node_modules/` and replace the `apps/app/node_modules` symlink with the standard pnpm workspace link
+  - Verify `turbo run build --filter @growi/app` succeeds after restoration (ensures the dev environment was not permanently damaged)
+  - _Requirements: 5.4_

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

@@ -49,15 +49,23 @@ turbo run build
 assemble-prod.sh
 assemble-prod.sh
   ├─ pnpm deploy out --prod --legacy   → out/node_modules/ (pnpm-native: .pnpm/ + symlinks)
   ├─ pnpm deploy out --prod --legacy   → out/node_modules/ (pnpm-native: .pnpm/ + symlinks)
   ├─ rm + mv out/node_modules → apps/app/node_modules/
   ├─ rm + mv out/node_modules → apps/app/node_modules/
+  ├─ [1b] symlink rewrite in apps/app/node_modules/:
+  │     non-scoped: ../../../node_modules/.pnpm/ → .pnpm/
+  │     scoped:    ../../../../node_modules/.pnpm/ → ../.pnpm/
   ├─ rm -rf .next/cache
   ├─ rm -rf .next/cache
   ├─ next.config.ts removal
   ├─ next.config.ts removal
-  └─ symlink rewrite: ../../../../ → ../../ in .next/node_modules/
+  └─ [2] symlink rewrite in .next/node_modules/:
+        ../../../../node_modules/.pnpm/ → ../../node_modules/.pnpm/
 
 
 cp -a to /tmp/release/           → preserves pnpm symlinks intact
 cp -a to /tmp/release/           → preserves pnpm symlinks intact
 COPY --from=builder /tmp/release/ → release image
 COPY --from=builder /tmp/release/ → release image
 ```
 ```
 
 
-The symlink rewrite step is essential: `.next/node_modules/` symlinks point to the workspace-root `.pnpm/` (4 levels up), which does not exist in the release image. After rewriting to 2 levels up, they resolve to `apps/app/node_modules/.pnpm/` (included in the deploy output), preserving pnpm's sibling-resolution for transitive dependencies.
+Two symlink rewrite steps are required:
+
+1. **`apps/app/node_modules/` rewrite** (`[1b]`): `pnpm deploy --prod` generates top-level symlinks in `out/node_modules/` pointing to the workspace-root `.pnpm/`. After `mv out/node_modules apps/app/node_modules`, these symlinks still reference the workspace-root path, which does not exist in production. Rewriting them to point within `apps/app/node_modules/.pnpm/` makes the deploy self-contained.
+
+2. **`.next/node_modules/` rewrite** (`[2]`): Turbopack generates symlinks in `.next/node_modules/` pointing to `../../../../node_modules/.pnpm/` (workspace root). After rewriting to `../../node_modules/.pnpm/`, they resolve to `apps/app/node_modules/.pnpm/`, preserving pnpm's sibling-resolution for transitive dependencies.
 
 
 ### Architecture Pattern & Boundary Map
 ### Architecture Pattern & Boundary Map
 
 
@@ -130,7 +138,7 @@ graph TB
     P4 --> P5
     P4 --> P5
 ```
 ```
 
 
-Each phase gate requires: production server starts without errors + login page returns HTTP 200.
+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.
 
 
 ---
 ---
 
 
@@ -307,7 +315,7 @@ These are not new components; they are targeted wrapping of existing imports usi
 
 
 **Implementation Notes**
 **Implementation Notes**
 - Apply `dynamic` wrapping to the specific consuming component file, not to the package entry point.
 - 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) confirm no hydration warnings in browser console.
+- 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.
 - Risks: Wrapping components that render visible content may cause flash of missing content (FOMC); test on slow connections.
 
 
 ---
 ---
@@ -318,45 +326,61 @@ These are not new components; they are targeted wrapping of existing imports usi
 
 
 以下の手順でプロダクションサーバーを起動する。devcontainer 環境での検証を想定している。
 以下の手順でプロダクションサーバーを起動する。devcontainer 環境での検証を想定している。
 
 
-**Step 1 — クリーンビルド**
+**Step 1 — クリーンビルド**(ワークスペースルートから実行)
 
 
 ```bash
 ```bash
-cd /workspace/growi/apps/app
-pnpm run build
+turbo run build --filter @growi/app
 ```
 ```
 
 
-**Step 2 — プロダクション用アセンブル**
+**Step 2 — プロダクション用アセンブル**(ワークスペースルートから実行)
 
 
 ```bash
 ```bash
-bash bin/assemble-prod.sh
+bash apps/app/bin/assemble-prod.sh
 ```
 ```
 
 
-> **注意**: `assemble-prod.sh` は `apps/app/next.config.ts` を削除する。次回の開発ビルドを実行する前に必ず復元すること。
+> **注意**:
+> - `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 されるためこれは正常な挙動。
+
+**Step 3 — プロダクションサーバー起動**(`apps/app/` から実行)
 
 
-**Step 3 — プロダクションサーバー起動**
+```bash
+cd apps/app && pnpm run server > /tmp/server.log 2>&1 &
+```
 
 
-`.env.production.local` が存在しない場合は作成する:
+起動完了を待つ:
 
 
+```bash
+timeout 60 bash -c 'until grep -q "Express server is listening" /tmp/server.log; do sleep 2; done'
 ```
 ```
-MONGO_URI=mongodb://mongo/growi-dev-wiki
+
+> **注意**: `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
 ```bash
-cd /workspace/growi/apps/app
-pnpm run server
+kill $(lsof -ti:3000)
 ```
 ```
 
 
-**Step 4 — 開発環境の復元**(検証後)
+**Step 6 — 開発環境の復元**(検証後)
 
 
 ```bash
 ```bash
 # next.config.ts を復元
 # next.config.ts を復元
 git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts
 git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts
-
-# node_modules を復元
-cd /workspace/growi
-pnpm install
 ```
 ```
 
 
 ---
 ---
@@ -365,43 +389,55 @@ pnpm install
 
 
 プロダクションサーバー起動後、以下のコマンドで SSR の正常動作を確認する。
 プロダクションサーバー起動後、以下のコマンドで SSR の正常動作を確認する。
 
 
-**基本確認(HTTP 200 チェック)**
+**検証コマンド(Production Server Startup Procedure の Step 5 参照)**
 
 
 ```bash
 ```bash
-curl -o /dev/null -s -w "%{http_code}\n" http://localhost:3000/login
-# → 200 が返れば OK
+# 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
 ```
 ```
 
 
-**モジュール解決エラーのチェック**
+**破損シンボリックリンクの確認**
 
 
-サーバーのログに `ERR_MODULE_NOT_FOUND` が出ていないことを確認する。バックグラウンド起動した場合:
+`assemble-prod.sh` 実行後、`.next/node_modules/` および `apps/app/node_modules/` 内のシンボリックリンクがすべて解決可能であることを確認する。
 
 
 ```bash
 ```bash
-grep -c "ERR_MODULE_NOT_FOUND" /tmp/growi-prod.log
-# → 0 が返れば OK
+# .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
 ```
 ```
 
 
-**破損シンボリックリンクの確認**
-
-`assemble-prod.sh` 実行後、`.next/node_modules/` 内にシンボリックリンクが存在するが、そのすべてが致命的なエラーを引き起こすわけではない。以下の 2 種類に分類される。
+> **注意**: `@growi/*` パッケージは `../../../../packages/` を指すシンボリックリンクだが、`packages/` はワークスペースルート直下に存在するため問題ない。
 
 
-| カテゴリ | 説明 | サーバーへの影響 |
-|---------|------|----------------|
-| **既存の破損リンク** | 元々 devDependencies だったパッケージのリンク(変更前から存在) | なし(サーバーが実際に `require()` しないため) |
-| **新規の破損リンク** | 今回の変更で devDependencies に移動したパッケージのリンク | `dynamic({ ssr: false })` で正しくラップされていれば無害 |
+**devDependencies 逆入り確認**
 
 
-Turbopack は SSR 静的解析時に `dynamic({ ssr: false })` 境界の内側のパッケージについても `.next/node_modules/` エントリを作成する。破損シンボリックリンクが存在すること自体は問題ではなく、サーバーが実際にそのパッケージを `require()` するかどうかが判断基準となる。
+```bash
+# devDependencies に列挙されているパッケージが .next/node_modules/ に現れないことを確認
+# (あれば Classification regression)
+```
 
 
-**devcontainer における再現性の注意事項**
+**devcontainer における再現性について**
 
 
-devcontainer では `pnpm deploy --prod` 後もワークスペースルートの `node_modules/` が残存するため、`.next/` を経由しないモジュール解決パス(例: `../../node_modules/foo`)では誤って green と判定される可能性がある。ただし `.next/node_modules/` 内のシンボリックリンクは直接ファイルシステムパスによる解決であり、Docker 本番環境と同じ挙動を示す。そのため Turbopack の外部化(externalisation)検証においては devcontainer テストで十分な精度が得られる。
+`pnpm deploy --prod` 後もワークスペースルートの `node_modules/` が残存するため、**必ず `mv node_modules node_modules.bak` を実施してからサーバーを起動すること**。この手順を省略すると `apps/app/node_modules/` の壊れたシンボリックリンクがワークスペースルートの `node_modules/` によって補完され、誤って green と判定される。
 
 
 ---
 ---
 
 
 ### Phase 1 — Smoke Test (Req 1.3, 1.4, 1.5)
 ### Phase 1 — Smoke Test (Req 1.3, 1.4, 1.5)
 
 
 - 上記「Production Server Startup Procedure」に従いサーバーを起動し、stdout に `ERR_MODULE_NOT_FOUND` が出力されないことを確認する。
 - 上記「Production Server Startup Procedure」に従いサーバーを起動し、stdout に `ERR_MODULE_NOT_FOUND` が出力されないことを確認する。
-- HTTP GET `/login`: assert 200 response and absence of SSR error log lines.
+- **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.
 - Run `launch-prod` CI job: assert job passes against MongoDB 6.0 and 8.0.
 
 
 ### Phase 2 — Emoji Rendering (Req 2.2)
 ### Phase 2 — Emoji Rendering (Req 2.2)
@@ -411,18 +447,21 @@ devcontainer では `pnpm deploy --prod` 後もワークスペースルートの
 
 
 ### Phase 3 — Hydration Integrity (Req 3.3)
 ### 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.
 - 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.
 - Visual regression: screenshot comparison of affected pages before and after `ssr: false` wrapping.
 
 
 ### Phase 4 — Admin Socket and Bootstrap (Req 4.2, 4.3)
 ### 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).
 - 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.
 - Bootstrap: assert Bootstrap dropdown/modal JavaScript functions correctly in browser after confirming `import()` placement.
 
 
 ### Phase 5 — Final Coverage Check (Req 5.1, 5.3)
 ### Phase 5 — Final Coverage Check (Req 5.1, 5.3)
 
 
-- Automated check (add to `assemble-prod.sh` or CI): after deploy, assert that every symlink in `apps/app/.next/node_modules/` resolves to an existing file in `apps/app/node_modules/.pnpm/`.
+- 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, verified with workspace-root `node_modules` renamed).
 - Assert no package listed in `devDependencies` appears in `apps/app/.next/node_modules/` after a production build.
 - Assert no package listed in `devDependencies` appears in `apps/app/.next/node_modules/` after a production build.
+- Run Production Server Startup Procedure (including `mv node_modules node_modules.bak`) and assert `GET /` HTTP 200 with expected content.
 
 
 ---
 ---
 
 

+ 45 - 18
.kiro/specs/optimise-deps-for-prod/tasks.md

@@ -45,37 +45,41 @@
 
 
 ---
 ---
 
 
-- [x] 3. Apply `dynamic({ ssr: false })` to eligible Group 1 components
+- [ ] 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`
-  - Applied `dynamic({ ssr: false })` in `apps/app/src/client/services/renderer/renderer.tsx`
+  - 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`
   - Moved `fslightbox-react` from `dependencies` to `devDependencies`
-  - Server test: HTTP 200 on `/login`, no `ERR_MODULE_NOT_FOUND`
+  - **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_
 
 
-- [x] 3.2 (P) Wrap `RevisionDiff.tsx` import with `dynamic({ ssr: false })` and verify `diff2html` leaves `dependencies`
+- [ ] 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.
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
   - _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`
+- [ ] 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.
   - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
   - _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`
+- [ ] 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.
   - _Requirements: 3.1, 3.2, 3.4, 3.5_
   - _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
+- [ ] 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_
   - _Requirements: 3.3, 3.4, 3.5_
 
 
 ---
 ---
 
 
-- [x] 4. Resolve ambiguous and phantom package classifications
+- [ ] 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`)
@@ -90,20 +94,22 @@
   - 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_
 
 
-- [x] 4.3 (P) Verify whether `bootstrap` JS `import()` is browser-only and classify accordingly
+- [ ] 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.)
   - _Requirements: 4.3_
   - _Requirements: 4.3_
 
 
-- [x] 4.4 (P) Investigate phantom packages and remove or reclassify them
+- [ ] 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.
   - _Requirements: 4.4_
   - _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`
-  - `pnpm install` run to verify lock file integrity
-  - `assemble-prod.sh` + server start: HTTP 200 on `/login`, no `ERR_MODULE_NOT_FOUND`
+- [ ] 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_
   - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
 
 
 ---
 ---
@@ -111,10 +117,31 @@
 - [ ] 5. Final validation and documentation
 - [ ] 5. Final validation and documentation
 
 
 - [ ] 5.1 Verify that every `.next/node_modules/` symlink resolves correctly in the release artifact
 - [ ] 5.1 Verify that every `.next/node_modules/` symlink resolves correctly in the release artifact
-  - Run `bash apps/app/bin/assemble-prod.sh` to produce the final artifact
-  - Enumerate every symlink under `apps/app/.next/node_modules/` with `find apps/app/.next/node_modules -maxdepth 2 -type l` and assert that each target path exists under `apps/app/node_modules/.pnpm/` (no broken symlinks)
+  - 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`.** 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.
+  - **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)
   - Assert that no package listed in `devDependencies` in `apps/app/package.json` appears in `apps/app/.next/node_modules/` (no classification regression)
-  - Start `pnpm run server` and confirm HTTP 200 on `/login` with no SSR errors in the server log
+  - 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**: PENDING RE-VERIFICATION.
+  - **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_
 
 
 - [x] 5.2 Add Turbopack externalisation rule documentation
 - [x] 5.2 Add Turbopack externalisation rule documentation

+ 13 - 0
apps/app/bin/assemble-prod.sh

@@ -10,6 +10,19 @@ rm -rf apps/app/node_modules
 mv out/node_modules apps/app/node_modules
 mv out/node_modules apps/app/node_modules
 echo "[1/4] Done."
 echo "[1/4] Done."
 
 
+# Rewrite apps/app/node_modules/ top-level symlinks from workspace root to the local .pnpm/ store.
+# pnpm deploy creates symlinks that point to ../../../../node_modules/.pnpm/ (workspace root),
+# which will not exist in production. Rewrite to point within apps/app/node_modules/.pnpm/ instead.
+echo "[1b/4] Rewriting apps/app/node_modules symlinks..."
+find apps/app/node_modules -maxdepth 2 -type l | while read -r link; do
+  target=$(readlink "$link")
+  # Scoped packages (@scope/pkg, maxdepth 2): ../../../../node_modules/.pnpm/ → ../.pnpm/
+  # Non-scoped packages (maxdepth 1): ../../../node_modules/.pnpm/ → .pnpm/
+  new_target=$(echo "$target" | sed 's|../../../../node_modules/\.pnpm/|../.pnpm/|; s|../../../node_modules/\.pnpm/|.pnpm/|')
+  if [ "$target" != "$new_target" ]; then ln -sfn "$new_target" "$link"; fi
+done
+echo "[1b/4] Done."
+
 # Redirect .next/node_modules/ symlinks from workspace root to deployed apps/app/node_modules/.pnpm/.
 # Redirect .next/node_modules/ symlinks from workspace root to deployed apps/app/node_modules/.pnpm/.
 # Turbopack generates symlinks pointing to ../../../../node_modules/.pnpm/ (workspace root),
 # Turbopack generates symlinks pointing to ../../../../node_modules/.pnpm/ (workspace root),
 # which will not exist in production environments.
 # which will not exist in production environments.

+ 29 - 9
apps/app/package.json

@@ -70,11 +70,20 @@
     "@azure/openai": "^2.0.0",
     "@azure/openai": "^2.0.0",
     "@azure/storage-blob": "^12.16.0",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
+    "@codemirror/autocomplete": "^6.18.4",
+    "@codemirror/commands": "^6.8.0",
+    "@codemirror/lang-markdown": "^6.3.2",
+    "@codemirror/language": "^6.12.1",
+    "@codemirror/language-data": "^6.5.1",
+    "@codemirror/merge": "^6.8.0",
     "@codemirror/state": "^6.5.2",
     "@codemirror/state": "^6.5.2",
+    "@codemirror/view": "^6.39.14",
     "@cspell/dynamic-import": "^8.15.4",
     "@cspell/dynamic-import": "^8.15.4",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.4",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.4",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.18.2",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.18.2",
     "@elastic/elasticsearch9": "npm:@elastic/elasticsearch@^9.0.3",
     "@elastic/elasticsearch9": "npm:@elastic/elasticsearch@^9.0.3",
+    "@emoji-mart/data": "^1.2.1",
+    "@emoji-mart/react": "^1.1.1",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "workspace:^",
     "@growi/core": "workspace:^",
@@ -88,9 +97,13 @@
     "@growi/remark-growi-directive": "workspace:^",
     "@growi/remark-growi-directive": "workspace:^",
     "@growi/remark-lsx": "workspace:^",
     "@growi/remark-lsx": "workspace:^",
     "@growi/slack": "workspace:^",
     "@growi/slack": "workspace:^",
+    "@handsontable/react": "=2.1.0",
     "@headless-tree/core": "^1.5.3",
     "@headless-tree/core": "^1.5.3",
     "@headless-tree/react": "^1.5.3",
     "@headless-tree/react": "^1.5.3",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@keycloak/keycloak-admin-client": "^18.0.0",
+    "@lezer/highlight": "^1.2.3",
+    "@marp-team/marp-core": "^3.9.1",
+    "@marp-team/marpit": "^2.6.1",
     "@opentelemetry/api": "^1.9.0",
     "@opentelemetry/api": "^1.9.0",
     "@opentelemetry/auto-instrumentations-node": "^0.60.1",
     "@opentelemetry/auto-instrumentations-node": "^0.60.1",
     "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
     "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
@@ -100,6 +113,9 @@
     "@opentelemetry/sdk-node": "^0.202.0",
     "@opentelemetry/sdk-node": "^0.202.0",
     "@opentelemetry/sdk-trace-node": "^2.0.1",
     "@opentelemetry/sdk-trace-node": "^2.0.1",
     "@opentelemetry/semantic-conventions": "^1.34.0",
     "@opentelemetry/semantic-conventions": "^1.34.0",
+    "@replit/codemirror-emacs": "^6.1.0",
+    "@replit/codemirror-vim": "^6.2.1",
+    "@replit/codemirror-vscode-keymap": "^6.0.2",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
     "@tanstack/react-virtual": "^3.13.12",
     "@tanstack/react-virtual": "^3.13.12",
@@ -113,9 +129,12 @@
     "axios-retry": "^3.2.4",
     "axios-retry": "^3.2.4",
     "babel-plugin-superjson-next": "^0.4.2",
     "babel-plugin-superjson-next": "^0.4.2",
     "body-parser": "^1.20.3",
     "body-parser": "^1.20.3",
+    "bootstrap": "=5.3.2",
     "browser-bunyan": "^1.8.0",
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
+    "cm6-theme-basic-light": "^0.2.0",
+    "codemirror": "^6.0.1",
     "compression": "^1.7.4",
     "compression": "^1.7.4",
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^4.6.0",
     "connect-mongo": "^4.6.0",
@@ -128,6 +147,7 @@
     "dayjs": "^1.11.7",
     "dayjs": "^1.11.7",
     "detect-indent": "^7.0.0",
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",
     "diff": "^5.0.0",
+    "diff2html": "^3.4.47",
     "diff_match_patch": "^0.1.1",
     "diff_match_patch": "^0.1.1",
     "dotenv-flow": "^3.2.0",
     "dotenv-flow": "^3.2.0",
     "downshift": "^8.2.3",
     "downshift": "^8.2.3",
@@ -149,6 +169,8 @@
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "http-errors": "^2.0.0",
     "i18next": "^23.16.5",
     "i18next": "^23.16.5",
+    "i18next-http-backend": "^2.6.2",
+    "i18next-localstorage-backend": "^4.2.0",
     "i18next-resources-to-backend": "^1.2.1",
     "i18next-resources-to-backend": "^1.2.1",
     "is-absolute-url": "^4.0.1",
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
     "is-iso-date": "^0.0.1",
@@ -190,6 +212,7 @@
     "openai": "^4.96.2",
     "openai": "^4.96.2",
     "openid-client": "^5.4.0",
     "openid-client": "^5.4.0",
     "p-retry": "^4.0.0",
     "p-retry": "^4.0.0",
+    "pako": "^2.1.0",
     "passport": "^0.6.0",
     "passport": "^0.6.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-google-oauth20": "^2.0.0",
@@ -207,7 +230,10 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-datepicker": "^4.7.0",
     "react-datepicker": "^4.7.0",
     "react-disable": "^0.1.1",
     "react-disable": "^0.1.1",
+    "react-dnd": "^14.0.5",
+    "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-dom": "^18.2.0",
+    "react-dropzone": "^14.2.3",
     "react-error-boundary": "^3.1.4",
     "react-error-boundary": "^3.1.4",
     "react-hook-form": "^7.45.4",
     "react-hook-form": "^7.45.4",
     "react-i18next": "^15.1.1",
     "react-i18next": "^15.1.1",
@@ -239,6 +265,7 @@
     "remark-parse": "^11.0.0",
     "remark-parse": "^11.0.0",
     "remark-rehype": "^11.1.1",
     "remark-rehype": "^11.1.1",
     "remark-stringify": "^11.0.0",
     "remark-stringify": "^11.0.0",
+    "reveal.js": "^4.4.8",
     "sanitize-filename": "^1.6.3",
     "sanitize-filename": "^1.6.3",
     "simplebar-react": "^2.3.6",
     "simplebar-react": "^2.3.6",
     "socket.io": "^4.7.5",
     "socket.io": "^4.7.5",
@@ -262,6 +289,7 @@
     "validator": "^13.15.22",
     "validator": "^13.15.22",
     "ws": "^8.17.1",
     "ws": "^8.17.1",
     "xss": "^1.0.15",
     "xss": "^1.0.15",
+    "y-codemirror.next": "^0.3.5",
     "y-mongodb-provider": "^0.2.0",
     "y-mongodb-provider": "^0.2.0",
     "y-socket.io": "^1.1.3",
     "y-socket.io": "^1.1.3",
     "yjs": "^13.6.18",
     "yjs": "^13.6.18",
@@ -275,12 +303,10 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@apidevtools/swagger-parser": "^10.1.1",
     "@apidevtools/swagger-parser": "^10.1.1",
-    "@emoji-mart/data": "^1.2.1",
     "@growi/core-styles": "workspace:^",
     "@growi/core-styles": "workspace:^",
     "@growi/custom-icons": "workspace:^",
     "@growi/custom-icons": "workspace:^",
     "@growi/editor": "workspace:^",
     "@growi/editor": "workspace:^",
     "@growi/ui": "workspace:^",
     "@growi/ui": "workspace:^",
-    "@handsontable/react": "=2.1.0",
     "@popperjs/core": "^2.11.8",
     "@popperjs/core": "^2.11.8",
     "@testing-library/jest-dom": "^6.5.0",
     "@testing-library/jest-dom": "^6.5.0",
     "@testing-library/user-event": "^14.5.2",
     "@testing-library/user-event": "^14.5.2",
@@ -307,17 +333,13 @@
     "@types/uuid": "^10.0.0",
     "@types/uuid": "^10.0.0",
     "@types/ws": "^8.18.1",
     "@types/ws": "^8.18.1",
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
-    "bootstrap": "=5.3.2",
     "commander": "^14.0.0",
     "commander": "^14.0.0",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
-    "diff2html": "^3.4.47",
     "eazy-logger": "^3.1.0",
     "eazy-logger": "^3.1.0",
     "fslightbox-react": "^1.7.6",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
     "happy-dom": "^15.7.4",
     "happy-dom": "^15.7.4",
     "i18next-chained-backend": "^4.6.2",
     "i18next-chained-backend": "^4.6.2",
-    "i18next-http-backend": "^2.6.2",
-    "i18next-localstorage-backend": "^4.2.0",
     "jotai-devtools": "^0.11.0",
     "jotai-devtools": "^0.11.0",
     "load-css-file": "^1.0.0",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
     "material-icons": "^1.11.3",
@@ -327,9 +349,6 @@
     "mongodb-memory-server-core": "^9.1.1",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
     "openapi-typescript": "^7.8.0",
     "openapi-typescript": "^7.8.0",
-    "react-dnd": "^14.0.5",
-    "react-dnd-html5-backend": "^14.1.0",
-    "react-dropzone": "^14.2.3",
     "rehype-rewrite": "^4.0.2",
     "rehype-rewrite": "^4.0.2",
     "remark-github-admonitions-to-directives": "^2.0.0",
     "remark-github-admonitions-to-directives": "^2.0.0",
     "sass": "^1.53.0",
     "sass": "^1.53.0",
@@ -337,6 +356,7 @@
     "supertest": "^7.1.4",
     "supertest": "^7.1.4",
     "swagger2openapi": "^7.0.8",
     "swagger2openapi": "^7.0.8",
     "tinykeys": "^3.0.0",
     "tinykeys": "^3.0.0",
+    "typescript": "~5.0.4",
     "unist-util-is": "^6.0.0",
     "unist-util-is": "^6.0.0",
     "unist-util-visit-parents": "^6.0.0"
     "unist-util-visit-parents": "^6.0.0"
   }
   }

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

@@ -1,4 +1,4 @@
-import { type JSX, useMemo } from 'react';
+import { type JSX, useEffect, useMemo, useState } 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,11 +6,13 @@ 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';

+ 25 - 17
apps/app/src/client/components/ReactMarkdownComponents/LightBox.tsx

@@ -1,7 +1,5 @@
-import type React from 'react';
-import type { DetailedHTMLProps, ImgHTMLAttributes, JSX } from 'react';
-import { useMemo, useState } from 'react';
-import FsLightbox from 'fslightbox-react';
+import type { ComponentType, DetailedHTMLProps, ImgHTMLAttributes, JSX } from 'react';
+import { useEffect, useState } from 'react';
 import { createPortal } from 'react-dom';
 import { createPortal } from 'react-dom';
 
 
 type Props = DetailedHTMLProps<
 type Props = DetailedHTMLProps<
@@ -9,22 +7,23 @@ type Props = DetailedHTMLProps<
   HTMLImageElement
   HTMLImageElement
 >;
 >;
 
 
+type FsLightboxProps = {
+  toggler: boolean;
+  sources: (string | undefined)[];
+  alt: string | undefined;
+  type: string;
+  exitFullscreenOnClose: boolean;
+};
+
 export const LightBox = (props: Props): JSX.Element => {
 export const LightBox = (props: Props): JSX.Element => {
   const [toggler, setToggler] = useState(false);
   const [toggler, setToggler] = useState(false);
+  // Dynamically import fslightbox-react so it stays out of the SSR bundle
+  const [FsLightbox, setFsLightbox] = useState<ComponentType<FsLightboxProps> | null>(null);
   const { alt, ...rest } = props;
   const { alt, ...rest } = props;
 
 
-  const lightboxPortal = useMemo(() => {
-    return createPortal(
-      <FsLightbox
-        toggler={toggler}
-        sources={[props.src]}
-        alt={alt}
-        type="image"
-        exitFullscreenOnClose
-      />,
-      document.body,
-    );
-  }, [alt, props.src, toggler]);
+  useEffect(() => {
+    import('fslightbox-react').then(m => setFsLightbox(() => m.default));
+  }, []);
 
 
   return (
   return (
     <>
     <>
@@ -37,7 +36,16 @@ export const LightBox = (props: Props): JSX.Element => {
         <img alt={alt} {...rest} />
         <img alt={alt} {...rest} />
       </button>
       </button>
 
 
-      {lightboxPortal}
+      {FsLightbox != null && createPortal(
+        <FsLightbox
+          toggler={toggler}
+          sources={[props.src]}
+          alt={alt}
+          type="image"
+          exitFullscreenOnClose
+        />,
+        document.body,
+      )}
     </>
     </>
   );
   );
 };
 };

+ 154 - 87
pnpm-lock.yaml

@@ -189,9 +189,30 @@ importers:
       '@browser-bunyan/console-formatted-stream':
       '@browser-bunyan/console-formatted-stream':
         specifier: ^1.8.0
         specifier: ^1.8.0
         version: 1.8.0
         version: 1.8.0
+      '@codemirror/autocomplete':
+        specifier: ^6.18.4
+        version: 6.18.4
+      '@codemirror/commands':
+        specifier: ^6.8.0
+        version: 6.8.0
+      '@codemirror/lang-markdown':
+        specifier: ^6.3.2
+        version: 6.3.2
+      '@codemirror/language':
+        specifier: ^6.12.1
+        version: 6.12.1
+      '@codemirror/language-data':
+        specifier: ^6.5.1
+        version: 6.5.1
+      '@codemirror/merge':
+        specifier: ^6.8.0
+        version: 6.8.0
       '@codemirror/state':
       '@codemirror/state':
         specifier: ^6.5.2
         specifier: ^6.5.2
         version: 6.5.4
         version: 6.5.4
+      '@codemirror/view':
+        specifier: ^6.39.14
+        version: 6.39.14
       '@cspell/dynamic-import':
       '@cspell/dynamic-import':
         specifier: ^8.15.4
         specifier: ^8.15.4
         version: 8.15.4
         version: 8.15.4
@@ -204,6 +225,12 @@ importers:
       '@elastic/elasticsearch9':
       '@elastic/elasticsearch9':
         specifier: npm:@elastic/elasticsearch@^9.0.3
         specifier: npm:@elastic/elasticsearch@^9.0.3
         version: '@elastic/elasticsearch@9.1.0'
         version: '@elastic/elasticsearch@9.1.0'
+      '@emoji-mart/data':
+        specifier: ^1.2.1
+        version: 1.2.1
+      '@emoji-mart/react':
+        specifier: ^1.1.1
+        version: 1.1.1(emoji-mart@5.6.0)(react@18.2.0)
       '@godaddy/terminus':
       '@godaddy/terminus':
         specifier: ^4.9.0
         specifier: ^4.9.0
         version: 4.12.1
         version: 4.12.1
@@ -243,6 +270,9 @@ importers:
       '@growi/slack':
       '@growi/slack':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/slack
         version: link:../../packages/slack
+      '@handsontable/react':
+        specifier: '=2.1.0'
+        version: 2.1.0(handsontable@6.2.2)
       '@headless-tree/core':
       '@headless-tree/core':
         specifier: ^1.5.3
         specifier: ^1.5.3
         version: 1.6.3
         version: 1.6.3
@@ -252,6 +282,15 @@ importers:
       '@keycloak/keycloak-admin-client':
       '@keycloak/keycloak-admin-client':
         specifier: ^18.0.0
         specifier: ^18.0.0
         version: 18.0.2
         version: 18.0.2
+      '@lezer/highlight':
+        specifier: ^1.2.3
+        version: 1.2.3
+      '@marp-team/marp-core':
+        specifier: ^3.9.1
+        version: 3.9.1(patch_hash=9339c96cb1f7b7d331a5faf3719935af8d4f5415f1f8b88ee73aad42627c5dd4)
+      '@marp-team/marpit':
+        specifier: ^2.6.1
+        version: 2.6.1
       '@opentelemetry/api':
       '@opentelemetry/api':
         specifier: ^1.9.0
         specifier: ^1.9.0
         version: 1.9.0
         version: 1.9.0
@@ -279,6 +318,15 @@ importers:
       '@opentelemetry/semantic-conventions':
       '@opentelemetry/semantic-conventions':
         specifier: ^1.34.0
         specifier: ^1.34.0
         version: 1.36.0
         version: 1.36.0
+      '@replit/codemirror-emacs':
+        specifier: ^6.1.0
+        version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.14)
+      '@replit/codemirror-vim':
+        specifier: ^6.2.1
+        version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.12.1)(@codemirror/search@6.5.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.14)
+      '@replit/codemirror-vscode-keymap':
+        specifier: ^6.0.2
+        version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.14)
       '@slack/web-api':
       '@slack/web-api':
         specifier: ^6.2.4
         specifier: ^6.2.4
         version: 6.12.0
         version: 6.12.0
@@ -318,6 +366,9 @@ importers:
       body-parser:
       body-parser:
         specifier: ^1.20.3
         specifier: ^1.20.3
         version: 1.20.3
         version: 1.20.3
+      bootstrap:
+        specifier: '=5.3.2'
+        version: 5.3.2(@popperjs/core@2.11.8)
       browser-bunyan:
       browser-bunyan:
         specifier: ^1.8.0
         specifier: ^1.8.0
         version: 1.8.0
         version: 1.8.0
@@ -327,6 +378,12 @@ importers:
       bunyan:
       bunyan:
         specifier: ^1.8.15
         specifier: ^1.8.15
         version: 1.8.15
         version: 1.8.15
+      cm6-theme-basic-light:
+        specifier: ^0.2.0
+        version: 0.2.0(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.14)(@lezer/highlight@1.2.3)
+      codemirror:
+        specifier: ^6.0.1
+        version: 6.0.1
       compression:
       compression:
         specifier: ^1.7.4
         specifier: ^1.7.4
         version: 1.7.4
         version: 1.7.4
@@ -363,6 +420,9 @@ importers:
       diff:
       diff:
         specifier: ^5.0.0
         specifier: ^5.0.0
         version: 5.2.0
         version: 5.2.0
+      diff2html:
+        specifier: ^3.4.47
+        version: 3.4.48
       diff_match_patch:
       diff_match_patch:
         specifier: ^0.1.1
         specifier: ^0.1.1
         version: 0.1.1
         version: 0.1.1
@@ -426,6 +486,12 @@ importers:
       i18next:
       i18next:
         specifier: ^23.16.5
         specifier: ^23.16.5
         version: 23.16.5
         version: 23.16.5
+      i18next-http-backend:
+        specifier: ^2.6.2
+        version: 2.6.2(encoding@0.1.13)
+      i18next-localstorage-backend:
+        specifier: ^4.2.0
+        version: 4.2.0
       i18next-resources-to-backend:
       i18next-resources-to-backend:
         specifier: ^1.2.1
         specifier: ^1.2.1
         version: 1.2.1
         version: 1.2.1
@@ -549,6 +615,9 @@ importers:
       p-retry:
       p-retry:
         specifier: ^4.0.0
         specifier: ^4.0.0
         version: 4.6.2
         version: 4.6.2
+      pako:
+        specifier: ^2.1.0
+        version: 2.1.0
       passport:
       passport:
         specifier: ^0.6.0
         specifier: ^0.6.0
         version: 0.6.0
         version: 0.6.0
@@ -600,9 +669,18 @@ importers:
       react-disable:
       react-disable:
         specifier: ^0.1.1
         specifier: ^0.1.1
         version: 0.1.1(react@18.2.0)
         version: 0.1.1(react@18.2.0)
+      react-dnd:
+        specifier: ^14.0.5
+        version: 14.0.5(@types/hoist-non-react-statics@3.3.5)(@types/node@25.2.3)(@types/react@18.3.3)(react@18.2.0)
+      react-dnd-html5-backend:
+        specifier: ^14.1.0
+        version: 14.1.0
       react-dom:
       react-dom:
         specifier: ^18.2.0
         specifier: ^18.2.0
         version: 18.2.0(react@18.2.0)
         version: 18.2.0(react@18.2.0)
+      react-dropzone:
+        specifier: ^14.2.3
+        version: 14.2.3(react@18.2.0)
       react-error-boundary:
       react-error-boundary:
         specifier: ^3.1.4
         specifier: ^3.1.4
         version: 3.1.4(react@18.2.0)
         version: 3.1.4(react@18.2.0)
@@ -696,6 +774,9 @@ importers:
       remark-stringify:
       remark-stringify:
         specifier: ^11.0.0
         specifier: ^11.0.0
         version: 11.0.0
         version: 11.0.0
+      reveal.js:
+        specifier: ^4.4.8
+        version: 4.6.1
       sanitize-filename:
       sanitize-filename:
         specifier: ^1.6.3
         specifier: ^1.6.3
         version: 1.6.3
         version: 1.6.3
@@ -765,6 +846,9 @@ importers:
       xss:
       xss:
         specifier: ^1.0.15
         specifier: ^1.0.15
         version: 1.0.15
         version: 1.0.15
+      y-codemirror.next:
+        specifier: ^0.3.5
+        version: 0.3.5(@codemirror/state@6.5.4)(@codemirror/view@6.39.14)(yjs@13.6.19)
       y-mongodb-provider:
       y-mongodb-provider:
         specifier: ^0.2.0
         specifier: ^0.2.0
         version: 0.2.0(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))(socks@2.8.3)(yjs@13.6.19)
         version: 0.2.0(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))(socks@2.8.3)(yjs@13.6.19)
@@ -781,9 +865,6 @@ importers:
       '@apidevtools/swagger-parser':
       '@apidevtools/swagger-parser':
         specifier: ^10.1.1
         specifier: ^10.1.1
         version: 10.1.1(openapi-types@12.1.3)
         version: 10.1.1(openapi-types@12.1.3)
-      '@emoji-mart/data':
-        specifier: ^1.2.1
-        version: 1.2.1
       '@growi/core-styles':
       '@growi/core-styles':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/core-styles
         version: link:../../packages/core-styles
@@ -796,9 +877,6 @@ importers:
       '@growi/ui':
       '@growi/ui':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/ui
         version: link:../../packages/ui
-      '@handsontable/react':
-        specifier: '=2.1.0'
-        version: 2.1.0(handsontable@6.2.2)
       '@popperjs/core':
       '@popperjs/core':
         specifier: ^2.11.8
         specifier: ^2.11.8
         version: 2.11.8
         version: 2.11.8
@@ -877,18 +955,12 @@ importers:
       babel-loader:
       babel-loader:
         specifier: ^8.2.5
         specifier: ^8.2.5
         version: 8.3.0(@babel/core@7.24.6)(webpack@5.92.1(@swc/core@1.10.7(@swc/helpers@0.5.18)))
         version: 8.3.0(@babel/core@7.24.6)(webpack@5.92.1(@swc/core@1.10.7(@swc/helpers@0.5.18)))
-      bootstrap:
-        specifier: '=5.3.2'
-        version: 5.3.2(@popperjs/core@2.11.8)
       commander:
       commander:
         specifier: ^14.0.0
         specifier: ^14.0.0
         version: 14.0.0
         version: 14.0.0
       connect-browser-sync:
       connect-browser-sync:
         specifier: ^2.1.0
         specifier: ^2.1.0
         version: 2.1.0(browser-sync@3.0.2)
         version: 2.1.0(browser-sync@3.0.2)
-      diff2html:
-        specifier: ^3.4.47
-        version: 3.4.48
       eazy-logger:
       eazy-logger:
         specifier: ^3.1.0
         specifier: ^3.1.0
         version: 3.1.0
         version: 3.1.0
@@ -904,12 +976,6 @@ importers:
       i18next-chained-backend:
       i18next-chained-backend:
         specifier: ^4.6.2
         specifier: ^4.6.2
         version: 4.6.2
         version: 4.6.2
-      i18next-http-backend:
-        specifier: ^2.6.2
-        version: 2.6.2(encoding@0.1.13)
-      i18next-localstorage-backend:
-        specifier: ^4.2.0
-        version: 4.2.0
       jotai-devtools:
       jotai-devtools:
         specifier: ^0.11.0
         specifier: ^0.11.0
         version: 0.11.0(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@4.2.1)
         version: 0.11.0(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(redux@4.2.1)
@@ -936,16 +1002,7 @@ importers:
         version: 1.10.0
         version: 1.10.0
       openapi-typescript:
       openapi-typescript:
         specifier: ^7.8.0
         specifier: ^7.8.0
-        version: 7.8.0(typescript@5.4.2)
-      react-dnd:
-        specifier: ^14.0.5
-        version: 14.0.5(@types/hoist-non-react-statics@3.3.5)(@types/node@25.2.3)(@types/react@18.3.3)(react@18.2.0)
-      react-dnd-html5-backend:
-        specifier: ^14.1.0
-        version: 14.1.0
-      react-dropzone:
-        specifier: ^14.2.3
-        version: 14.2.3(react@18.2.0)
+        version: 7.8.0(typescript@5.0.4)
       rehype-rewrite:
       rehype-rewrite:
         specifier: ^4.0.2
         specifier: ^4.0.2
         version: 4.0.2
         version: 4.0.2
@@ -967,6 +1024,9 @@ importers:
       tinykeys:
       tinykeys:
         specifier: ^3.0.0
         specifier: ^3.0.0
         version: 3.0.0
         version: 3.0.0
+      typescript:
+        specifier: ~5.0.4
+        version: 5.0.4
       unist-util-is:
       unist-util-is:
         specifier: ^6.0.0
         specifier: ^6.0.0
         version: 6.0.0
         version: 6.0.0
@@ -15651,7 +15711,7 @@ snapshots:
       '@azure/core-util': 1.10.0
       '@azure/core-util': 1.10.0
       '@azure/logger': 1.1.2
       '@azure/logger': 1.1.2
       http-proxy-agent: 7.0.2
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@10.0.0)
+      https-proxy-agent: 7.0.6
       tslib: 2.8.1
       tslib: 2.8.1
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -15753,7 +15813,7 @@ snapshots:
       '@babel/traverse': 7.24.6
       '@babel/traverse': 7.24.6
       '@babel/types': 7.25.6
       '@babel/types': 7.25.6
       convert-source-map: 2.0.0
       convert-source-map: 2.0.0
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       gensync: 1.0.0-beta.2
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       json5: 2.2.3
       semver: 6.3.1
       semver: 6.3.1
@@ -15852,7 +15912,7 @@ snapshots:
       '@babel/helper-split-export-declaration': 7.24.6
       '@babel/helper-split-export-declaration': 7.24.6
       '@babel/parser': 7.25.6
       '@babel/parser': 7.25.6
       '@babel/types': 7.25.6
       '@babel/types': 7.25.6
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       globals: 11.12.0
       globals: 11.12.0
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -16393,7 +16453,7 @@ snapshots:
 
 
   '@elastic/elasticsearch@7.17.13':
   '@elastic/elasticsearch@7.17.13':
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       hpagent: 0.1.2
       hpagent: 0.1.2
       ms: 2.1.3
       ms: 2.1.3
       secure-json-parse: 2.7.0
       secure-json-parse: 2.7.0
@@ -16422,7 +16482,7 @@ snapshots:
     dependencies:
     dependencies:
       '@opentelemetry/api': 1.9.0
       '@opentelemetry/api': 1.9.0
       '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
       '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       hpagent: 1.2.0
       hpagent: 1.2.0
       ms: 2.1.3
       ms: 2.1.3
       secure-json-parse: 3.0.2
       secure-json-parse: 3.0.2
@@ -16435,7 +16495,7 @@ snapshots:
     dependencies:
     dependencies:
       '@opentelemetry/api': 1.9.0
       '@opentelemetry/api': 1.9.0
       '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
       '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       hpagent: 1.2.0
       hpagent: 1.2.0
       ms: 2.1.3
       ms: 2.1.3
       secure-json-parse: 4.0.0
       secure-json-parse: 4.0.0
@@ -16618,7 +16678,7 @@ snapshots:
   '@eslint/eslintrc@2.1.4':
   '@eslint/eslintrc@2.1.4':
     dependencies:
     dependencies:
       ajv: 6.12.6
       ajv: 6.12.6
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       espree: 9.6.1
       espree: 9.6.1
       globals: 13.24.0
       globals: 13.24.0
       ignore: 5.3.2
       ignore: 5.3.2
@@ -16731,7 +16791,7 @@ snapshots:
   '@humanwhocodes/config-array@0.11.14':
   '@humanwhocodes/config-array@0.11.14':
     dependencies:
     dependencies:
       '@humanwhocodes/object-schema': 2.0.3
       '@humanwhocodes/object-schema': 2.0.3
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       minimatch: 3.1.2
       minimatch: 3.1.2
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -16764,7 +16824,7 @@ snapshots:
       '@antfu/install-pkg': 1.1.0
       '@antfu/install-pkg': 1.1.0
       '@antfu/utils': 8.1.1
       '@antfu/utils': 8.1.1
       '@iconify/types': 2.0.0
       '@iconify/types': 2.0.0
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       globals: 15.15.0
       globals: 15.15.0
       kolorist: 1.8.0
       kolorist: 1.8.0
       local-pkg: 1.1.1
       local-pkg: 1.1.1
@@ -17283,7 +17343,7 @@ snapshots:
     dependencies:
     dependencies:
       agent-base: 7.1.4
       agent-base: 7.1.4
       http-proxy-agent: 7.0.2
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@10.0.0)
+      https-proxy-agent: 7.0.6
       lru-cache: 10.4.3
       lru-cache: 10.4.3
       socks-proxy-agent: 8.0.4
       socks-proxy-agent: 8.0.4
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -18061,7 +18121,7 @@ snapshots:
       ajv: 8.17.1
       ajv: 8.17.1
       chalk: 4.1.2
       chalk: 4.1.2
       compare-versions: 6.1.1
       compare-versions: 6.1.1
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       esbuild: 0.24.0
       esbuild: 0.24.0
       esutils: 2.0.3
       esutils: 2.0.3
       fs-extra: 11.2.0
       fs-extra: 11.2.0
@@ -18224,7 +18284,7 @@ snapshots:
 
 
   '@puppeteer/browsers@2.4.0':
   '@puppeteer/browsers@2.4.0':
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       extract-zip: 2.0.1
       extract-zip: 2.0.1
       progress: 2.0.3
       progress: 2.0.3
       proxy-agent: 6.4.0
       proxy-agent: 6.4.0
@@ -19243,7 +19303,7 @@ snapshots:
       '@swc-node/sourcemap-support': 0.5.1
       '@swc-node/sourcemap-support': 0.5.1
       '@swc/core': 1.10.7(@swc/helpers@0.5.18)
       '@swc/core': 1.10.7(@swc/helpers@0.5.18)
       colorette: 2.0.20
       colorette: 2.0.20
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       oxc-resolver: 1.12.0
       oxc-resolver: 1.12.0
       pirates: 4.0.6
       pirates: 4.0.6
       tslib: 2.8.1
       tslib: 2.8.1
@@ -19258,7 +19318,7 @@ snapshots:
       '@swc-node/sourcemap-support': 0.5.1
       '@swc-node/sourcemap-support': 0.5.1
       '@swc/core': 1.10.7(@swc/helpers@0.5.18)
       '@swc/core': 1.10.7(@swc/helpers@0.5.18)
       colorette: 2.0.20
       colorette: 2.0.20
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       oxc-resolver: 1.12.0
       oxc-resolver: 1.12.0
       pirates: 4.0.6
       pirates: 4.0.6
       tslib: 2.8.1
       tslib: 2.8.1
@@ -20449,7 +20509,7 @@ snapshots:
     dependencies:
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 0.2.3
       '@bcoe/v8-coverage': 0.2.3
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
       istanbul-lib-report: 3.0.1
       istanbul-lib-source-maps: 5.0.6
       istanbul-lib-source-maps: 5.0.6
@@ -20688,7 +20748,7 @@ snapshots:
 
 
   agent-base@6.0.2:
   agent-base@6.0.2:
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -21861,7 +21921,7 @@ snapshots:
 
 
   connect-mongo@4.6.0(express-session@1.18.0)(mongodb@4.17.2(@aws-sdk/client-sso-oidc@3.600.0)):
   connect-mongo@4.6.0(express-session@1.18.0)(mongodb@4.17.2(@aws-sdk/client-sso-oidc@3.600.0)):
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       express-session: 1.18.0
       express-session: 1.18.0
       kruptein: 3.0.6
       kruptein: 3.0.6
       mongodb: 4.17.2(@aws-sdk/client-sso-oidc@3.600.0)
       mongodb: 4.17.2(@aws-sdk/client-sso-oidc@3.600.0)
@@ -22689,7 +22749,7 @@ snapshots:
   engine.io-client@6.6.4:
   engine.io-client@6.6.4:
     dependencies:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       engine.io-parser: 5.2.3
       engine.io-parser: 5.2.3
       ws: 8.18.3
       ws: 8.18.3
       xmlhttprequest-ssl: 2.1.2
       xmlhttprequest-ssl: 2.1.2
@@ -22708,7 +22768,7 @@ snapshots:
       base64id: 2.0.0
       base64id: 2.0.0
       cookie: 0.7.2
       cookie: 0.7.2
       cors: 2.8.5
       cors: 2.8.5
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       engine.io-parser: 5.2.3
       engine.io-parser: 5.2.3
       ws: 8.18.3
       ws: 8.18.3
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -22942,7 +23002,7 @@ snapshots:
       ajv: 6.12.6
       ajv: 6.12.6
       chalk: 4.1.2
       chalk: 4.1.2
       cross-spawn: 7.0.6
       cross-spawn: 7.0.6
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       doctrine: 3.0.0
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
       escape-string-regexp: 4.0.0
       eslint-scope: 7.2.2
       eslint-scope: 7.2.2
@@ -23132,7 +23192,7 @@ snapshots:
 
 
   extract-zip@2.0.1:
   extract-zip@2.0.1:
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       get-stream: 5.2.0
       get-stream: 5.2.0
       yauzl: 2.10.0
       yauzl: 2.10.0
     optionalDependencies:
     optionalDependencies:
@@ -23314,7 +23374,7 @@ snapshots:
 
 
   follow-redirects@1.15.11(debug@4.4.3):
   follow-redirects@1.15.11(debug@4.4.3):
     optionalDependencies:
     optionalDependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
 
 
   for-each@0.3.3:
   for-each@0.3.3:
     dependencies:
     dependencies:
@@ -23467,7 +23527,7 @@ snapshots:
   gaxios@6.7.1(encoding@0.1.13):
   gaxios@6.7.1(encoding@0.1.13):
     dependencies:
     dependencies:
       extend: 3.0.2
       extend: 3.0.2
-      https-proxy-agent: 7.0.6(supports-color@10.0.0)
+      https-proxy-agent: 7.0.6
       is-stream: 2.0.0
       is-stream: 2.0.0
       node-fetch: 2.7.0(encoding@0.1.13)
       node-fetch: 2.7.0(encoding@0.1.13)
       uuid: 9.0.1
       uuid: 9.0.1
@@ -23545,7 +23605,7 @@ snapshots:
     dependencies:
     dependencies:
       basic-ftp: 5.0.5
       basic-ftp: 5.0.5
       data-uri-to-buffer: 6.0.2
       data-uri-to-buffer: 6.0.2
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       fs-extra: 11.2.0
       fs-extra: 11.2.0
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -24061,14 +24121,14 @@ snapshots:
     dependencies:
     dependencies:
       '@tootallnate/once': 2.0.0
       '@tootallnate/once': 2.0.0
       agent-base: 6.0.2
       agent-base: 6.0.2
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
   http-proxy-agent@7.0.2:
   http-proxy-agent@7.0.2:
     dependencies:
     dependencies:
       agent-base: 7.1.4
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -24091,7 +24151,14 @@ snapshots:
   https-proxy-agent@5.0.1:
   https-proxy-agent@5.0.1:
     dependencies:
     dependencies:
       agent-base: 6.0.2
       agent-base: 6.0.2
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  https-proxy-agent@7.0.6:
+    dependencies:
+      agent-base: 7.1.4
+      debug: 4.4.3(supports-color@5.5.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -24512,7 +24579,7 @@ snapshots:
   istanbul-lib-source-maps@5.0.6:
   istanbul-lib-source-maps@5.0.6:
     dependencies:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.31
       '@jridgewell/trace-mapping': 0.3.31
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-coverage: 3.2.2
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -24609,7 +24676,7 @@ snapshots:
       decimal.js: 10.6.0
       decimal.js: 10.6.0
       html-encoding-sniffer: 4.0.0
       html-encoding-sniffer: 4.0.0
       http-proxy-agent: 7.0.2
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@10.0.0)
+      https-proxy-agent: 7.0.6
       is-potential-custom-element-name: 1.0.1
       is-potential-custom-element-name: 1.0.1
       nwsapi: 2.2.22
       nwsapi: 2.2.22
       parse5: 7.3.0
       parse5: 7.3.0
@@ -25763,7 +25830,7 @@ snapshots:
   micromark@4.0.0:
   micromark@4.0.0:
     dependencies:
     dependencies:
       '@types/debug': 4.1.7
       '@types/debug': 4.1.7
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       decode-named-character-reference: 1.0.2
       decode-named-character-reference: 1.0.2
       devlop: 1.1.0
       devlop: 1.1.0
       micromark-core-commonmark: 2.0.1
       micromark-core-commonmark: 2.0.1
@@ -25957,10 +26024,10 @@ snapshots:
     dependencies:
     dependencies:
       async-mutex: 0.4.1
       async-mutex: 0.4.1
       camelcase: 6.3.0
       camelcase: 6.3.0
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       find-cache-dir: 3.3.2
       find-cache-dir: 3.3.2
       follow-redirects: 1.15.11(debug@4.4.3)
       follow-redirects: 1.15.11(debug@4.4.3)
-      https-proxy-agent: 7.0.6(supports-color@10.0.0)
+      https-proxy-agent: 7.0.6
       mongodb: 5.9.2(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))
       mongodb: 5.9.2(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))
       new-find-package-json: 2.0.0
       new-find-package-json: 2.0.0
       semver: 7.7.4
       semver: 7.7.4
@@ -26064,7 +26131,7 @@ snapshots:
 
 
   mquery@4.0.3:
   mquery@4.0.3:
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -26154,7 +26221,7 @@ snapshots:
 
 
   new-find-package-json@2.0.0:
   new-find-package-json@2.0.0:
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -26506,14 +26573,14 @@ snapshots:
 
 
   openapi-types@12.1.3: {}
   openapi-types@12.1.3: {}
 
 
-  openapi-typescript@7.8.0(typescript@5.4.2):
+  openapi-typescript@7.8.0(typescript@5.0.4):
     dependencies:
     dependencies:
       '@redocly/openapi-core': 1.34.5(supports-color@10.0.0)
       '@redocly/openapi-core': 1.34.5(supports-color@10.0.0)
       ansi-colors: 4.1.3
       ansi-colors: 4.1.3
       change-case: 5.4.4
       change-case: 5.4.4
       parse-json: 8.3.0
       parse-json: 8.3.0
       supports-color: 10.0.0
       supports-color: 10.0.0
-      typescript: 5.4.2
+      typescript: 5.0.4
       yargs-parser: 21.1.1
       yargs-parser: 21.1.1
 
 
   openapi3-ts@4.2.2:
   openapi3-ts@4.2.2:
@@ -26654,10 +26721,10 @@ snapshots:
     dependencies:
     dependencies:
       '@tootallnate/quickjs-emscripten': 0.23.0
       '@tootallnate/quickjs-emscripten': 0.23.0
       agent-base: 7.1.4
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       get-uri: 6.0.3
       get-uri: 6.0.3
       http-proxy-agent: 7.0.2
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@10.0.0)
+      https-proxy-agent: 7.0.6
       pac-resolver: 7.0.1
       pac-resolver: 7.0.1
       socks-proxy-agent: 8.0.4
       socks-proxy-agent: 8.0.4
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -26786,7 +26853,7 @@ snapshots:
   passport-saml@3.2.4:
   passport-saml@3.2.4:
     dependencies:
     dependencies:
       '@xmldom/xmldom': 0.7.13
       '@xmldom/xmldom': 0.7.13
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       passport-strategy: 1.0.0
       passport-strategy: 1.0.0
       xml-crypto: 2.1.5
       xml-crypto: 2.1.5
       xml-encryption: 2.0.0
       xml-encryption: 2.0.0
@@ -27069,9 +27136,9 @@ snapshots:
   proxy-agent@6.4.0:
   proxy-agent@6.4.0:
     dependencies:
     dependencies:
       agent-base: 7.1.4
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       http-proxy-agent: 7.0.2
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@10.0.0)
+      https-proxy-agent: 7.0.6
       lru-cache: 7.18.3
       lru-cache: 7.18.3
       pac-proxy-agent: 7.0.2
       pac-proxy-agent: 7.0.2
       proxy-from-env: 1.1.0
       proxy-from-env: 1.1.0
@@ -27117,7 +27184,7 @@ snapshots:
 
 
   puppeteer-cluster@0.24.0(puppeteer@23.6.1(typescript@5.4.2)):
   puppeteer-cluster@0.24.0(puppeteer@23.6.1(typescript@5.4.2)):
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       puppeteer: 23.6.1(typescript@5.4.2)
       puppeteer: 23.6.1(typescript@5.4.2)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -27126,7 +27193,7 @@ snapshots:
     dependencies:
     dependencies:
       '@puppeteer/browsers': 2.4.0
       '@puppeteer/browsers': 2.4.0
       chromium-bidi: 0.8.0(devtools-protocol@0.0.1354347)
       chromium-bidi: 0.8.0(devtools-protocol@0.0.1354347)
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       devtools-protocol: 0.0.1354347
       devtools-protocol: 0.0.1354347
       typed-query-selector: 2.12.0
       typed-query-selector: 2.12.0
       ws: 8.18.3
       ws: 8.18.3
@@ -27945,7 +28012,7 @@ snapshots:
 
 
   require-in-the-middle@7.4.0:
   require-in-the-middle@7.4.0:
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       module-details-from-path: 1.0.3
       module-details-from-path: 1.0.3
       resolve: 1.22.8
       resolve: 1.22.8
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -27989,7 +28056,7 @@ snapshots:
 
 
   retry-request@4.2.2:
   retry-request@4.2.2:
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       extend: 3.0.2
       extend: 3.0.2
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -28475,7 +28542,7 @@ snapshots:
 
 
   socket.io-adapter@2.5.6:
   socket.io-adapter@2.5.6:
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       ws: 8.18.3
       ws: 8.18.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - bufferutil
       - bufferutil
@@ -28485,7 +28552,7 @@ snapshots:
   socket.io-client@4.8.3:
   socket.io-client@4.8.3:
     dependencies:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       engine.io-client: 6.6.4
       engine.io-client: 6.6.4
       socket.io-parser: 4.2.5
       socket.io-parser: 4.2.5
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -28496,7 +28563,7 @@ snapshots:
   socket.io-parser@4.2.5:
   socket.io-parser@4.2.5:
     dependencies:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -28505,7 +28572,7 @@ snapshots:
       accepts: 1.3.8
       accepts: 1.3.8
       base64id: 2.0.0
       base64id: 2.0.0
       cors: 2.8.5
       cors: 2.8.5
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       engine.io: 6.6.5
       engine.io: 6.6.5
       socket.io-adapter: 2.5.6
       socket.io-adapter: 2.5.6
       socket.io-parser: 4.2.5
       socket.io-parser: 4.2.5
@@ -28517,7 +28584,7 @@ snapshots:
   socks-proxy-agent@7.0.0:
   socks-proxy-agent@7.0.0:
     dependencies:
     dependencies:
       agent-base: 6.0.2
       agent-base: 6.0.2
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       socks: 2.8.3
       socks: 2.8.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -28525,7 +28592,7 @@ snapshots:
   socks-proxy-agent@8.0.4:
   socks-proxy-agent@8.0.4:
     dependencies:
     dependencies:
       agent-base: 7.1.4
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       socks: 2.8.3
       socks: 2.8.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -28668,7 +28735,7 @@ snapshots:
   streamroller@3.1.5:
   streamroller@3.1.5:
     dependencies:
     dependencies:
       date-format: 4.0.14
       date-format: 4.0.14
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       fs-extra: 8.1.0
       fs-extra: 8.1.0
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
@@ -28853,7 +28920,7 @@ snapshots:
       cosmiconfig: 9.0.0(typescript@5.0.4)
       cosmiconfig: 9.0.0(typescript@5.0.4)
       css-functions-list: 3.2.2
       css-functions-list: 3.2.2
       css-tree: 2.3.1
       css-tree: 2.3.1
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       fast-glob: 3.3.2
       fast-glob: 3.3.2
       fastest-levenshtein: 1.0.16
       fastest-levenshtein: 1.0.16
       file-entry-cache: 8.0.0
       file-entry-cache: 8.0.0
@@ -28908,7 +28975,7 @@ snapshots:
     dependencies:
     dependencies:
       component-emitter: 1.3.1
       component-emitter: 1.3.1
       cookiejar: 2.1.4
       cookiejar: 2.1.4
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       fast-safe-stringify: 2.1.1
       fast-safe-stringify: 2.1.1
       form-data: 4.0.4
       form-data: 4.0.4
       formidable: 3.5.4
       formidable: 3.5.4
@@ -29497,7 +29564,7 @@ snapshots:
       buffer: 6.0.3
       buffer: 6.0.3
       chalk: 4.1.2
       chalk: 4.1.2
       cli-highlight: 2.1.11
       cli-highlight: 2.1.11
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       dotenv: 8.6.0
       dotenv: 8.6.0
       glob: 7.2.3
       glob: 7.2.3
       js-yaml: 4.1.1
       js-yaml: 4.1.1
@@ -29907,7 +29974,7 @@ snapshots:
   vite-node@2.1.1(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0):
   vite-node@2.1.1(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0):
     dependencies:
     dependencies:
       cac: 6.7.14
       cac: 6.7.14
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       pathe: 1.1.2
       pathe: 1.1.2
       vite: 5.4.21(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0)
       vite: 5.4.21(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0)
     transitivePeerDependencies:
     transitivePeerDependencies:
@@ -29926,7 +29993,7 @@ snapshots:
       '@microsoft/api-extractor': 7.43.0(@types/node@20.19.17)
       '@microsoft/api-extractor': 7.43.0(@types/node@20.19.17)
       '@rollup/pluginutils': 5.2.0(rollup@4.39.0)
       '@rollup/pluginutils': 5.2.0(rollup@4.39.0)
       '@vue/language-core': 1.8.27(typescript@5.0.4)
       '@vue/language-core': 1.8.27(typescript@5.0.4)
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       kolorist: 1.8.0
       kolorist: 1.8.0
       magic-string: 0.30.11
       magic-string: 0.30.11
       typescript: 5.0.4
       typescript: 5.0.4
@@ -29940,7 +30007,7 @@ snapshots:
 
 
   vite-tsconfig-paths@5.0.1(typescript@5.0.4)(vite@5.4.21(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0)):
   vite-tsconfig-paths@5.0.1(typescript@5.0.4)(vite@5.4.21(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0)):
     dependencies:
     dependencies:
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       globrex: 0.1.2
       globrex: 0.1.2
       tsconfck: 3.0.3(typescript@5.0.4)
       tsconfck: 3.0.3(typescript@5.0.4)
     optionalDependencies:
     optionalDependencies:
@@ -29976,7 +30043,7 @@ snapshots:
       '@vitest/spy': 2.1.1
       '@vitest/spy': 2.1.1
       '@vitest/utils': 2.1.1
       '@vitest/utils': 2.1.1
       chai: 5.1.1
       chai: 5.1.1
-      debug: 4.4.3(supports-color@10.0.0)
+      debug: 4.4.3(supports-color@5.5.0)
       magic-string: 0.30.11
       magic-string: 0.30.11
       pathe: 1.1.2
       pathe: 1.1.2
       std-env: 3.7.0
       std-env: 3.7.0