Răsfoiți Sursa

remove spec

Yuki Takei 1 lună în urmă
părinte
comite
2b5c1f0866

+ 0 - 518
.kiro/specs/migrate-to-turbopack/design.md

@@ -1,518 +0,0 @@
-# Design Document: Migrate to Turbopack
-
-## Overview
-
-**Purpose**: This feature migrates GROWI's Next.js bundler from webpack to Turbopack, delivering dramatically faster dev server compilation (5-10x faster Fast Refresh) and improved build performance for the development team.
-
-**Users**: All GROWI developers will benefit from faster HMR, shorter page compilation times, and reduced dev server startup latency.
-
-**Impact**: Changes the build pipeline in `apps/app` by replacing the webpack bundler configuration with Turbopack equivalents while preserving all existing custom functionality (SuperJSON SSR, server-only package exclusion, ESM transpilation).
-
-### Goals
-- Enable Turbopack as the default bundler for `next dev` with the custom Express server
-- Migrate all 6 webpack customizations to Turbopack-compatible equivalents
-- Precompile all vendor CSS into JS modules via Vite for Turbopack compliance
-- Convert all `:global` block form syntax in CSS Modules to function form for Turbopack compatibility
-- Maintain webpack fallback via environment variable during the transition period
-- Enable Turbopack for production `next build` after dev stability is confirmed
-
-### Non-Goals
-- Migrating from Pages Router to App Router
-- Replacing next-i18next with a different i18n library
-- Rewriting the custom Express server architecture
-- Achieving feature parity for dev module analysis tooling (deferred)
-- Implementing i18n HMR under Turbopack (acceptable tradeoff)
-- Refactoring CSS architecture beyond the mechanical syntax conversion
-
-## Architecture
-
-### Existing Architecture Analysis
-
-The current build pipeline uses webpack exclusively, configured via:
-
-1. **Server initialization**: `src/server/crowi/index.ts` calls `next({ dev, webpack: true })` to explicitly opt out of Turbopack
-2. **next.config.ts**: Contains `webpack()` hook with 6 custom configurations (loaders, plugins, resolve fallbacks)
-3. **Build scripts**: `next build --webpack` and `next dev` (via nodemon) both use webpack
-4. **i18n config**: `config/next-i18next.config.js` loads `I18NextHMRPlugin` conditionally in dev mode
-
-Key constraints:
-- Pages Router with `.page.{ts,tsx}` extension convention
-- Custom Express server with middleware stack
-- 70+ ESM packages requiring transpilation
-- Server/client boundary enforcement via null-loader
-
-### Architecture Pattern & Boundary Map
-
-```mermaid
-graph TB
-    subgraph ConfigLayer[Configuration Layer]
-        NextConfig[next.config.ts]
-        I18nConfig[next-i18next.config.js]
-    end
-
-    subgraph ServerInit[Server Initialization]
-        Crowi[crowi/index.ts]
-    end
-
-    subgraph BundlerSelection[Bundler Selection]
-        EnvFlag[USE_WEBPACK env var]
-        EnvFlag -->|true| Webpack[Webpack Pipeline]
-        EnvFlag -->|false/unset| Turbopack[Turbopack Pipeline]
-    end
-
-    subgraph TurboConfig[Turbopack Config]
-        Rules[turbopack.rules]
-        Aliases[turbopack.resolveAlias]
-    end
-
-    subgraph WebpackConfig[Webpack Config - Fallback]
-        WPHook[webpack hook]
-        NullLoader[null-loader rules]
-        SSRLoader[superjson-ssr-loader]
-        Plugins[I18NextHMRPlugin + Stats]
-    end
-
-    Crowi --> EnvFlag
-    NextConfig --> TurboConfig
-    NextConfig --> WebpackConfig
-    Rules --> SuperjsonRule[superjson-ssr-loader server-only]
-    Aliases --> PkgAliases[Package exclusion aliases]
-    Aliases --> FsAlias[fs browser alias]
-    I18nConfig -->|Turbopack mode| NoHMR[No HMR plugins]
-    I18nConfig -->|webpack mode| Plugins
-```
-
-**Architecture Integration**:
-- Selected pattern: Feature-flag phased migration with dual configuration
-- Domain boundaries: Build configuration layer only — no changes to application code, server logic, or page components
-- Existing patterns preserved: Express custom server, Pages Router, SuperJSON SSR, server-client boundary
-- New components: Empty module file, Turbopack config block, env-based bundler selection, Vite vendor CSS precompilation
-- Steering compliance: Maintains server-client boundary enforcement; aligns with existing build optimization strategy
-- CSS compatibility: All `:global` block form syntax converted to function form; all vendor CSS precompiled via Vite into JS modules
-
-### Technology Stack
-
-| Layer | Choice / Version | Role in Feature | Notes |
-|-------|------------------|-----------------|-------|
-| Bundler (primary) | Turbopack (Next.js 16 built-in) | Dev and build bundler | Replaces webpack as default |
-| Bundler (fallback) | Webpack (Next.js 16 built-in) | Fallback during transition | Retained via `--webpack` flag |
-| Runtime | Next.js ^16.0.0 | Framework providing both bundler options | `next()` API supports `turbopack` parameter |
-| Loader compat | loader-runner (Turbopack built-in) | Executes webpack loaders in Turbopack | Subset of webpack loader API |
-
-No new external dependencies are introduced. The migration uses only built-in Next.js 16 capabilities.
-
-## System Flows
-
-### Dev Server Startup with Bundler Selection
-
-```mermaid
-sequenceDiagram
-    participant Dev as Developer
-    participant Nodemon as Nodemon
-    participant Crowi as crowi/index.ts
-    participant NextAPI as next() API
-    participant TP as Turbopack
-    participant WP as Webpack
-
-    Dev->>Nodemon: pnpm run dev
-    Nodemon->>Crowi: Execute src/server/app.ts
-    Crowi->>Crowi: Check USE_WEBPACK env var
-
-    alt USE_WEBPACK is set
-        Crowi->>NextAPI: next({ dev, webpack: true })
-        NextAPI->>WP: Initialize webpack pipeline
-        WP->>WP: Apply webpack() hook config
-    else Default - Turbopack
-        Crowi->>NextAPI: next({ dev })
-        NextAPI->>TP: Initialize Turbopack pipeline
-        TP->>TP: Apply turbopack config from next.config.ts
-    end
-
-    NextAPI-->>Crowi: app.prepare() resolves
-    Crowi->>Dev: Server ready
-```
-
-## Requirements Traceability
-
-| Requirement | Summary | Components | Interfaces | Flows |
-|-------------|---------|------------|------------|-------|
-| 1.1 | Remove webpack: true from next() | ServerInit | next() options | Dev startup |
-| 1.2 | Turbopack config in next.config.ts | TurbopackConfig | turbopack key | - |
-| 1.3 | Fast Refresh equivalent | TurbopackConfig | Built-in | Dev startup |
-| 1.4 | Faster HMR for .page.tsx | TurbopackConfig | Built-in | Dev startup |
-| 2.1 | Alias server packages to empty module | ResolveAliasConfig | resolveAlias | - |
-| 2.2 | Resolve fs to false in browser | ResolveAliasConfig | resolveAlias | - |
-| 2.3 | No server packages in client output | ResolveAliasConfig | resolveAlias | - |
-| 3.1 | Register superjson-ssr-loader | TurbopackRulesConfig | turbopack.rules | - |
-| 3.2 | Auto-wrap getServerSideProps | TurbopackRulesConfig | turbopack.rules | - |
-| 3.3 | Identical SuperJSON output | TurbopackRulesConfig | Loader output | - |
-| 3.4 | Fallback mechanism if loader incompatible | TurbopackRulesConfig | - | - |
-| 4.1 | ESM packages without ERR_REQUIRE_ESM | TranspileConfig | transpilePackages | - |
-| 4.2 | Remark/rehype ecosystem bundles correctly | TranspileConfig | transpilePackages | - |
-| 5.1 | Translation changes reflected in browser | I18nConfig | Manual refresh | - |
-| 5.2 | Alternative i18n HMR or documented tradeoff | I18nConfig | Documentation | - |
-| 5.3 | next-i18next functions under Turbopack | I18nConfig | Runtime config | - |
-| 6.1 | Production bundle works | BuildConfig | next build | - |
-| 6.2 | Turbopack or webpack fallback for build | BuildConfig | --webpack flag | - |
-| 6.3 | Production tests pass | BuildConfig | Test suite | - |
-| 7.1 | Alternative module analysis | Deferred | - | - |
-| 7.2 | DUMP_INITIAL_MODULES report | Deferred | - | - |
-| 8.1 | Vendor CSS precompiled via Vite ?inline | VendorCSSPrecompilation | .vendor-styles.ts files | - |
-| 8.2 | All direct CSS imports migrated | VendorCSSPrecompilation | Component imports | - |
-| 8.3 | Naming convention *.vendor-styles.ts | VendorCSSPrecompilation | File naming | - |
-| 8.4 | Turborepo pre:styles-components tasks | VendorCSSPrecompilation | turbo.json | - |
-| 8.5 | Prebuilt files git-ignored | VendorCSSPrecompilation | .gitignore | - |
-| 9.1 | Function form `:global(...)` syntax | GlobalSyntaxConversion | 128 files | - |
-| 9.2 | Identical CSS output after conversion | GlobalSyntaxConversion | Compilation | - |
-| 9.3 | Nested blocks converted correctly | GlobalSyntaxConversion | Nested selectors | - |
-| 9.4 | No Ambiguous CSS errors under Turbopack | GlobalSyntaxConversion | Dev startup | Dev startup |
-| 9.5 | webpack fallback works with function form | GlobalSyntaxConversion | USE_WEBPACK | - |
-| 10.1 | Switch via env var or CLI flag | BundlerSwitch | USE_WEBPACK env | Dev startup |
-| 10.2 | Webpack mode fully functional | WebpackFallback | webpack() hook | - |
-| 10.3 | Dual config in next.config.ts | NextConfigDual | Both configs | - |
-
-## Components and Interfaces
-
-| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
-|-----------|-------------|--------|--------------|------------------|-----------|
-| ServerInit | Server | Toggle bundler via env var in next() call | 1.1, 1.3, 1.4, 8.1 | next() API (P0) | - |
-| TurbopackConfig | Config | Define turbopack block in next.config.ts | 1.2, 3.1, 3.2 | next.config.ts (P0) | - |
-| ResolveAliasConfig | Config | Alias server-only packages and fs in browser | 2.1, 2.2, 2.3 | empty-module.ts (P0) | - |
-| EmptyModule | Util | Provide empty export file for resolveAlias | 2.1, 2.2 | None | - |
-| I18nConfig | Config | Remove HMR plugins when Turbopack is active | 5.1, 5.2, 5.3 | next-i18next (P1) | - |
-| VendorCSSPrecompilation | CSS/Build | Precompile third-party CSS into JS via Vite | 8.1, 8.2, 8.3, 8.4, 8.5 | Vite, Turborepo (P0) | - |
-| GlobalSyntaxConversion | CSS | Convert `:global` block form to function form | 9.1, 9.2, 9.3, 9.4, 9.5 | 128 .module.scss files (P0) | - |
-| BuildScripts | Config | Update package.json scripts for Turbopack | 6.1, 6.2, 10.1 | package.json (P0) | - |
-| WebpackFallback | Config | Maintain webpack() hook for fallback | 10.2, 10.3 | next.config.ts (P0) | - |
-
-### Configuration Layer
-
-#### ServerInit — Bundler Toggle
-
-| Field | Detail |
-|-------|--------|
-| Intent | Select Turbopack or webpack based on environment variable when initializing Next.js |
-| Requirements | 1.1, 1.3, 1.4, 8.1 |
-
-**Responsibilities & Constraints**
-- Read `USE_WEBPACK` environment variable to determine bundler choice
-- Pass appropriate option to `next()` API: omit `webpack: true` for Turbopack (default), keep it for webpack fallback
-- Preserve existing ts-node hook save/restore logic around `app.prepare()`
-
-**Dependencies**
-- External: Next.js `next()` API — bundler selection (P0)
-
-**Implementation Notes**
-- Integration: Minimal change in `src/server/crowi/index.ts` — replace hardcoded `webpack: true` with conditional
-- Validation: Verify server starts correctly with both `USE_WEBPACK=1` and without
-- Risks: None — `next()` API parameter is officially documented
-
-#### TurbopackConfig — Rules and Loaders
-
-| Field | Detail |
-|-------|--------|
-| Intent | Configure Turbopack-specific rules for custom loaders in next.config.ts |
-| Requirements | 1.2, 3.1, 3.2, 3.3, 3.4 |
-
-**Responsibilities & Constraints**
-- Register `superjson-ssr-loader` for `*.page.ts` and `*.page.tsx` files with server-only condition
-- Use `turbopack.rules` with `condition: { not: 'browser' }` for server-side targeting
-- Loader must return JavaScript code (already satisfied by existing loader)
-
-**Dependencies**
-- Inbound: next.config.ts — configuration host (P0)
-- External: Turbopack loader-runner — loader execution (P0)
-- External: superjson-ssr-loader.ts — existing loader (P0)
-
-**Contracts**: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [ ]
-
-**Implementation Notes**
-- Integration: Add `turbopack` key to nextConfig object alongside existing `webpack()` hook
-- Validation: Verify pages with `getServerSideProps` still have SuperJSON wrapping applied
-- Risks: If loader-runner subset causes issues, fallback to code generation approach (pre-process files before build). See `research.md` Risk 2.
-
-##### Turbopack Rules Configuration Shape
-
-```typescript
-// Type definition for the turbopack.rules config
-interface TurbopackRulesConfig {
-  turbopack: {
-    rules: {
-      // Server-only: superjson-ssr-loader for .page.ts/.page.tsx
-      '*.page.ts': Array<{
-        condition: { not: 'browser' };
-        loaders: string[];  // [path.resolve(__dirname, 'src/utils/superjson-ssr-loader.ts')]
-        as: '*.ts';
-      }>;
-      '*.page.tsx': Array<{
-        condition: { not: 'browser' };
-        loaders: string[];
-        as: '*.tsx';
-      }>;
-    };
-    resolveAlias: Record<string, string | { browser: string }>;
-  };
-}
-```
-
-#### ResolveAliasConfig — Package Exclusion
-
-| Field | Detail |
-|-------|--------|
-| Intent | Exclude server-only packages from client bundle using Turbopack resolveAlias |
-| Requirements | 2.1, 2.2, 2.3 |
-
-**Responsibilities & Constraints**
-- Map 7 server-only packages to empty module in browser context
-- Map `fs` to empty module in browser context
-- Use conditional `{ browser: '...' }` syntax for client-only aliasing
-
-**Dependencies**
-- External: EmptyModule file — alias target (P0)
-
-##### Resolve Alias Mapping
-
-```typescript
-// resolveAlias configuration mapping
-interface ResolveAliasConfig {
-  // fs fallback for browser
-  fs: { browser: string };  // path to empty module
-
-  // Server-only packages aliased to empty module in browser
-  'dtrace-provider': { browser: string };
-  mongoose: { browser: string };
-  'mathjax-full': { browser: string };
-  'i18next-fs-backend': { browser: string };
-  bunyan: { browser: string };
-  'bunyan-format': { browser: string };
-  'core-js': { browser: string };
-}
-```
-
-**Implementation Notes**
-- Integration: Add `resolveAlias` under `turbopack` key in next.config.ts
-- Validation: Verify client pages render without "Module not found" errors for each aliased package
-- Risks: The current webpack null-loader uses regex patterns (e.g., `/\/bunyan\//`) which match file paths. Turbopack resolveAlias uses package names. The `bunyan` alias should match imports of `bunyan` but must not interfere with `browser-bunyan`. Test carefully. See `research.md` Risk 3.
-
-#### EmptyModule — Alias Target
-
-| Field | Detail |
-|-------|--------|
-| Intent | Provide a minimal JavaScript module that exports nothing, used as alias target for excluded packages |
-| Requirements | 2.1, 2.2 |
-
-**Responsibilities & Constraints**
-- Export an empty default and named export to satisfy any import style
-- File location: `src/lib/empty-module.ts`
-
-**Implementation Notes**
-- Minimal file: `export default {}; export {};`
-
-#### I18nConfig — HMR Plugin Removal
-
-| Field | Detail |
-|-------|--------|
-| Intent | Conditionally remove i18next-hmr plugins when Turbopack is active |
-| Requirements | 5.1, 5.2, 5.3 |
-
-**Responsibilities & Constraints**
-- Detect whether Turbopack or webpack is active
-- When Turbopack: exclude `I18NextHMRPlugin` from webpack plugins and `HMRPlugin` from i18next `use` array
-- When webpack: preserve existing HMR plugin behavior
-
-**Dependencies**
-- Inbound: next.config.ts — plugin registration (P1)
-- Inbound: next-i18next.config.js — i18next plugin registration (P1)
-- External: i18next-hmr — webpack-only HMR (P1)
-
-**Implementation Notes**
-- Integration: In `next.config.ts`, the `I18NextHMRPlugin` is inside the `webpack()` hook which only runs when webpack is active — no change needed there. In `next-i18next.config.js`, the `HMRPlugin` is loaded regardless of bundler — need to guard it with the same `USE_WEBPACK` env var check or detect Turbopack via absence of webpack context.
-- Validation: Verify next-i18next functions correctly (routing, SSR translations) under Turbopack without HMR plugins
-- Risks: `HMRPlugin` in next-i18next.config.js may reference webpack internals that fail under Turbopack. Guard with env var check. See `research.md` Risk 1.
-
-#### BuildScripts — Package.json Updates
-
-| Field | Detail |
-|-------|--------|
-| Intent | Update build and dev scripts to reflect Turbopack as default with webpack opt-in |
-| Requirements | 6.1, 6.2, 8.1 |
-
-**Responsibilities & Constraints**
-- `build:client` script: keep `next build --webpack` initially (Phase 1), migrate to `next build` in Phase 2
-- `dev` script: no change needed (bundler selection happens in crowi/index.ts, not CLI)
-- `launch-dev:ci` script: inherits bundler from crowi/index.ts
-
-**Implementation Notes**
-- Phase 1: Only server initialization changes. Build scripts remain unchanged.
-- Phase 2: Remove `--webpack` from `build:client` after Turbopack build verification.
-
-### CSS Compatibility Layer
-
-#### VendorCSSPrecompilation — Global CSS Import Migration via Vite
-
-| Field | Detail |
-|-------|--------|
-| Intent | Precompile third-party CSS into JS modules via Vite to comply with Turbopack's strict global CSS import rule |
-| Requirements | 8.1, 8.2, 8.3, 8.4, 8.5 |
-
-**Responsibilities & Constraints**
-- Create `{ComponentName}.vendor-styles.ts` entry point files using Vite's `?inline` CSS import suffix
-- Each entry point imports CSS as inline strings and injects them into `document.head` via `<style>` tag at runtime
-- Vite precompiles these into `{ComponentName}.vendor-styles.prebuilt.js` files (git-ignored, regenerated by Turborepo)
-- Components import the prebuilt `.js` file instead of raw CSS
-- Two-track system: commons track (`vendor.scss` for globally shared CSS) and components track (`*.vendor-styles.ts` for component-specific CSS)
-
-**Vite Build Configuration** (`vite.vendor-styles-components.ts`)
-- Collects all `src/**/*.vendor-styles.ts` as entry points via `fs.globSync`
-- Outputs to `src/` preserving directory structure, with `.vendor-styles.prebuilt.js` suffix
-- Uses `rollupOptions.input` (not `lib.entry`) for multi-entry compilation
-- Includes `moveAssetsToPublic` Vite plugin that runs at `closeBundle`:
-  1. Moves emitted font/asset files from `src/assets/` to `public/static/fonts/`
-  2. Rewrites `/assets/` → `/static/fonts/` URL references in all `.vendor-styles.prebuilt.js` files
-  3. Removes the temporary `src/assets/` directory
-- Fonts are served at `/static/fonts/*` via the existing `express.static(crowi.publicDir)` route (which serves `public/` at `/`)
-- `/public/static/fonts` is git-ignored (build artifact, regenerated by `pre:styles-components`)
-
-**Turborepo Integration**
-- `pre:styles-components` / `dev:pre:styles-components` tasks added as dependencies of `build` and `dev`
-- Inputs: `vite.vendor-styles-components.ts`, `src/**/*.vendor-styles.ts`, `package.json`
-- Outputs: `src/**/*.vendor-styles.prebuilt.js`, `public/static/fonts/**`
-
-**Vendor-Styles Entry Points (8 files covering 13 CSS imports)**
-
-| Entry Point | CSS Sources | Consuming Components |
-|---|---|---|
-| `Renderer.vendor-styles.ts` | `@growi/remark-lsx`, `@growi/remark-attachment-refs`, `katex` | renderer.tsx |
-| `GrowiEditor.vendor-styles.ts` | `@growi/editor` | PageEditor, CommentEditor |
-| `HandsontableModal.vendor-styles.ts` | `handsontable` (non-full variant) | HandsontableModal |
-| `DateRangePicker.vendor-styles.ts` | `react-datepicker` | DateRangePicker |
-| `RevisionDiff.vendor-styles.ts` | `diff2html` | RevisionDiff |
-| `DrawioViewerWithEditButton.vendor-styles.ts` | `@growi/remark-drawio` | DrawioViewerWithEditButton |
-| `ImageCropModal.vendor-styles.ts` | `react-image-crop` | ImageCropModal |
-| `Presentation.vendor-styles.ts` | `@growi/presentation` | Presentation, Slides |
-
-**Note**: `simplebar-react` CSS is handled by the commons track (`src/styles/vendor.scss`) — its direct import was simply removed from `Sidebar.tsx`.
-
-**Implementation Notes**
-- Entry points use `// @ts-nocheck` since `?inline` is a Vite-specific import suffix not understood by TypeScript
-- Multiple CSS imports for the same component are combined in a single entry point (e.g., `Renderer.vendor-styles.ts` imports 3 CSS files)
-- Shared entry points (e.g., `GrowiEditor.vendor-styles.ts`) are referenced from multiple components via relative path
-- SSR note: CSS is injected at runtime via `<style>` tags, so it is not available during SSR. Most consuming components already use `next/dynamic` with `ssr: false`, avoiding FOUC
-- **Font/asset handling**: When vendor CSS references external assets (e.g., KaTeX's `@font-face` declarations referencing `fonts/KaTeX_*.woff2`), Vite emits them to `src/assets/` during build. The `moveAssetsToPublic` plugin then relocates these to `public/static/fonts/` and rewrites the URL references in prebuilt JS to `/static/fonts/...`. This ensures fonts are served by the existing Express static file middleware without additional server configuration.
-
-#### GlobalSyntaxConversion — `:global` Block-to-Function Form
-
-| Field | Detail |
-|-------|--------|
-| Intent | Convert all `:global` block form syntax to function form for Turbopack CSS Modules compatibility |
-| Requirements | 9.1, 9.2, 9.3, 9.4, 9.5 |
-
-**Responsibilities & Constraints**
-- Convert 128 `.module.scss` files containing 255 occurrences of `:global` block form
-- The conversion is mechanical — each block form has an exact function form equivalent
-- Both webpack and Turbopack support the function form, so no behavioral change
-
-**Conversion Patterns**
-
-| # | Block Form (Before) | Function Form (After) |
-|---|---|---|
-| 1 | `.parent :global { .child { color: red; } }` | `.parent { :global(.child) { color: red; } }` |
-| 2 | `.parent :global { .a { } .b { } }` | `.parent { :global(.a) { } :global(.b) { } }` |
-| 3 | `.parent :global { .child { .nested { } } }` | `.parent { :global(.child) { :global(.nested) { } } }` |
-| 4 | `&:global { &.modifier { } }` | `&:global(.modifier) { }` |
-| 5 | `:global { .class { } }` (top-level) | `:global(.class) { }` |
-| 6 | `.parent :global { .child { &.modifier { } } }` | `.parent { :global(.child) { &:global(.modifier) { } } }` |
-
-**Implementation Strategy**
-- Perform conversion file-by-file using AST-aware or regex-based transformation
-- Each converted file must produce identical CSS output
-- Verify with stylelint and visual diff after conversion
-- webpack fallback (`USE_WEBPACK=1`) must continue to work with function form syntax
-
-**Dependencies**
-- None — pure syntax transformation, no runtime or build tool changes
-
-**Risks**
-- Complex nested structures may require manual attention
-- The vendor wrapper files (`vendor-*.module.scss`) use `:global { @import }` which is a different pattern — these are NOT affected by this conversion since they use the top-level `:global` block as a CSS Modules scoping mechanism for imported stylesheets
-
-## Error Handling
-
-### Error Strategy
-
-Build-time errors from Turbopack migration are the primary concern. All errors surface during compilation and are visible in terminal output.
-
-### Error Categories and Responses
-
-**Module Resolution Errors**: `Cannot resolve 'X'` — indicates a package not properly aliased in `resolveAlias`. Fix: add the missing package to the alias map.
-
-**Loader Execution Errors**: `Turbopack loader failed` — indicates `superjson-ssr-loader` incompatibility. Fix: check loader-runner API usage; fallback to code generation if needed.
-
-**Runtime Errors**: `SuperJSON deserialization failed` — indicates loader transform produced different output. Fix: compare webpack and Turbopack loader output for affected pages.
-
-**i18n Errors**: `Cannot find module 'i18next-hmr'` or HMR plugin crash — indicates HMR plugin loaded in Turbopack mode. Fix: guard HMR plugin loading with env var check.
-
-**Global CSS Import Errors**: `Global CSS cannot be imported from files other than your Custom <App>` — indicates a direct global CSS import from a non-`_app` file. Fix: create a `{Component}.vendor-styles.ts` entry point, run `pre:styles-components`, and import the prebuilt `.js` file instead.
-
-**CSS Modules Syntax Errors**: `Ambiguous CSS module class not supported` — indicates `:global` block form usage. Fix: convert the block form to function form `:global(...)`.
-
-## Testing Strategy
-
-### Smoke Tests (Manual, Phase 1)
-1. Start dev server with Turbopack (default): verify all pages load without errors
-2. Start dev server with `USE_WEBPACK=1`: verify webpack fallback works identically
-3. Edit a `.page.tsx` file: verify Fast Refresh applies the change
-4. Navigate to a page with `getServerSideProps`: verify SuperJSON data renders correctly
-5. Navigate to a page importing remark/rehype plugins: verify Markdown rendering works
-6. Verify no "Module not found" errors in browser console for server-only packages
-
-### Regression Tests (Automated)
-1. Run existing `vitest run` test suite — all tests must pass (tests don't depend on bundler)
-2. Run `turbo run lint --filter @growi/app` — all lint checks must pass
-3. Run `next build --webpack` — verify webpack build still works (fallback)
-4. Run `next build` (Turbopack) — verify Turbopack production build works (Phase 2)
-
-### Integration Verification
-1. Test i18n: Switch locale in the UI, verify translations load correctly
-2. Test SuperJSON: Visit pages with complex serialized props (ObjectId, dates), verify correct rendering
-3. Test client bundle: Check browser DevTools network tab to confirm excluded packages are not in client JS
-
-## Migration Strategy
-
-### Phase 1: Dev Server Migration (Immediate)
-
-```mermaid
-flowchart LR
-    A[Add turbopack config] --> B[Add empty-module.ts]
-    B --> C[Update crowi/index.ts]
-    C --> D[Guard i18n HMR plugins]
-    D --> E[Vendor CSS precompilation]
-    E --> F[Convert :global syntax]
-    F --> G[Smoke test Turbopack dev]
-    G --> H[Smoke test webpack fallback]
-    H --> I[Merge to dev branch]
-```
-
-1. Add `turbopack` configuration block to `next.config.ts` (rules + resolveAlias)
-2. Create `src/lib/empty-module.ts`
-3. Update `src/server/crowi/index.ts`: replace `webpack: true` with `USE_WEBPACK` env var check
-4. Guard `HMRPlugin` in `next-i18next.config.js` with env var check
-5. Precompile vendor CSS via Vite `?inline` into `.vendor-styles.prebuilt.js` files
-6. Convert all `:global` block form syntax to function form across 128 `.module.scss` files
-7. Run smoke tests with both Turbopack and webpack modes
-8. Merge — all developers now use Turbopack by default for dev
-
-### Phase 2: Production Build Migration (After Verification)
-
-1. Verify Turbopack dev has been stable for a sufficient period
-2. Remove `--webpack` from `build:client` script
-3. Test production build with Turbopack
-4. Run full CI pipeline to validate
-
-### Phase 3: Cleanup (After Full Stability)
-
-1. Remove `webpack()` hook from `next.config.ts`
-2. Remove `USE_WEBPACK` env var check from `crowi/index.ts`
-3. Remove `I18NextHMRPlugin` imports and `HMRPlugin` references entirely
-4. Remove `ChunkModuleStatsPlugin` and related code
-5. Evaluate removing unnecessary `transpilePackages` entries

+ 0 - 160
.kiro/specs/migrate-to-turbopack/requirements.md

@@ -1,160 +0,0 @@
-# Requirements Document
-
-## Introduction
-
-Migrate the GROWI main application (`apps/app`) from webpack to Turbopack for Next.js dev and build pipelines. The primary goal is to dramatically improve dev server compilation speed (HMR / Fast Refresh) while preserving all existing custom functionality.
-
-### Background
-
-GROWI uses a custom Express server with `next({ dev, webpack: true })` to initialize Next.js. As of Next.js 16, the `next()` programmatic API officially supports a `turbopack: true` option (enabled by default), making Turbopack compatible with custom servers. The current webpack opt-out exists solely because custom webpack loaders/plugins in `next.config.ts` require migration to Turbopack equivalents.
-
-### Key Research Findings
-
-1. **Custom server support confirmed**: Next.js 16 `next()` API accepts `turbopack` and `webpack` boolean options. Turbopack is enabled by default — GROWI's `webpack: true` is an explicit opt-out.
-2. **Turbopack loader compatibility**: Turbopack supports a subset of webpack loaders via `loader-runner`. Only loaders returning JavaScript are supported. Conditions (`browser`, `{not: 'browser'}`, `development`, etc.) are available for fine-grained rule targeting.
-3. **resolveAlias**: Replaces webpack `resolve.fallback` and `null-loader` patterns. Supports conditional aliasing (e.g., `{ browser: './empty.js' }`).
-4. **No webpack plugin API**: Turbopack does not support arbitrary webpack plugins. `I18NextHMRPlugin` and `ChunkModuleStatsPlugin` cannot be ported directly.
-
-### Current Webpack Customizations (Migration Scope)
-
-| # | Customization | Type | Turbopack Path |
-|---|---|---|---|
-| 1 | `superjson-ssr-loader` (server-side, `.page.{ts,tsx}`) | Loader | `turbopack.rules` with `{not: 'browser'}` condition |
-| 2 | `resolve.fallback: { fs: false }` (client-side) | Resolve | `turbopack.resolveAlias: { fs: { browser: false } }` |
-| 3 | `null-loader` for 7 packages (client-side) | Loader | `turbopack.resolveAlias` with `{ browser: '' }` or empty module |
-| 4 | `source-map-loader` (dev, non-node_modules) | Loader | Built-in Turbopack source map support |
-| 5 | `I18NextHMRPlugin` (dev, client-side) | Plugin | Drop or replace — Next.js Fast Refresh may cover HMR needs |
-| 6 | `ChunkModuleStatsPlugin` (dev, client-side) | Plugin | Drop or build alternative analysis tooling |
-| 7 | `transpilePackages` (70+ ESM packages) | Config | Supported natively by Turbopack |
-| 8 | `optimizePackageImports` (11 @growi/* packages) | Config | Supported natively by Turbopack |
-
-## Requirements
-
-### Requirement 1: Turbopack Activation for Dev Server
-
-**Objective:** As a developer, I want the Next.js dev server to use Turbopack instead of webpack, so that HMR and page compilation are significantly faster.
-
-#### Acceptance Criteria
-
-1. When the dev server starts, the Next.js build system shall use Turbopack as the bundler (remove `webpack: true` from `next()` call).
-2. The Next.js build system shall accept Turbopack configuration via `turbopack` key in `next.config.ts`.
-3. While the dev server is running with Turbopack, the Next.js build system shall provide Fast Refresh functionality equivalent to the current webpack-based HMR.
-4. When a `.page.tsx` file is modified, the Next.js build system shall apply the change via Fast Refresh within noticeably faster time compared to the current webpack compilation.
-
-### Requirement 2: Server-Only Package Exclusion from Client Bundle
-
-**Objective:** As a developer, I want server-only packages (mongoose, dtrace-provider, bunyan, etc.) excluded from the client bundle, so that the client bundle remains lean and free of Node.js-specific dependencies.
-
-#### Acceptance Criteria
-
-1. The Turbopack configuration shall alias the following packages to empty modules in browser context: `dtrace-provider`, `mongoose`, `mathjax-full`, `i18next-fs-backend`, `bunyan`, `bunyan-format`, `core-js`.
-2. The Turbopack configuration shall resolve `fs` to `false` in browser context to prevent "Module not found: Can't resolve 'fs'" errors.
-3. When a client-side page is rendered, the Next.js build system shall not include any of the excluded server-only packages in the client JavaScript output.
-4. If a new server-only package is accidentally imported from client code, the Next.js build system shall either fail the build or exclude it via the configured aliases.
-
-### Requirement 3: SuperJSON SSR Loader Migration
-
-**Objective:** As a developer, I want the SuperJSON auto-wrapping of `getServerSideProps` to work under Turbopack, so that SSR data serialization continues to function transparently.
-
-#### Acceptance Criteria
-
-1. The Turbopack configuration shall register `superjson-ssr-loader` as a custom loader for `*.page.ts` and `*.page.tsx` files on the server side.
-2. When a `.page.tsx` file exports `getServerSideProps`, the build system shall auto-wrap it with `withSuperJSONProps` during compilation.
-3. The SuperJSON serialization/deserialization shall produce identical output for all existing pages compared to the current webpack-based build.
-4. If the `superjson-ssr-loader` is incompatible with Turbopack's loader-runner subset, the build system shall provide an alternative mechanism (e.g., Babel plugin, SWC plugin, or code generation) that achieves the same transformation.
-
-### Requirement 4: ESM Package Transpilation Compatibility
-
-**Objective:** As a developer, I want all 70+ ESM packages currently listed in `transpilePackages` to work correctly under Turbopack, so that no `ERR_REQUIRE_ESM` errors occur.
-
-#### Acceptance Criteria
-
-1. The Next.js build system shall handle all packages listed in `transpilePackages` without `ERR_REQUIRE_ESM` errors under Turbopack.
-2. When a page importing remark/rehype/micromark ecosystem packages is compiled, the Next.js build system shall bundle them correctly.
-3. If Turbopack natively resolves ESM packages without explicit `transpilePackages` configuration, the build system shall still produce correct output for all affected packages.
-
-### Requirement 5: i18n HMR Behavior
-
-**Objective:** As a developer, I want translation file changes to be reflected in the dev browser without a full page reload, so that i18n development workflow remains productive.
-
-#### Acceptance Criteria
-
-1. While the dev server is running, when a translation JSON file under `public/static/locales/` is modified, the Next.js build system shall reflect the change in the browser.
-2. If the current `I18NextHMRPlugin` is incompatible with Turbopack, the build system shall provide an alternative mechanism for i18n hot reloading or document a manual-reload workflow as an acceptable tradeoff.
-3. The i18n integration (`next-i18next` configuration) shall function correctly under Turbopack without runtime errors.
-
-### Requirement 6: Production Build Compatibility
-
-**Objective:** As a developer, I want the production build (`next build`) to continue working correctly, so that deployment is not disrupted by the Turbopack migration.
-
-#### Acceptance Criteria
-
-1. When `pnpm run build:client` is executed, the Next.js build system shall produce a working production bundle.
-2. The production build shall either use Turbopack (if stable for production) or fall back to webpack (`next build --webpack`) without configuration conflicts.
-3. The production build output shall pass all existing integration and E2E tests.
-4. If Turbopack production builds are not yet stable, the build system shall maintain `--webpack` flag for production while using Turbopack for development only.
-
-### Requirement 7: Dev Module Analysis Tooling
-
-**Objective:** As a developer, I want visibility into module counts and chunk composition during dev builds, so that I can continue optimizing bundle size.
-
-#### Acceptance Criteria
-
-1. If `ChunkModuleStatsPlugin` is incompatible with Turbopack, the build system shall provide an alternative mechanism for analyzing initial vs async module counts during dev compilation.
-2. When `DUMP_INITIAL_MODULES=1` is set, the analysis tooling shall output module breakdown reports comparable to the current `initial-modules-analysis.md` format.
-3. If no Turbopack-compatible analysis tooling is feasible, this requirement may be deferred and documented as a known limitation.
-
-### Requirement 8: Global CSS Import Restriction Compliance
-
-**Objective:** As a developer, I want third-party CSS files to be properly handled for Turbopack, so that no "Global CSS cannot be imported from files other than your Custom `<App>`" errors occur.
-
-#### Background
-
-Turbopack strictly enforces the Pages Router rule that global CSS can only be imported from `_app.page.tsx`. Under webpack, this rule was not enforced — components could freely `import 'package/style.css'`. Turbopack rejects these imports at compile time.
-
-The solution uses a **two-track vendor CSS system**:
-- **Commons track** (`vendor.scss` → `src/styles/prebuilt/`): Globally shared vendor CSS (e.g., `simplebar-react`) compiled via `vite.vendor-styles-commons.ts`
-- **Components track** (`*.vendor-styles.ts` → `*.vendor-styles.prebuilt.js`): Component-specific vendor CSS precompiled via `vite.vendor-styles-components.ts` using Vite's `?inline` CSS import suffix
-
-#### Acceptance Criteria
-
-1. When a component requires third-party CSS (e.g., `handsontable`, `katex`, `diff2html`), the CSS shall be precompiled into a `.vendor-styles.prebuilt.js` file via Vite and imported as a regular JS module.
-2. All existing direct global CSS imports from non-`_app` files shall be migrated to either the commons track (if globally needed) or the components track (if component-specific).
-3. The vendor-styles entry point files shall follow the naming convention `{ComponentName}.vendor-styles.ts`, producing `{ComponentName}.vendor-styles.prebuilt.js` output.
-4. The prebuilt output files shall be git-ignored and regenerated by Turborepo `pre:styles-components` / `dev:pre:styles-components` tasks before build/dev.
-5. Vendor CSS precompilation shall use Vite's `?inline` suffix to inline CSS as a string, injecting it at runtime via `<style>` tag insertion into `document.head`.
-
-### Requirement 9: CSS Modules `:global` Syntax Compatibility
-
-**Objective:** As a developer, I want all CSS Module files to use Turbopack-compatible `:global` syntax, so that no "Ambiguous CSS module class not supported" errors occur.
-
-#### Background
-
-Turbopack's CSS Modules implementation does not support the block form of `:global { ... }` — it only supports the function form `:global(...)`. The GROWI codebase uses the block form extensively (128 files, 255 occurrences). This is a mechanical syntax difference:
-
-| Pattern (webpack) | Equivalent (Turbopack) |
-|---|---|
-| `.parent :global { .child { } }` | `.parent { :global(.child) { } }` |
-| `&:global { &.modifier { } }` | `&:global(.modifier) { }` |
-| `:global { .class { } }` (standalone) | `:global(.class) { }` |
-
-#### Acceptance Criteria
-
-1. All `.module.scss` and `.module.css` files shall use the function form `:global(...)` instead of the block form `:global { ... }`.
-2. The conversion shall be mechanical and preserve the exact same CSS output (class name scoping behavior unchanged).
-3. Nested `:global` blocks (e.g., `.parent :global { .child { .grandchild { } } }`) shall be converted to nested `:global(...)` selectors.
-4. When the dev server starts with Turbopack, no "Ambiguous CSS module class" errors shall appear.
-5. When the dev server starts with webpack (`USE_WEBPACK=1`), the converted syntax shall produce identical behavior (webpack supports both block and function forms).
-
-### Requirement 10: Incremental Migration Path
-
-**Objective:** As a developer, I want the ability to switch between Turbopack and webpack during the migration period, so that I can fall back to webpack if Turbopack issues are discovered.
-
-#### Acceptance Criteria
-
-1. The Next.js build system shall support switching between Turbopack and webpack via an environment variable or CLI flag (e.g., `--webpack`).
-2. When webpack mode is selected, all existing webpack customizations shall remain functional without modification.
-3. The `next.config.ts` shall maintain both `webpack()` hook and `turbopack` configuration simultaneously during the migration period.
-4. When the migration is complete and verified, the build system shall remove the webpack fallback configuration in a follow-up cleanup (Phase 3).
-5. The converted CSS Modules syntax (function form `:global(...)`) shall work identically under both Turbopack and webpack modes.
-

+ 0 - 195
.kiro/specs/migrate-to-turbopack/research.md

@@ -1,195 +0,0 @@
-# Research & Design Decisions
-
-## Summary
-- **Feature**: `migrate-to-turbopack`
-- **Discovery Scope**: Complex Integration
-- **Key Findings**:
-  - Next.js 16 `next()` programmatic API officially supports `turbopack: true` for custom servers — the primary blocker is resolved
-  - Turbopack `rules` with `condition: { not: 'browser' }` enables server-only loaders, making `superjson-ssr-loader` migration feasible
-  - Turbopack is stable for both dev and production builds in Next.js 16, meaning full migration (not dev-only) is possible
-  - `i18next-hmr` webpack plugin has no Turbopack equivalent; i18n HMR requires alternative approach or manual reload tradeoff
-  - `resolveAlias` with `{ browser: ... }` conditional aliasing replaces both `resolve.fallback` and `null-loader` patterns
-
-## Research Log
-
-### Custom Server + Turbopack Compatibility
-- **Context**: GROWI uses Express custom server calling `next({ dev, webpack: true })`. Need to confirm Turbopack works with programmatic API.
-- **Sources Consulted**:
-  - [Next.js Custom Server Guide](https://nextjs.org/docs/app/guides/custom-server) (v16.1.6, 2026-02-27)
-  - [GitHub Discussion #49325](https://github.com/vercel/next.js/discussions/49325)
-  - [GitHub Issue #65479](https://github.com/vercel/next.js/issues/65479)
-- **Findings**:
-  - The `next()` function in Next.js 16 accepts: `turbopack: boolean` (enabled by default) and `webpack: boolean`
-  - GROWI's current `next({ dev, webpack: true })` explicitly opts out of Turbopack
-  - Switching to `next({ dev })` or `next({ dev, turbopack: true })` enables Turbopack with custom server
-  - No `TURBOPACK=1` env var hack needed — official API parameter available
-- **Implications**: The custom server is NOT a blocker. Migration focus shifts entirely to webpack config equivalents.
-
-### Turbopack Loader System (turbopack.rules)
-- **Context**: `superjson-ssr-loader` must run server-side only on `.page.{ts,tsx}` files.
-- **Sources Consulted**:
-  - [Turbopack Config Docs](https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack) (v16.1.6)
-  - [GitHub Discussion #63150](https://github.com/vercel/next.js/discussions/63150) — server/client conditional loaders
-- **Findings**:
-  - `turbopack.rules` supports glob-based file matching with `condition` for environment targeting
-  - Server-only condition: `condition: { not: 'browser' }`
-  - Browser-only condition: `condition: 'browser'`
-  - Advanced conditions available: `all`, `any`, `not`, `path` (glob/RegExp), `content` (RegExp), `foreign`, `development`, `production`
-  - Loaders must return JavaScript code (our superjson-ssr-loader already does)
-  - Missing loader APIs: `importModule`, `loadModule`, `emitFile`, `this.mode`, `this.target`, `this.resolve`
-  - `superjson-ssr-loader` uses only `source` parameter (simple string transform) — compatible with loader-runner subset
-- **Implications**: superjson-ssr-loader migration is straightforward via `turbopack.rules` with server condition.
-
-### resolveAlias for Package Exclusion (null-loader replacement)
-- **Context**: 7 packages excluded from client bundle via null-loader, plus fs fallback.
-- **Sources Consulted**:
-  - [Turbopack Config Docs](https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack)
-  - [Turbopack Resolve Fallback Forum](https://nextjs-forum.com/post/1189694920328487023)
-  - [GitHub Issue #88540](https://github.com/vercel/next.js/issues/88540) — resolveAlias transitive dependency issues
-- **Findings**:
-  - `turbopack.resolveAlias` supports conditional aliasing: `{ browser: './empty-module.js' }`
-  - For `fs`: `resolveAlias: { fs: { browser: './src/lib/empty.ts' } }` with an empty file
-  - For null-loader replacements: alias each package to an empty module in browser context
-  - Known issue: resolveAlias may not resolve transitive dependencies correctly (GitHub #88540), but our null-loader targets are direct imports not transitive
-  - Regex-based test patterns (e.g., `/\/bunyan\//`) need conversion to package-name aliases
-- **Implications**: Direct 1:1 mapping possible. Need an `empty.ts` module (`export default {}` or empty file). Regex patterns convert to package name strings.
-
-### I18NextHMRPlugin and i18n HMR
-- **Context**: `I18NextHMRPlugin` is a webpack plugin providing HMR for translation JSON files. Turbopack has no plugin API.
-- **Sources Consulted**:
-  - [i18next-hmr npm](https://www.npmjs.com/package/i18next-hmr)
-  - [GitHub Issue #2113](https://github.com/i18next/next-i18next/issues/2113) — Turbopack support
-  - [i18next-hmr GitHub](https://github.com/felixmosh/i18next-hmr)
-- **Findings**:
-  - `i18next-hmr` provides: (1) `I18NextHMRPlugin` webpack plugin for client, (2) `HMRPlugin` i18next plugin for server and client
-  - The webpack plugin watches locale files and triggers HMR updates — no Turbopack equivalent exists
-  - next-i18next + Turbopack compatibility status is unclear/problematic (issue #2113 closed as stale)
-  - `next-i18next` core functionality (i18n routing, SSR) should work independently of bundler, since it uses Next.js i18n config
-  - The HMR plugin is dev-only convenience — not required for functionality
-- **Implications**:
-  - Drop `I18NextHMRPlugin` webpack plugin when using Turbopack
-  - Also drop `HMRPlugin` from `next-i18next.config.js` `use` array when Turbopack is active
-  - Translation changes require manual browser refresh in Turbopack dev mode
-  - Acceptable tradeoff: Turbopack's overall faster compilation outweighs i18n HMR loss
-
-### Turbopack Production Build Status
-- **Context**: Need to determine if production builds can also use Turbopack.
-- **Sources Consulted**:
-  - [Next.js 16 Blog](https://nextjs.org/blog/next-16)
-  - [Turbopack Stable Announcement](https://nextjs.org/blog/turbopack-for-development-stable)
-  - [Progosling: Turbopack Default](https://progosling.com/en/dev-digest/2026-02/nextjs-16-turbopack-default)
-- **Findings**:
-  - Turbopack is stable for both `next dev` and `next build` in Next.js 16
-  - 50%+ dev sessions and 20%+ production builds already on Turbopack (Next.js 15.3+ stats)
-  - `next build` with custom webpack config will fail by default — must use `--webpack` flag or migrate config
-  - Both Turbopack config (`turbopack` key) and webpack config (`webpack()` hook) can coexist in next.config.ts
-- **Implications**: Full migration (dev + build) is feasible. Incremental approach: dev first, then production build.
-
-### ChunkModuleStatsPlugin Replacement
-- **Context**: Custom webpack plugin logging initial/async module counts. Turbopack has no plugin API.
-- **Sources Consulted**: Turbopack API docs, no third-party analysis tools found for Turbopack
-- **Findings**:
-  - Turbopack exposes no compilation hooks or chunk graph API
-  - No equivalent plugin mechanism exists
-  - `@next/bundle-analyzer` may work with Turbopack production builds (uses webpack-bundle-analyzer under the hood — needs verification)
-  - Alternative: Use Turbopack's built-in trace/debug features or browser DevTools for analysis
-- **Implications**: Defer module analysis tooling. Accept this as a temporary limitation. Existing webpack mode can be used for detailed analysis when needed.
-
-### ESM transpilePackages under Turbopack
-- **Context**: 70+ packages in `transpilePackages` for ESM compatibility.
-- **Sources Consulted**: Turbopack docs, Next.js 16 upgrade guide
-- **Findings**:
-  - `transpilePackages` is supported by both webpack and Turbopack in Next.js
-  - Turbopack handles ESM natively with better resolution than webpack
-  - Some packages in the list may not need explicit transpilation under Turbopack
-  - `optimizePackageImports` (experimental) is also supported under Turbopack
-- **Implications**: Keep `transpilePackages` config as-is initially. Test removing entries incrementally after migration verified.
-
-### Vendor CSS Handling under Turbopack Pages Router
-- **Context**: Turbopack Pages Router strictly enforces that global CSS can only be imported from `_app.page.tsx`. Components importing vendor CSS (e.g., `import 'katex/dist/katex.min.css'`) fail to compile. Need a strategy that avoids centralizing all vendor CSS in `_app` (which would degrade FCP for pages that don't need those styles).
-- **Sources Consulted**:
-  - [Next.js CSS Modules Docs](https://nextjs.org/docs/app/getting-started/css#css-modules)
-  - [Vite CSS ?inline Suffix](https://vite.dev/guide/features.html#disabling-css-injection-into-the-page)
-  - Implementation experiments in this repository
-- **Approaches Evaluated**:
-  1. **Centralize in `_app.page.tsx`** — Move all vendor CSS imports to `_app`. Simple but degrades FCP for pages that don't need those styles.
-  2. **CSS Module wrappers** (`:global { @import '...' }`) — Create `vendor-*.module.scss` files wrapping vendor CSS in `:global {}`. Failed because Turbopack rejects `:global` block form entirely.
-  3. **Vite precompilation with `?inline`** — Create `*.vendor-styles.ts` entry points that import CSS via Vite's `?inline` suffix (inlines CSS as a string), then inject into `document.head` via `<style>` tags at runtime. Components import the prebuilt `.js` output instead of raw CSS.
-- **Findings**:
-  - Approach 2 failed: Turbopack does not support `:global` block form in CSS Modules — not just for selectors but also for `@import` wrappers
-  - Approach 3 works: Vite `?inline` converts CSS to a JS string export. The prebuilt JS file contains no CSS imports, so Turbopack sees it as a regular JS module
-  - `handsontable/dist/handsontable.full.min.css` contains IE CSS hacks (`*zoom:1`, `filter:alpha()`) that Turbopack's CSS parser (lightningcss) cannot parse. Switched to `handsontable/dist/handsontable.css` (non-full, non-minified variant)
-  - Vite's `?inline` approach runs at prebuild time (before Turbopack/Next.js), fitting naturally into the existing Turborepo task pipeline alongside `pre:styles-commons`
-  - SSR caveat: CSS injected via `<style>` tags is not available during SSR. Most consuming components already use `next/dynamic` with `ssr: false`, so FOUC is not a practical concern
-- **Implications**: Selected Approach 3. Two-track vendor CSS system: commons track (`vendor.scss` for globally shared CSS like `simplebar-react`) and components track (`*.vendor-styles.ts` for component-specific CSS precompiled by Vite).
-
-## Architecture Pattern Evaluation
-
-| Option | Description | Strengths | Risks / Limitations | Notes |
-|--------|-------------|-----------|---------------------|-------|
-| Dev-Only Migration | Use Turbopack for dev, keep webpack for build | Low risk, immediate DX gain | Dual config maintenance | Recommended as Phase 1 |
-| Full Migration | Use Turbopack for both dev and build | Single config, simpler maintenance | Higher risk, production impact | Target as Phase 2 |
-| Feature-Flag Approach | Environment variable toggles bundler choice | Maximum flexibility, easy rollback | Complexity in config | Use during transition |
-
-## Design Decisions
-
-### Decision: Dual-Config with Phased Migration
-- **Context**: Need to migrate 6 webpack customizations while maintaining stability
-- **Alternatives Considered**:
-  1. Big-bang migration — convert everything at once, remove webpack config
-  2. Dev-only first — Turbopack for dev, webpack for build
-  3. Feature-flag approach — `USE_WEBPACK=1` toggles between bundlers
-- **Selected Approach**: Option 3 (feature-flag) as implementation vehicle, with Option 2 as the initial target state
-- **Rationale**: Allows any developer to fall back to webpack instantly if Turbopack issues arise. The flag approach naturally supports phased rollout.
-- **Trade-offs**: Slightly more complex next.config.ts during transition. Both configs must be maintained until webpack is fully removed.
-- **Follow-up**: After verification period, remove webpack config and flag in a cleanup task.
-
-### Decision: Drop i18n HMR Plugin
-- **Context**: `I18NextHMRPlugin` is webpack-only. No Turbopack equivalent.
-- **Alternatives Considered**:
-  1. Keep webpack for dev to preserve i18n HMR
-  2. Drop i18n HMR, accept manual refresh for translation changes
-  3. Investigate custom Turbopack-compatible i18n HMR solution
-- **Selected Approach**: Option 2 — drop i18n HMR
-- **Rationale**: The performance gain from Turbopack (5-10x faster Fast Refresh) far outweighs the loss of i18n-specific HMR. Translation editing is a small fraction of dev time.
-- **Trade-offs**: Translation file changes require manual browser refresh. Overall dev experience still dramatically improved.
-- **Follow-up**: Monitor if `i18next-hmr` adds Turbopack support in the future.
-
-### Decision: Empty Module File for resolveAlias
-- **Context**: null-loader replaces modules with empty exports. Turbopack resolveAlias needs an actual file path.
-- **Alternatives Considered**:
-  1. Create `src/lib/empty-module.ts` with `export default {}`
-  2. Use `false` value in resolveAlias (like webpack resolve.fallback)
-  3. Use inline empty string path
-- **Selected Approach**: Option 1 — create a dedicated empty module file
-- **Rationale**: Explicit, documented, easy to understand. Works reliably with conditional browser aliasing.
-- **Trade-offs**: One extra file in the codebase.
-- **Follow-up**: Verify all 7 null-loader targets work correctly with the alias approach.
-
-### Decision: Vite Precompilation for Vendor CSS
-- **Context**: Turbopack Pages Router rejects global CSS imports outside `_app.page.tsx`. Need per-component vendor CSS without centralizing everything in `_app`.
-- **Alternatives Considered**:
-  1. Centralize all vendor CSS in `_app.page.tsx` — simple but degrades FCP
-  2. CSS Module wrappers with `:global { @import }` — Turbopack rejects `:global` block form
-  3. Vite precompilation with `?inline` suffix — CSS inlined into JS at prebuild time
-- **Selected Approach**: Option 3 — Vite precompilation
-- **Rationale**: Keeps CSS co-located with consuming components (no FCP penalty for unrelated pages). Fits naturally into the existing Turborepo prebuild pipeline (parallel to `pre:styles-commons`). No Turbopack restrictions apply since the output is pure JS.
-- **Trade-offs**: Additional prebuild step (fast in practice). Runtime CSS injection means styles are not available during SSR (acceptable since most consuming components use `ssr: false`).
-- **Naming Convention**: `{ComponentName}.vendor-styles.ts` → `{ComponentName}.vendor-styles.prebuilt.js`
-
-## Risks & Mitigations
-- **Risk 1**: next-i18next may have runtime issues under Turbopack (not just HMR) — **Mitigation**: Test i18n routing and SSR early; maintain webpack fallback flag
-- **Risk 2**: superjson-ssr-loader may use unsupported loader-runner API features — **Mitigation**: The loader only performs regex-based string transforms on the `source` argument; no advanced APIs used. If issues arise, convert to a Babel plugin or code generation.
-- **Risk 3**: resolveAlias may not handle transitive dependencies from null-loaded packages — **Mitigation**: Current null-loader targets are matched by regex on file paths; resolveAlias uses package names. Test each package individually.
-- **Risk 4**: ESM packages may behave differently under Turbopack resolution — **Mitigation**: Keep `transpilePackages` list unchanged initially; test pages using remark/rehype ecosystem.
-- **Risk 5**: Production build regression — **Mitigation**: Phase 1 keeps webpack for production; Phase 2 migrates production only after dev is verified stable.
-
-## References
-- [Next.js Custom Server Guide](https://nextjs.org/docs/app/guides/custom-server) — confirms `turbopack` option in `next()` API
-- [Next.js Turbopack Config](https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack) — rules, resolveAlias, conditions
-- [Next.js 16 Release Blog](https://nextjs.org/blog/next-16) — Turbopack stable for dev and build
-- [Next.js Upgrade Guide v16](https://nextjs.org/docs/app/guides/upgrading/version-16) — migration steps
-- [GitHub Discussion #63150](https://github.com/vercel/next.js/discussions/63150) — server/client conditional loaders
-- [GitHub Discussion #49325](https://github.com/vercel/next.js/discussions/49325) — custom server + Turbopack
-- [i18next-hmr](https://github.com/felixmosh/i18next-hmr) — webpack/vite only, no Turbopack support
-- [Turbopack Loader API Limitations](https://nextjs.org/docs/app/api-reference/turbopack) — missing features documented

+ 0 - 22
.kiro/specs/migrate-to-turbopack/spec.json

@@ -1,22 +0,0 @@
-{
-  "feature_name": "migrate-to-turbopack",
-  "created_at": "2026-03-04T00:00:00Z",
-  "updated_at": "2026-03-04T14:00:00Z",
-  "language": "en",
-  "phase": "tasks-generated",
-  "approvals": {
-    "requirements": {
-      "generated": true,
-      "approved": true
-    },
-    "design": {
-      "generated": true,
-      "approved": true
-    },
-    "tasks": {
-      "generated": true,
-      "approved": true
-    }
-  },
-  "ready_for_implementation": true
-}

+ 0 - 136
.kiro/specs/migrate-to-turbopack/tasks.md

@@ -1,136 +0,0 @@
-# Implementation Plan
-
-## Phase 1: Dev Server Turbopack Migration
-
-- [x] 1. Create empty module and Turbopack configuration foundation
-- [x] 1.1 (P) Create the empty module file used as the alias target for excluded server-only packages
-  - Create a minimal TypeScript module at the designated location that exports an empty default and named export
-  - The module satisfies any import style (default, named, namespace) so that aliased packages resolve without errors
-  - _Requirements: 2.1, 2.2_
-
-- [x] 1.2 (P) Add the Turbopack configuration block to the Next.js config with resolve aliases for server-only package exclusion
-  - Add a `turbopack` key to the Next.js config object containing `resolveAlias` entries
-  - Alias `fs` to the empty module in browser context to prevent "Module not found" errors on the client
-  - Alias all 7 server-only packages (`dtrace-provider`, `mongoose`, `mathjax-full`, `i18next-fs-backend`, `bunyan`, `bunyan-format`, `core-js`) to the empty module in browser context
-  - Use the conditional `{ browser: '...' }` syntax for each alias so that server-side resolution remains unaffected
-  - Verify that the `bunyan` alias does not interfere with `browser-bunyan` (different package name, no collision expected)
-  - Keep the existing `webpack()` hook untouched — both configs coexist
-  - _Requirements: 1.2, 2.1, 2.2, 2.3, 8.3_
-
-- [x] 1.3 Add the superjson-ssr-loader as a Turbopack custom loader rule for server-side page files
-  - Register the existing `superjson-ssr-loader` under `turbopack.rules` for `*.page.ts` and `*.page.tsx` file patterns
-  - Apply the `condition: { not: 'browser' }` condition so the loader runs only on the server side
-  - Set the `as` output type to `*.ts` / `*.tsx` respectively so Turbopack continues processing the transformed output
-  - The loader performs a simple regex-based source transform and returns JavaScript — no unsupported loader-runner APIs are used
-  - _Requirements: 1.2, 3.1, 3.2, 3.3, 3.4_
-
-- [x] 2. Update server initialization to toggle between Turbopack and webpack
-- [x] 2.1 Replace the hardcoded `webpack: true` in the custom server with environment-variable-based bundler selection
-  - Read the `USE_WEBPACK` environment variable at server startup
-  - When `USE_WEBPACK` is set (truthy), pass `{ dev, webpack: true }` to preserve the existing webpack pipeline
-  - When `USE_WEBPACK` is not set (default), omit the `webpack` option so Turbopack activates as the Next.js 16 default
-  - Preserve the existing ts-node hook save/restore logic surrounding `app.prepare()`
-  - _Requirements: 1.1, 1.3, 1.4, 8.1_
-
-- [x] 3. Guard i18n HMR plugin loading for Turbopack compatibility
-- [x] 3.1 Conditionally skip the i18next-hmr plugin in the i18n configuration when Turbopack is active
-  - In the next-i18next configuration file, check the `USE_WEBPACK` environment variable before loading the `HMRPlugin`
-  - When `USE_WEBPACK` is not set (Turbopack mode), exclude `HMRPlugin` from the `use` array to prevent webpack-internal references from crashing
-  - When `USE_WEBPACK` is set (webpack mode), preserve the existing `HMRPlugin` behavior for both server and client
-  - The `I18NextHMRPlugin` in the webpack hook of next.config.ts requires no change — it only executes when webpack is active
-  - Translation file changes under Turbopack require a manual browser refresh (documented tradeoff)
-  - _Requirements: 5.1, 5.2, 5.3_
-
-- [x] 4. Precompile vendor CSS via Vite for Turbopack compatibility
-- [x] 4.1 Create Vite config and Turborepo tasks for vendor CSS precompilation
-  - Created `vite.vendor-styles-components.ts` — collects all `src/**/*.vendor-styles.ts` as entry points, precompiles via Vite `?inline` suffix
-  - Includes `moveAssetsToPublic` plugin: moves Vite-emitted font files from `src/assets/` to `public/static/fonts/` and rewrites URL references in prebuilt JS (`/assets/` → `/static/fonts/`)
-  - Fonts served at `/static/fonts/*` via existing `express.static(crowi.publicDir)` — no additional Express route needed
-  - Added `pre:styles-components` / `dev:pre:styles-components` tasks to `turbo.json` as dependencies of `build` and `dev`
-  - Added corresponding npm scripts to `package.json`
-  - Added `/src/**/*.vendor-styles.prebuilt.js` and `/public/static/fonts` to `.gitignore`
-  - _Requirements: 8.3, 8.4, 8.5_
-
-- [x] 4.2 Create vendor-styles entry points and migrate CSS imports from components
-  - Created 8 `*.vendor-styles.ts` entry point files covering 13 vendor CSS imports from 12 component files
-  - Each entry point uses `?inline` CSS import and injects into `document.head` via `<style>` tag
-  - Replaced direct CSS imports in components with `.vendor-styles.prebuilt` JS imports
-  - Switched `handsontable/dist/handsontable.full.min.css` to `handsontable/dist/handsontable.css` (non-full, non-minified) to avoid IE CSS hack parse errors in Turbopack
-  - `simplebar-react` CSS handled by commons track (`vendor.scss`) — direct import simply removed from `Sidebar.tsx`
-  - `katex` CSS added to `Renderer.vendor-styles.ts` (used by `rehype-katex` in the renderer)
-  - _Requirements: 8.1, 8.2, 8.3_
-
-- [x] 5. Convert `:global` block form to function form in CSS Modules
-- [x] 5.1 (P) Convert all `:global` block form syntax to function form across all `.module.scss` files
-  - Scan all `.module.scss` files for `:global {` block form usage (128 files, 255 occurrences)
-  - Convert each occurrence to the function form `:global(...)` following the 6 conversion patterns documented in design.md
-  - Preserve nested selector structure and any `&` parent selectors
-  - Exclude `vendor-*.module.scss` files (these use `:global { @import }` which is a different pattern)
-  - _Requirements: 9.1, 9.2, 9.3_
-
-- [x] 5.2 Verify CSS output equivalence after conversion
-  - Run stylelint across all converted files
-  - Run the existing vitest test suite to confirm no regressions
-  - Start dev server with Turbopack and verify no "Ambiguous CSS module class" errors
-  - Start dev server with `USE_WEBPACK=1` and verify identical behavior
-  - _Requirements: 9.4, 9.5_
-
-- [x] 6. Smoke test the Turbopack dev server and webpack fallback
-- [x] 6.1 Verify the dev server starts and pages load correctly under Turbopack
-  - Dev server starts and root page compiles + renders (HTTP 200) with no CSS errors
-  - Turbopack production build (`next build`) compiles all routes successfully with 0 errors
-  - Fixed `MessageCard.module.scss` — removed standalone `&:local` (Turbopack doesn't support it)
-  - Fixed `DefaultContentSkelton.module.scss` — replaced `@extend .grw-skeleton-text` with shared selector group
-  - Fixed `handsontable` CSS — switched to non-full, non-minified variant to avoid IE CSS hack parse errors
-  - _Requirements: 1.1, 1.3, 1.4, 2.3, 3.2, 3.3, 4.1, 4.2, 5.3, 9.4_
-
-- [x] 6.2 Verify the webpack fallback mode works identically to the pre-migration state
-  - Added `USE_WEBPACK` env var support in `crowi/index.ts` — when set, passes `{ webpack: true }` to `next()` API
-  - Webpack production build (`next build --webpack`) compiles all 40 routes successfully
-  - Turbopack production build (`next build`) compiles all routes successfully (20.6s vs webpack 41s)
-  - `I18NextHMRPlugin` is inside the `webpack()` hook which only runs when webpack is active — no additional guard needed
-  - `ChunkModuleStatsPlugin` is inside the `webpack()` hook which only runs when webpack is active
-  - All 1385 tests pass with no regressions
-  - _Requirements: 9.5, 10.1, 10.2, 10.3_
-
-- [x] 6.3 Run existing automated tests and lint checks
-  - Run the vitest test suite to confirm no test regressions
-  - Run lint checks (typecheck, biome, stylelint) to confirm no new errors
-  - Run the production build with `--webpack` to confirm it still works
-  - _Requirements: 6.1, 6.2, 6.3_
-
-## Phase 2: Production Build Migration (Deferred)
-
-- [x] 7. Migrate production build from webpack to Turbopack
-- [x] 7.1 Remove the `--webpack` flag from the production client build script
-  - Updated `build:client` script from `next build --webpack` to `next build` (Turbopack default)
-  - Production build completes successfully with all routes compiled
-  - _Requirements: 6.1, 6.2, 6.3_
-
-## Phase 3: Cleanup
-
-- [x] 8. Remove webpack fallback configuration and deprecated plugins
-- [x] 8.1 Remove the `webpack()` hook, `USE_WEBPACK` env var check, and deprecated plugin code
-  - Removed the entire `webpack()` hook function from `next.config.ts`
-  - Removed `USE_WEBPACK` conditional from `crowi/index.ts` — now always uses Turbopack (`next({ dev })`)
-  - Removed `I18NextHMRPlugin` usage (was inside the removed `webpack()` hook)
-  - Removed `ChunkModuleStatsPlugin` and its helper code from `next.config.utils.ts`
-  - Removed unused `listScopedPackages` export from `next.config.utils.ts`
-  - Removed unused imports (`createChunkModuleStatsPlugin`, `localePath`) from `next.config.ts`
-  - Removed devDependencies: `null-loader`, `source-map-loader`, `i18next-hmr`
-  - Turbopack production build compiles all 40 routes successfully; 1385 tests pass
-  - _Requirements: 10.1, 10.2, 10.3_
-
-## Deferred Requirements
-
-- **Requirement 7 (Dev Module Analysis Tooling)**: Requirements 7.1, 7.2 are intentionally deferred. Turbopack has no plugin API for compilation hooks, and no equivalent analysis tooling exists. A Turbopack-native solution may emerge as the ecosystem matures.
-
-## Implementation Notes (Discovered During Phase 1)
-
-- **resolveAlias paths**: Turbopack `resolveAlias` requires **relative paths** (e.g., `./src/lib/empty-module.ts`), not absolute paths from `path.resolve()`. Absolute paths cause "server relative imports are not implemented yet" errors.
-- **Vendor CSS precompiled via Vite**: The `vendor-*.module.scss` wrapper approach (`:global { @import }`) failed because Turbopack rejects `:global` block form entirely. The `_app.page.tsx` centralization approach was also rejected due to FCP degradation. Final solution: Vite precompilation with `?inline` suffix — 8 `*.vendor-styles.ts` entry points covering 13 vendor CSS imports, precompiled into `.vendor-styles.prebuilt.js` files by Turborepo `pre:styles-components` task.
-- **`:global` block form**: Turbopack's CSS Modules implementation only supports the function form `:global(...)`. The block form `:global { }` (supported by webpack) causes "Ambiguous CSS module class not supported" errors. Conversion is mechanical — 128 files, 255 occurrences.
-- **Standalone `:local`**: Turbopack doesn't support standalone `:local` or `&:local` in CSS Modules. Inside `:global(...)` function form, properties are already locally scoped by default, so `&:local` wrappers can simply be removed.
-- **Sass `@extend` in CSS Modules**: `@extend .class` fails when the target is wrapped in `:global(.class)` — Sass doesn't match them as the same selector. Replace with shared selector groups (comma-separated selectors).
-- **handsontable CSS**: `handsontable.full.min.css` contains IE CSS star hacks (`*zoom:1`, `*display:inline`) and `filter:alpha()` that Turbopack's CSS parser (lightningcss) cannot parse. Use `handsontable/dist/handsontable.css` (non-full, non-minified) instead — the "full" variant includes Pikaday which is unused.
-- **Vendor CSS font handling**: When Vite precompiles CSS that references external assets (e.g., KaTeX `@font-face` with `url(fonts/KaTeX_*.woff2)`), it emits asset files to `src/assets/` and rewrites URLs to `/assets/...`. Since `src/assets/` is not served by Express, a `moveAssetsToPublic` Vite plugin was added to relocate fonts to `public/static/fonts/` and rewrite URL references to `/static/fonts/...` in prebuilt JS. This aligns with the existing `public/static/` convention (`/public/static/js`, `/public/static/styles`).

+ 82 - 35
apps/app/.claude/skills/build-optimization/SKILL.md

@@ -1,58 +1,110 @@
 ---
 name: build-optimization
-description: GROWI apps/app webpack configuration, module optimization, and build measurement tooling. Auto-invoked when working in apps/app.
-user-invocable: false
+description: GROWI apps/app Turbopack configuration, module optimization, and build measurement tooling. Auto-invoked when working in apps/app.
+user-invokable: false
 ---
 
 # Build Optimization (apps/app)
 
-## Next.js Version & Bundler Strategy
+## Next.js Version & Bundler
 
-- **Next.js 16** (`^16.0.0`) with **Webpack** bundler (not Turbopack)
-- Turbopack is the default in v16, but GROWI opts out via `--webpack` flag due to custom webpack configuration
-- Build: `next build --webpack`; Dev: Express server calls `next({ dev })` which uses webpack when `webpack()` config exists
+- **Next.js 16** (`^16.0.0`) with **Turbopack** bundler (default)
+- Build: `next build`; Dev: Express server calls `next({ dev })` which uses Turbopack by default
 - React stays at `^18.2.0` — Pages Router has full React 18 support in v16
+- Webpack has been fully removed (no `webpack()` hook, no `--webpack` flag)
 
-## Custom Webpack Configuration
+## Turbopack Configuration
 
-| Component | File | Purpose |
-|-----------|------|---------|
-| **superjson-ssr-loader** | `src/utils/superjson-ssr-loader.js` | Auto-wraps `getServerSideProps` with SuperJSON serialization |
-| **null-loader rules** (7) | `next.config.ts` | Exclude server-only packages from client bundle |
-| **I18NextHMRPlugin** | `next.config.ts` | i18n hot module replacement in dev mode |
-| **ChunkModuleStatsPlugin** | `src/utils/next.config.utils.js` | Dev-time module count analysis (initial/async-only/total) |
-| **source-map-loader** | `next.config.ts` | Source map extraction in dev builds |
+### Custom Loader Rules (`turbopack.rules`)
 
-### null-loader Rules
+| Rule | Pattern | Condition | Purpose |
+|------|---------|-----------|---------|
+| superjson-ssr-loader | `*.page.ts`, `*.page.tsx` | `{ not: 'browser' }` (server-only) | Auto-wraps `getServerSideProps` with SuperJSON serialization |
 
-7 packages excluded from client bundle: `dtrace-provider`, `mongoose`, `mathjax-full`, `i18next-fs-backend`, `bunyan`, `bunyan-format`, `core-js`
+- Loaders are registered in `next.config.ts` under `turbopack.rules`
+- `condition: { not: 'browser' }` restricts the loader to server-side compilation only
+- `as: '*.ts'` / `as: '*.tsx'` tells Turbopack to continue processing the transformed output as TypeScript
 
-**Important**: Any changes to these loaders/plugins must be verified against the module count baseline.
+### Resolve Aliases (`turbopack.resolveAlias`)
+
+7 server-only packages + `fs` are aliased to `./src/lib/empty-module.ts` in browser context:
+
+| Package | Reason |
+|---------|--------|
+| `fs` | Node.js built-in, not available in browser |
+| `dtrace-provider` | Native module, server-only |
+| `mongoose` | MongoDB driver, server-only |
+| `i18next-fs-backend` | File-system i18n loader, server-only |
+| `bunyan` | Server-side logger |
+| `bunyan-format` | Server-side logger formatter |
+| `core-js` | Server-side polyfills |
+
+- Uses conditional `{ browser: './src/lib/empty-module.ts' }` syntax so server-side resolution is unaffected
+- `resolveAlias` requires **relative paths** (e.g., `./src/lib/empty-module.ts`), not absolute paths — absolute paths cause "server relative imports are not implemented yet" errors
+- If a new server-only package leaks into the client bundle, add it to `resolveAlias` with the same pattern
 
 ## SuperJSON Serialization Architecture
 
-The `next-superjson` SWC plugin was replaced by a custom webpack loader:
+The `next-superjson` SWC plugin was replaced by a custom loader:
 
-- **Build time**: `superjson-ssr-loader.js` auto-wraps `getServerSideProps` in `.page.{ts,tsx}` files with `withSuperJSONProps()`
+- **Build time**: `superjson-ssr-loader.ts` auto-wraps `getServerSideProps` in `.page.{ts,tsx}` files with `withSuperJSONProps()` via Turbopack `rules`
 - **Runtime (server)**: `withSuperJSONProps()` in `src/pages/utils/superjson-ssr.ts` serializes props via superjson
 - **Runtime (client)**: `_app.page.tsx` calls `deserializeSuperJSONProps()` for centralized deserialization
 - **No per-page changes needed** — new pages automatically get superjson serialization
 - Custom serializers registered in `_app.page.tsx` (ObjectId, PageRevisionWithMeta)
 
+## CSS Modules Turbopack Compatibility
+
+### `:global` Syntax
+
+Turbopack only supports the **function form** `:global(...)`. The block form `:global { ... }` is NOT supported:
+
+```scss
+// WRONG — Turbopack rejects this
+.parent :global {
+  .child { color: red; }
+}
+
+// CORRECT — function form
+.parent {
+  :global(.child) { color: red; }
+}
+```
+
+Nested blocks must also use function form:
+
+```scss
+// WRONG
+.parent :global {
+  .child {
+    .grandchild { }
+  }
+}
+
+// CORRECT
+.parent {
+  :global(.child) {
+    :global(.grandchild) { }
+  }
+}
+```
+
+### Other Turbopack CSS Restrictions
+
+- **Standalone `:local` / `&:local`**: Not supported. Inside `:global(...)`, properties are locally scoped by default — remove `&:local` wrappers
+- **`@extend` with `:global()`**: `@extend .class` fails when target is wrapped in `:global(.class)` — Sass doesn't match them as the same selector. Use shared selector groups (comma-separated selectors) instead
+- **IE CSS hacks**: `*zoom:1`, `*display:inline`, `filter:alpha()` cannot be parsed by Turbopack's CSS parser (lightningcss). Avoid CSS files containing these hacks
+
+### Vendor CSS Imports
+
+Global CSS cannot be imported from files other than `_app.page.tsx` under Turbopack Pages Router. See the `vendor-styles-components` skill for the precompilation system that handles per-component vendor CSS.
+
 ## Module Optimization Configuration
 
 - `bundlePagesRouterDependencies: true` — bundles server-side dependencies for Pages Router
 - `serverExternalPackages: ['handsontable']` — packages excluded from server-side bundling
 - `optimizePackageImports` — 11 `@growi/*` packages configured (expansion to third-party packages was tested and reverted — it increased dev module count)
 
-## Module Count Measurement
-
-KPI: `[ChunkModuleStats] initial: N, async-only: N, total: N`
-
-- `initial` = modules in eager (initial) chunks — the primary reduction target
-- Measured via `bin/measure-chunk-stats.sh` (cleans `.next`, starts `next dev`, triggers compilation)
-- Any changes to webpack config or import patterns should be verified against the `initial` count
-
 ## Effective Module Reduction Techniques
 
 Techniques that have proven effective for reducing module count, ordered by typical impact:
@@ -63,7 +115,7 @@ Techniques that have proven effective for reducing module count, ordered by typi
 | `next/dynamic({ ssr: false })` | Client-only heavy components (e.g., Mermaid diagrams, interactive editors) |
 | Subpath imports | Packages with large barrel exports (e.g., `date-fns/format` instead of `date-fns`) |
 | Deep ESM imports | Packages that re-export multiple engines via barrel (e.g., `react-syntax-highlighter/dist/esm/prism-async-light`) |
-| null-loader | Server-only packages leaking into client bundle via transitive imports |
+| resolveAlias | Server-only packages leaking into client bundle via transitive imports |
 | Lightweight replacements | Replace large libraries used for a single feature (e.g., `tinykeys` instead of `react-hotkeys`, regex instead of `validator`) |
 
 ### Techniques That Did NOT Work
@@ -71,11 +123,6 @@ Techniques that have proven effective for reducing module count, ordered by typi
 - **Expanding `optimizePackageImports` to third-party packages** — In dev mode, this resolves individual sub-module files instead of barrel, resulting in MORE module entries. Reverted.
 - **Refactoring internal barrel exports** — Internal barrels (`states/`, `features/`) are small and well-scoped; refactoring had no measurable impact.
 
-## Turbopack Migration Path (Future)
-
-Turbopack adoption is deferred. Key blockers:
+## i18n HMR
 
-- `webpack()` config not supported — null-loader rules need `turbopack.resolveAlias` migration
-- Custom loaders (superjson-ssr-loader) need Turbopack rules testing
-- I18NextHMRPlugin has no Turbopack equivalent
-- Use `--webpack` flag in both dev and build until migration is complete
+`I18NextHMRPlugin` was removed during the Turbopack migration. Translation file changes require a manual browser refresh. The performance gain from Turbopack (faster Fast Refresh overall) outweighs the loss of i18n-specific HMR. Monitor if `i18next-hmr` adds Turbopack support in the future.

+ 116 - 0
apps/app/.claude/skills/vendor-styles-components/SKILL.md

@@ -0,0 +1,116 @@
+---
+name: vendor-styles-components
+description: Vendor CSS precompilation system for Turbopack compatibility. How to add third-party CSS to components without violating Pages Router global CSS restriction. Auto-invoked when working in apps/app.
+---
+
+# Vendor CSS Precompilation (apps/app)
+
+## Problem
+
+Turbopack (Pages Router) strictly enforces: **global CSS can only be imported from `_app.page.tsx`**. Components cannot `import 'package/style.css'` directly — Turbopack rejects these at compile time.
+
+Centralizing all vendor CSS in `_app` would degrade FCP for pages that don't need those styles.
+
+## Solution: Two-Track Vendor CSS System
+
+### Commons Track (globally shared CSS)
+
+- **File**: `src/styles/vendor.scss`
+- **For**: CSS needed on most pages (e.g., `simplebar-react`)
+- **Mechanism**: Compiled via `vite.vendor-styles-commons.ts` into `src/styles/prebuilt/`
+- **Imported from**: `_app.page.tsx`
+
+### Components Track (component-specific CSS)
+
+- **For**: CSS needed only by specific components
+- **Mechanism**: Vite precompiles `*.vendor-styles.ts` entry points into `*.vendor-styles.prebuilt.js` using `?inline` CSS import suffix
+- **Output**: Pure JS modules (no CSS imports) — Turbopack sees them as regular JS
+
+## How It Works
+
+1. **Entry point** (`ComponentName.vendor-styles.ts`): imports CSS via Vite `?inline` suffix, which inlines CSS as a string
+2. **Runtime injection**: the entry point creates a `<style>` tag and appends CSS to `document.head`
+3. **Vite prebuild** (`pre:styles-components` Turborepo task): compiles entry points into `*.vendor-styles.prebuilt.js`
+4. **Component import**: imports the `.prebuilt.js` file instead of raw CSS
+
+### Entry Point Template
+
+```typescript
+// @ts-nocheck -- Processed by Vite only; ?inline is a Vite-specific import suffix
+import css from 'some-package/dist/style.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css;
+document.head.appendChild(s);
+```
+
+For multiple CSS sources in one component:
+
+```typescript
+// @ts-nocheck
+import css1 from 'package-a/style.css?inline';
+import css2 from 'package-b/style.css?inline';
+
+const s = document.createElement('style');
+s.textContent = css1 + css2;
+document.head.appendChild(s);
+```
+
+## Current Entry Points
+
+| Entry Point | CSS Sources | Consuming Components |
+|---|---|---|
+| `Renderer.vendor-styles.ts` | `@growi/remark-lsx`, `@growi/remark-attachment-refs`, `katex` | renderer.tsx |
+| `GrowiEditor.vendor-styles.ts` | `@growi/editor` | PageEditor, CommentEditor |
+| `HandsontableModal.vendor-styles.ts` | `handsontable` (non-full variant) | HandsontableModal |
+| `DateRangePicker.vendor-styles.ts` | `react-datepicker` | DateRangePicker |
+| `RevisionDiff.vendor-styles.ts` | `diff2html` | RevisionDiff |
+| `DrawioViewerWithEditButton.vendor-styles.ts` | `@growi/remark-drawio` | DrawioViewerWithEditButton |
+| `ImageCropModal.vendor-styles.ts` | `react-image-crop` | ImageCropModal |
+| `Presentation.vendor-styles.ts` | `@growi/presentation` | Presentation, Slides |
+
+## Adding New Vendor CSS
+
+1. Create `{ComponentName}.vendor-styles.ts` next to the consuming component:
+   ```typescript
+   // @ts-nocheck
+   import css from 'new-package/dist/style.css?inline';
+   const s = document.createElement('style');
+   s.textContent = css;
+   document.head.appendChild(s);
+   ```
+2. In the component, replace `import 'new-package/dist/style.css'` with:
+   ```typescript
+   import './ComponentName.vendor-styles.prebuilt';
+   ```
+3. Run `pnpm run pre:styles-components` (or let Turborepo handle it during `dev`/`build`)
+4. The `.prebuilt.js` file is git-ignored and auto-generated
+
+**Decision guide**: If the CSS is needed on nearly every page, add it to the commons track (`vendor.scss`) instead.
+
+## Font/Asset Handling
+
+When vendor CSS references external assets (e.g., KaTeX `@font-face` with `url(fonts/KaTeX_*.woff2)`):
+
+- Vite emits asset files to `src/assets/` during build
+- The `moveAssetsToPublic` plugin (in `vite.vendor-styles-components.ts`) relocates them to `public/static/fonts/`
+- URL references in prebuilt JS are rewritten from `/assets/` to `/static/fonts/`
+- Fonts are served by the existing `express.static(crowi.publicDir)` middleware
+- Both `public/static/fonts/` and `src/**/*.vendor-styles.prebuilt.js` are git-ignored
+
+## Build Pipeline Integration
+
+```
+turbo.json tasks:
+  pre:styles-components  →  build (dependency)
+  dev:pre:styles-components  →  dev (dependency)
+
+Inputs:  vite.vendor-styles-components.ts, src/**/*.vendor-styles.ts, package.json
+Outputs: src/**/*.vendor-styles.prebuilt.js, public/static/fonts/**
+```
+
+## Important Caveats
+
+- **SSR**: CSS is injected via `<style>` tags at runtime — not available during SSR. Most consuming components use `next/dynamic({ ssr: false })`, so FOUC is not a practical concern
+- **`@ts-nocheck`**: Required because `?inline` is a Vite-specific import suffix not understood by TypeScript
+- **handsontable**: Must use `handsontable/dist/handsontable.css` (non-full, non-minified). The "full" variant (`handsontable.full.min.css`) contains IE CSS hacks (`*zoom:1`, `filter:alpha()`) that Turbopack's CSS parser (lightningcss) cannot parse. The "full" variant also includes Pikaday which is unused.