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

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

@@ -9,9 +9,9 @@ The GROWI production Docker build uses a multi-stage Dockerfile: `pruner` → `d
 
 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]).
+This design removes both root causes: in pnpm v10, `pnpm deploy --legacy` already creates 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`).
+**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 --legacy` in pnpm v10.
 
 **Users**: Release engineers and CI/CD pipelines maintaining the production Docker build.
 
@@ -71,7 +71,7 @@ assemble-prod.sh (current):
 ```
 
 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/`
+- **[1b] root cause**: Deploy output is placed at `apps/app/`, but in pnpm v10 `--legacy` already creates self-contained symlinks — step [1b] is no longer needed. (Historical root cause in pre-v10 pnpm: `--legacy` produced workspace-root-pointing symlinks requiring rewriting.)
 - **[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
@@ -83,7 +83,7 @@ graph TB
         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]
+        Deploy[pnpm deploy out --prod --legacy]
         ProdNM[out/node_modules prod-only self-contained]
         Replace[rm node_modules mv out/node_modules node_modules]
         Symlink[ln -sfn apps/app/node_modules]
@@ -116,7 +116,7 @@ graph TB
 ```
 
 **Key decisions**:
-- Drop `--legacy`: isolated linker creates self-contained `out/node_modules/` with relative `.pnpm/` symlinks — no step [1b] needed
+- Keep `--legacy`: in pnpm v10 it creates self-contained `out/node_modules/` with relative `.pnpm/` symlinks — no step [1b] needed. Removing `--legacy` would require `inject-workspace-packages=true` with no practical benefit.
 - 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
 
@@ -125,7 +125,7 @@ graph TB
 | 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 |
+| Package manager | pnpm v10+ | `pnpm deploy --prod --legacy` | Keeps `--legacy`; pnpm v10 produces self-contained symlinks regardless |
 | 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) |
 
@@ -147,7 +147,7 @@ graph TB
     end
 
     subgraph New
-        N1[pnpm deploy --prod no legacy]
+        N1[pnpm deploy --prod --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]
@@ -194,7 +194,7 @@ graph LR
 
 **Responsibilities & Constraints**
 - Run from workspace root (same CWD as current usage)
-- Deploy production dependencies using `pnpm deploy out --prod --filter @growi/app` (no `--legacy`)
+- Deploy production dependencies using `pnpm deploy out --prod --legacy --filter @growi/app`
 - 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
@@ -218,9 +218,9 @@ graph LR
 - 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.
+- Integration: Keeps `pnpm deploy out --prod --legacy` 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 --legacy --filter @growi/app` — confirmed in pnpm v10.32.1
+- Risks: pnpm version may affect `--legacy` behavior — pnpm v9 with `--legacy` produced workspace-root-pointing symlinks (step [1b] was needed); pnpm v10 does not. If downgrading pnpm below v10, step [1b] may need to be reinstated.
 
 ---
 
@@ -384,7 +384,7 @@ graph LR
 
 **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.
+**Phase 1 gate**: `readlink out/node_modules/react` must start with `.pnpm/` after `pnpm deploy out --prod --legacy --filter @growi/app`. Verified in pnpm v10.32.1: result is `.pnpm/react@18.2.0/node_modules/react`. If upgrading/downgrading pnpm, re-verify this output.
 
 ---
 
@@ -401,5 +401,5 @@ graph LR
   ```
 
 ### 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.
+- **`ERR_MODULE_NOT_FOUND`**: Package in `dependencies` was not included in `pnpm deploy` output. Check whether the package is listed in `dependencies` (not `devDependencies`) in `apps/app/package.json`.
 - **`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.

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

@@ -4,7 +4,7 @@
 - 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.
+  1. **pnpm v10**: `pnpm deploy --prod --legacy` creates self-contained symlinks within the deploy output's local `.pnpm/` store (verified: `out/node_modules/react` → `.pnpm/react@18.2.0/node_modules/react`). The `--legacy` flag no longer causes workspace-root-pointing symlinks. Without `--legacy`, pnpm v10 requires `inject-workspace-packages=true` and also produces self-contained symlinks — but introduces an extra config dependency. Keeping `--legacy` is the simpler choice.
   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/`).
 
@@ -20,13 +20,14 @@
   - 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`
+### Topic: `pnpm deploy` symlink behavior with and without `--legacy` (pnpm v10)
 - 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 v10 + `--legacy`**: produces a pnpm-native `.pnpm/` structure with self-contained relative symlinks. Verified in pnpm v10.32.1: `out/node_modules/react` → `.pnpm/react@18.2.0/node_modules/react`; `out/node_modules/@codemirror/state` → `../.pnpm/@codemirror+state@6.5.4/node_modules/@codemirror/state`. The `--legacy` flag now only bypasses the `inject-workspace-packages` gate introduced in pnpm v10; it does NOT produce hoisted/workspace-root-pointing symlinks.
+  - **pnpm v10 without `--legacy`**: fails with `ERR_PNPM_DEPLOY_NONINJECTED_WORKSPACE` unless `inject-workspace-packages=true` is set in `.npmrc`. When set, it also produces self-contained symlinks — identical output to `--legacy`. Introduces an extra config dependency with no practical benefit over keeping `--legacy`.
+  - **Legacy assumption (pre-pnpm v10)**: Earlier pnpm versions with `--legacy` created symlinks referencing the workspace-root `.pnpm/` store, which required step [1b] to rewrite them after `mv`. This assumption is no longer valid in pnpm v10.
   - `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.
+- Implications: Step [1b] is no longer needed in pnpm v10 regardless of `--legacy`. The real root cause of step [1b] was placing the deploy output at `apps/app/` (not `--legacy` itself). Changing the placement to workspace root eliminates both step [1b] and step [2].
 
 ### 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/`)
@@ -61,21 +62,20 @@
 
 | 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 |
+| A: Remove `--legacy` only | Drop `--legacy` from `pnpm deploy`, keep `apps/app/node_modules/` placement | Minimal change | Step [2] still required; pnpm v10 requires `inject-workspace-packages=true` | Partial improvement; adds config dependency |
 | 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** |
+| C: Keep `--legacy` + workspace-root staging | Keep `--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); no `.npmrc` changes | `--legacy` flag remains (cosmetically); pnpm v10 behavior verified | **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
+### Decision: Keep `--legacy` in `pnpm deploy`; eliminate step [1b] by changing placement (not by removing `--legacy`)
+- Context: The original assumption was that `--legacy` caused workspace-root-pointing symlinks, requiring step [1b]. Verified in pnpm v10: `--legacy` produces self-contained `.pnpm/` symlinks — step [1b] is unnecessary regardless of `--legacy`.
 - 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
+  1. Remove `--legacy` — requires `inject-workspace-packages=true` in pnpm v10 (extra config); same symlink output
+  2. Keep `--legacy` — works in pnpm v10 without any `.npmrc` changes (selected)
+- Selected Approach: Keep `--legacy`; eliminate step [1b] by changing placement from `apps/app/` to workspace root
+- Rationale: The true root cause of step [1b] was placing the deploy output at `apps/app/` (misaligned with Turbopack's symlink base), not `--legacy` itself. Fixing placement eliminates both step [1b] and step [2] without requiring `.npmrc` changes.
+- Trade-offs: `--legacy` flag remains in the script. It is now a pnpm v10 gate-bypass, not a linker-mode selector.
 
 ### 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/`.
@@ -92,12 +92,12 @@
 - 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`
+- Selected Approach: `pnpm deploy out --prod --legacy --filter @growi/app`
 - 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
+- pnpm version compatibility: `--legacy` behavior changed between pnpm v9 and v10 (v9: hoisted symlinks; v10: self-contained). The implementation assumes pnpm v10+ behavior. — Mitigation: verify in CI with the same pnpm version as the Dockerfile; if downgrading pnpm, step [1b] may need to be reinstated.
 - `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
 

+ 13 - 12
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/tasks.md

@@ -4,36 +4,37 @@
 
 | Phase | Major Task | Sub-tasks | Requirements |
 |-------|-----------|-----------|--------------|
-| 1 | Verify `pnpm deploy` (without `--legacy`) symlink behavior | 1.1 | 2.1, 2.2, 2.3 |
+| 1 | Verify `pnpm deploy --legacy` symlink behavior in pnpm v10 | 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
+- [x] 1. Verify `pnpm deploy --legacy` creates self-contained symlinks in pnpm v10
 
-- [ ] 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`
+- [x] 1.1 Run `pnpm deploy out --prod --legacy --filter @growi/app` and inspect top-level symlinks in `out/node_modules/`
+  - From workspace root, run `rm -rf out && pnpm deploy out --prod --legacy --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
+  - If either symlink points to `../../../node_modules/.pnpm/` or `../../../../node_modules/.pnpm/`, pnpm version is pre-v10 and step [1b] must be retained; do not proceed with removal
   - Verify `out/node_modules/.pnpm/` exists and contains physical package directories (not symlinks to the workspace-root `.pnpm/` store)
   - Clean up: `rm -rf out`
+  - **Verified in pnpm v10.32.1**: `react` → `.pnpm/react@18.2.0/node_modules/react`; `@codemirror/state` → `../.pnpm/@codemirror+state@6.5.4/node_modules/@codemirror/state` ✓
   - _Requirements: 2.1, 2.2, 2.3_
 
 ---
 
-- [ ] 2. Simplify `assemble-prod.sh` by removing `--legacy`, both symlink-rewrite steps, and restructuring node_modules placement
+- [x] 2. Simplify `assemble-prod.sh` by deleting 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`)
+- [x] 2.1 Delete the step [1b] bash block (keep `--legacy`)
+  - In `apps/app/bin/assemble-prod.sh`, keep `pnpm deploy out --prod --legacy --filter @growi/app` unchanged
   - 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)
+  - In pnpm v10, `--legacy` already creates self-contained `.pnpm/` symlinks; step [1b] rewriting is no longer needed
+  - Update the progress step count in remaining echo messages to reflect removal of step [1b]
   - _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
+- [x] 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
@@ -48,7 +49,7 @@
 
 ---
 
-- [ ] 3. (P) Update Dockerfile `builder`-stage artifact staging to include workspace-root `node_modules/`
+- [x] 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`)

+ 10 - 37
apps/app/bin/assemble-prod.sh

@@ -3,51 +3,24 @@
 # Run from the workspace root.
 set -euo pipefail
 
-echo "[1/4] Deploying production dependencies..."
+echo "[1/3] Deploying production dependencies..."
 rm -rf out
 pnpm deploy out --prod --legacy --filter @growi/app
+rm -rf node_modules
+mv out/node_modules node_modules
 rm -rf apps/app/node_modules
-mv out/node_modules apps/app/node_modules
-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/.
-# Turbopack generates symlinks pointing to ../../../../node_modules/.pnpm/ (workspace root),
-# which will not exist in production environments.
-# Rewriting to ../../node_modules/.pnpm/ (apps/app/) uses the pnpm deploy output instead,
-# preserving pnpm's isolated structure so transitive deps remain resolvable.
-echo "[2/4] Rewriting .next/node_modules symlinks..."
-if [ -d apps/app/.next/node_modules ]; then
-  find apps/app/.next/node_modules -maxdepth 2 -type l | while read -r link; do
-    target=$(readlink "$link")
-    new_target=$(echo "$target" | sed 's|../../../../node_modules/\.pnpm/|../../node_modules/.pnpm/|')
-    if [ "$target" != "$new_target" ]; then ln -sfn "$new_target" "$link"; fi
-  done
-else
-  echo "[2/4] Skipped (no .next/node_modules directory)."
-fi
-echo "[2/4] Done."
+ln -sfn ../../node_modules apps/app/node_modules
+rm -rf out
+echo "[1/3] Done."
 
-echo "[3/4] Removing build cache..."
+echo "[2/3] Removing build cache..."
 rm -rf apps/app/.next/cache
-echo "[3/4] Done."
+echo "[2/3] Done."
 
 # Remove next.config.ts to prevent Next.js from attempting to install TypeScript at server startup,
 # which would corrupt node_modules (e.g. @growi/core).
-echo "[4/4] Removing next.config.ts..."
+echo "[3/3] Removing next.config.ts..."
 rm -f apps/app/next.config.ts
-echo "[4/4] Done."
+echo "[3/3] Done."
 
 echo "Assembly complete."

+ 1 - 0
apps/app/docker/Dockerfile

@@ -96,6 +96,7 @@ RUN bash apps/app/bin/assemble-prod.sh
 # Stage artifacts into a clean directory for COPY --from
 RUN mkdir -p /tmp/release/apps/app && \
   cp package.json /tmp/release/ && \
+  cp -a node_modules /tmp/release/ && \
   cp -a apps/app/.next apps/app/config apps/app/dist apps/app/public \
        apps/app/resource apps/app/tmp \
        apps/app/package.json apps/app/node_modules \