Browse Source

Merge branch 'dev/7.5.x' into support/upgrade-nextjs

Yuki Takei 1 month ago
parent
commit
27cd145bfc

+ 290 - 0
.kiro/specs/remove-ts-node/design.md

@@ -0,0 +1,290 @@
+# Design Document: Remove ts-node
+
+## Overview
+
+**Purpose**: This feature removes the `ts-node` runtime dependency from `apps/app` development and CI scripts, replacing it with Node.js 24's native TypeScript type-stripping. This eliminates the Next.js 15 hook conflict where `nextApp.prepare()` destroys `require.extensions['.ts']` registered by ts-node.
+
+**Users**: GROWI developers and CI pipeline will use Node.js 24 native TypeScript execution for the dev server, migrations, and REPL.
+
+**Impact**: Changes the TypeScript execution mechanism from ts-node (SWC transpilation) to Node.js built-in type erasure. Requires renaming ~64 `.js` files to `.ts` and removing the hook conflict workaround.
+
+### Goals
+- Remove `ts-node/register/transpile-only` from `apps/app` dev/CI scripts
+- Eliminate the `require.extensions['.ts']` hook save/restore workaround
+- Maintain path alias resolution (`~/`, `^/`) without code changes
+- Maintain `dotenv-flow/config` preloading behavior
+- Clean up `ts-node` configuration from `apps/app/tsconfig.json`
+
+### Non-Goals
+- Migrating `apps/slackbot-proxy` (blocked by decorator dependency on `@tsed/*` and TypeORM — deferred to separate work)
+- Removing `ts-node` from root `package.json` `devDependencies` (still needed by slackbot-proxy)
+- Removing `@swc/core` or `tsconfig-paths` from root (used by other tools)
+- Changing the production build pipeline (already compiles to JavaScript via `tsc`)
+
+## Architecture
+
+### Existing Architecture Analysis
+
+The current dev server execution chain is:
+
+```
+node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config src/server/app.ts
+```
+
+This loads three preload modules:
+1. **ts-node/register/transpile-only** — registers `.ts` extension handler via `require.extensions['.ts']`, transpiles TypeScript to JavaScript using SWC
+2. **tsconfig-paths/register** — patches `Module._resolveFilename` to resolve `~/` and `^/` path aliases from `tsconfig.json` `paths`
+3. **dotenv-flow/config** — loads `.env*` files into `process.env`
+
+The hook conflict occurs when Next.js 15 loads `next.config.ts`: it registers its own `require.extensions['.ts']` hook, then `deregisterHook()` deletes it — which also destroys ts-node's previously registered hook. The current workaround in `crowi/index.ts` saves and restores the hook around `nextApp.prepare()`.
+
+### Architecture Pattern & Boundary Map
+
+```mermaid
+graph TB
+    subgraph Before
+        Node1[node] --> TSNode[ts-node/register]
+        Node1 --> TscPaths1[tsconfig-paths/register]
+        Node1 --> DotEnv1[dotenv-flow/config]
+        TSNode --> AppTS1[src/server/app.ts]
+        TSNode --> JSFiles[Mixed .js files]
+    end
+
+    subgraph After
+        Node2[node] --> TscPaths2[tsconfig-paths/register]
+        Node2 --> DotEnv2[dotenv-flow/config]
+        Node2 --> NativeTS[Node.js 24 native type-stripping]
+        NativeTS --> AppTS2[src/server/app.ts]
+        NativeTS --> ConvertedTS[Converted .ts files]
+    end
+```
+
+**Architecture Integration**:
+- **Selected pattern**: Remove ts-node from the preload chain; rely on Node.js 24 built-in `.ts` extension handler
+- **Existing patterns preserved**: `tsconfig-paths/register` for path aliases (independent of ts-node), `dotenv-flow/config` for env loading
+- **New components rationale**: None — this is a removal/simplification, not an addition
+- **Steering compliance**: Follows GROWI's Node.js 24 target (`"node": "^24"`)
+
+### Technology Stack
+
+| Layer | Choice / Version | Role in Feature | Notes |
+|-------|------------------|-----------------|-------|
+| Runtime | Node.js 24 | Native TypeScript type-stripping | Default in Node.js 24, no flags needed |
+| Path Resolution | tsconfig-paths 4.2.0 | Runtime `~/` and `^/` alias resolution via `Module._resolveFilename` | Already in use; independent of ts-node (see `research.md`) |
+| Env Loading | dotenv-flow 3.2.0 | `.env` file loading via `-r` flag | No changes needed |
+| Removed | ts-node 10.9.2 | No longer used in `apps/app` | Remains in root for slackbot-proxy |
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Interfaces | Flows |
+|-------------|---------|------------|------------|-------|
+| 1.1–1.3 | Dev/CI scripts start without ts-node | PackageJsonScripts | — | Dev startup |
+| 1.4–1.5 | Remove ts-node config from tsconfig.json | TsconfigCleanup | — | — |
+| 1.6 | Replace ts-node composite script | PackageJsonScripts | — | — |
+| 2.1–2.4 | Path aliases resolve correctly | PathAliasResolution | — | Module resolution |
+| 3.1–3.3 | Hook conflict workaround removed | HookWorkaroundRemoval | — | Next.js setup |
+| 4.1–4.3 | Mixed .js files continue working | JsToTsConversion | — | — |
+| 5.1–5.2 | dotenv-flow continues loading | PackageJsonScripts | — | Dev startup |
+| 6.1–6.2 | project-dir-utils detects config | — (already implemented) | — | — |
+| 7.1 | Remove ts-node from root devDeps | — (deferred: slackbot-proxy) | — | — |
+| 7.2 | Evaluate tsconfig-paths removal | — (retained: still used) | — | — |
+| 7.3 | Evaluate @swc/core removal | — (retained: VSCode debug, pdf-converter) | — | — |
+| 8.1–8.4 | CI jobs pass | PackageJsonScripts | — | CI pipeline |
+
+## Components and Interfaces
+
+| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
+|-----------|--------------|--------|--------------|------------------|-----------|
+| PackageJsonScripts | Build Config | Replace ts-node composite script with native TS execution | 1.1–1.3, 1.6, 5.1–5.2, 8.1–8.4 | tsconfig-paths (P0), dotenv-flow (P0) | — |
+| TsconfigCleanup | Build Config | Remove ts-node config sections from tsconfig.json | 1.4–1.5 | — | — |
+| HookWorkaroundRemoval | Server Runtime | Delete require.extensions save/restore workaround | 3.1–3.3 | Next.js (P0) | — |
+| JsToTsConversion | Server Source | Rename .js → .ts for files with ESM import syntax | 4.1–4.3, 2.3 | — | — |
+| EnumToConstConversion | Server Source | Convert UploadStatus enum to const object | — (enabler) | — | Service |
+| PathAliasResolution | Module Resolution | Retain tsconfig-paths/register for ~/  and ^/ aliases | 2.1–2.4 | tsconfig-paths (P0) | — |
+
+### Build Config
+
+#### PackageJsonScripts
+
+| Field | Detail |
+|-------|--------|
+| Intent | Replace the `ts-node` composite script in `apps/app/package.json` with a native Node.js 24 equivalent |
+| Requirements | 1.1–1.3, 1.6, 5.1–5.2, 8.1–8.4 |
+
+**Responsibilities & Constraints**
+- Replace `"ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config"` with `"node-dev": "node -r tsconfig-paths/register -r dotenv-flow/config"`
+- Rename the script from `ts-node` to `node-dev` to avoid confusion (ts-node is no longer used)
+- Update all 4 callers that reference `pnpm run ts-node` → `pnpm run node-dev`: `dev`, `launch-dev:ci`, `dev:migrate-mongo`, `repl`
+
+**Dependencies**
+- Outbound: tsconfig-paths/register — path alias resolution (P0)
+- Outbound: dotenv-flow/config — env variable loading (P0)
+- External: Node.js 24 — native `.ts` execution (P0)
+
+**Implementation Notes**
+- `nodemon --exec pnpm run node-dev --inspect` pattern continues to work since nodemon only watches file changes and re-executes the command
+
+#### TsconfigCleanup
+
+| Field | Detail |
+|-------|--------|
+| Intent | Remove the `ts-node` configuration section from `apps/app/tsconfig.json` |
+| Requirements | 1.4–1.5 |
+
+**Responsibilities & Constraints**
+- Remove the `"ts-node": { "transpileOnly": true, "swc": true, "compilerOptions": { ... } }` section
+- The `compilerOptions.module: "CommonJS"` and `moduleResolution: "Node"` settings inside `ts-node` are ts-node-specific overrides; the base config uses `"module": "ESNext"` and `"moduleResolution": "Bundler"` which are correct for the build pipeline
+- Note: `apps/slackbot-proxy/tsconfig.json` cleanup is deferred (out of scope)
+
+### Server Runtime
+
+#### HookWorkaroundRemoval
+
+| Field | Detail |
+|-------|--------|
+| Intent | Remove the `require.extensions['.ts']` save/restore workaround in `crowi/index.ts` |
+| Requirements | 3.1–3.3 |
+
+**Responsibilities & Constraints**
+- Delete lines 557–566 in `src/server/crowi/index.ts` (the `savedTsHook` / `require.extensions` block)
+- Node.js 24 native type-stripping does not register `require.extensions['.ts']` — it uses an internal mechanism that Next.js cannot destroy
+- After removal, the Next.js setup block simplifies to just `this.nextApp = next({ dev }); await this.nextApp.prepare();`
+
+**Dependencies**
+- Inbound: Next.js 15 `nextApp.prepare()` — the trigger for the former conflict (P0)
+
+### Server Source
+
+#### JsToTsConversion
+
+| Field | Detail |
+|-------|--------|
+| Intent | Rename `.js` files with ESM `import` syntax to `.ts` so they execute correctly under Node.js 24 native type-stripping |
+| Requirements | 4.1–4.3, 2.3 |
+
+**Responsibilities & Constraints**
+- Rename 52 files that use ESM `import` syntax (which ts-node currently transpiles to CJS)
+- Optionally rename the remaining 12 pure CJS files for consistency (recommended)
+- No content changes needed inside files — ESM `import` + `module.exports` is valid TypeScript, and `require()` is valid with `esModuleInterop: true`
+- Use `git mv` for each rename to preserve git history
+
+**File Categories** (see `research.md` for full audit):
+
+| Category | Count | Action |
+|----------|-------|--------|
+| Pure ESM (import/export only) | 8 | Rename `.js` → `.ts` |
+| Mixed (ESM import + module.exports) | 44 | Rename `.js` → `.ts` |
+| Pure CJS (require/module.exports only) | 12 | Rename `.js` → `.ts` (optional, for consistency) |
+
+**Implementation Notes**
+- All internal imports of these files use path aliases (`~/`, `^/`) or relative paths without extensions — `tsconfig-paths` and Node.js resolution handle `.ts` extension matching
+- Imports referencing these files from `.ts` callers already omit the extension, so no caller changes are needed
+- A dedicated commit for renames (before any code changes) preserves `git blame` accuracy
+
+#### EnumToConstConversion
+
+| Field | Detail |
+|-------|--------|
+| Intent | Convert `enum UploadStatus` to a const object pattern compatible with Node.js 24 native type-stripping |
+| Requirements | — (enabler for 1.1–1.3) |
+
+**Responsibilities & Constraints**
+- Convert in `src/server/service/file-uploader/multipart-uploader.ts`
+- The enum is used as value comparisons only (`UploadStatus.BEFORE_INIT`, etc.) — no reverse mapping (`UploadStatus[0]`) detected
+- Consumers: `multipart-uploader.spec.ts`, `gcs/multipart-uploader.ts`, `aws/multipart-uploader.ts`
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+Current:
+```typescript
+export enum UploadStatus {
+  BEFORE_INIT,
+  IN_PROGRESS,
+  COMPLETED,
+  ABORTED,
+}
+```
+
+Target:
+```typescript
+export const UploadStatus = {
+  BEFORE_INIT: 0,
+  IN_PROGRESS: 1,
+  COMPLETED: 2,
+  ABORTED: 3,
+} as const;
+
+export type UploadStatus = (typeof UploadStatus)[keyof typeof UploadStatus];
+```
+
+- Preconditions: All consumers use `UploadStatus.MEMBER_NAME` syntax (no numeric access)
+- Postconditions: `UploadStatus.BEFORE_INIT === 0`, `UploadStatus.IN_PROGRESS === 1`, etc. — identical runtime values
+- Invariants: Consumers require no changes — `UploadStatus` as both value and type is preserved by the dual declaration
+
+### Module Resolution
+
+#### PathAliasResolution
+
+| Field | Detail |
+|-------|--------|
+| Intent | Retain `tsconfig-paths/register` for runtime path alias resolution without ts-node |
+| Requirements | 2.1–2.4 |
+
+**Responsibilities & Constraints**
+- `tsconfig-paths/register` is loaded via `node -r tsconfig-paths/register` before the application entry point
+- It patches `Module._resolveFilename` to intercept `~/` and `^/` imports and resolve them against `tsconfig.json` `paths`
+- This mechanism is independent of ts-node (see `research.md` — zero ts-node dependency, works via standard CJS module resolution)
+- Node.js 24 native type-stripping does not interfere with `Module._resolveFilename` patching
+
+**Dependencies**
+- External: tsconfig-paths 4.2.0 — CJS module resolver patch (P0)
+- External: `apps/app/tsconfig.json` `paths` config — alias definitions (P0)
+
+**Implementation Notes**
+- No code changes needed — the existing `tsconfig-paths/register` mechanism continues to work identically
+- The only change is removing `ts-node/register/transpile-only` from the preload chain
+
+## Testing Strategy
+
+### Unit Tests
+- `multipart-uploader.spec.ts` — verify `UploadStatus` const conversion preserves all value comparisons and error behaviors
+- Existing server-side unit tests — run full suite to detect any breakage from `.js` → `.ts` renames
+
+### Integration Tests
+- Dev server startup (`pnpm run dev`) — verify server starts, loads Next.js, serves pages
+- CI launch (`pnpm run launch-dev:ci`) — verify health check passes
+- Migration scripts (`pnpm run dev:migrate`) — verify database migrations execute
+- REPL (`pnpm run repl`) — verify interactive session starts
+
+### Smoke Tests
+- Path alias resolution: verify `~/` and `^/` imports resolve correctly in renamed `.ts` files
+- `dotenv-flow` loading: verify `.env` variables are available at startup
+- Next.js `next.config.ts` loading: verify no hook conflict without the workaround code
+
+### CI Pipeline
+- `ci-app-lint` — typecheck and biome pass with renamed files
+- `ci-app-test` — all tests pass
+- `ci-app-launch-dev` — dev server starts and responds
+- `test-prod-node24 / build-prod` — production build succeeds (unaffected but verified)
+
+## Migration Strategy
+
+The migration is executed in ordered commits to preserve git history and enable bisection:
+
+```mermaid
+graph LR
+    C1[Commit 1: enum to const] --> C2[Commit 2: git mv .js to .ts]
+    C2 --> C3[Commit 3: update package.json scripts]
+    C3 --> C4[Commit 4: remove hook workaround]
+    C4 --> C5[Commit 5: remove tsconfig ts-node section]
+```
+
+1. **Commit 1**: Convert `enum UploadStatus` to `const` object (isolated, testable)
+2. **Commit 2**: `git mv` all `.js` → `.ts` renames (pure rename, no content changes — preserves blame)
+3. **Commit 3**: Rename `ts-node` script to `node-dev`, remove `ts-node/register/transpile-only`, update all 4 callers
+4. **Commit 4**: Remove hook workaround from `crowi/index.ts`
+5. **Commit 5**: Remove `ts-node` section from `apps/app/tsconfig.json`
+
+Rollback: Each commit is independently revertible. If issues arise, revert commits 3–5 to restore ts-node execution while keeping the file renames.

+ 231 - 0
.kiro/specs/remove-ts-node/gap-analysis.md

@@ -0,0 +1,231 @@
+# Gap Analysis: Remove ts-node
+
+## Executive Summary
+
+- **Scope**: Replace `ts-node` with Node.js 24 native type-stripping in `apps/app` and `apps/slackbot-proxy` dev/CI scripts
+- **Core challenge**: Path alias resolution (`~/`, `^/`) — `tsconfig-paths/register` must remain or be replaced since Node.js native TS does not read `tsconfig.json`
+- **Major blocker for slackbot-proxy**: Heavy use of TypeScript decorators (`@tsed/*`, TypeORM `@Entity`, `@Column`) — Node.js native type-stripping does **not** support decorators, even with `--experimental-transform-types`
+- **CJS/ESM concern**: ~60 `.js` files in `apps/app/src/server/` use ESM `import` syntax but the package has no `"type": "module"` — currently transpiled by ts-node to CJS. Node.js 24 cannot transpile these.
+- **Recommended approach**: Hybrid — straightforward removal for `apps/app` (with `.js` → `.ts` conversions as needed), separate investigation for `apps/slackbot-proxy` due to decorator dependency
+
+---
+
+## 1. Current State Investigation
+
+### 1.1 ts-node Usage Map
+
+| Location | Usage | Purpose |
+|---|---|---|
+| `apps/app/package.json:48` | `"ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config"` | Composite script used by `dev`, `launch-dev:ci`, `dev:migrate-mongo`, `repl` |
+| `apps/slackbot-proxy/package.json:23` | `"ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config"` | Composite script used by `dev`, `dev:ci` |
+| `apps/app/tsconfig.json:30-37` | `"ts-node": { "transpileOnly": true, "swc": true, ... }` | Configures ts-node to use SWC, CJS module output |
+| `apps/slackbot-proxy/tsconfig.json:42-45` | `"ts-node": { "transpileOnly": true, "swc": true }` | Same SWC-based transpile-only mode |
+| `package.json:81` | `"ts-node": "^10.9.2"` (root devDependency) | Hoisted dependency for both apps |
+
+### 1.2 Hook Conflict Workaround
+
+**File**: `apps/app/src/server/crowi/index.ts:557-566`
+
+```typescript
+const savedTsHook = require.extensions['.ts'];
+this.nextApp = next({ dev });
+await this.nextApp.prepare();
+if (savedTsHook && !require.extensions['.ts']) {
+  require.extensions['.ts'] = savedTsHook;
+}
+```
+
+This saves/restores ts-node's `require.extensions['.ts']` hook around `nextApp.prepare()` because Next.js 15's `next.config.ts` transpiler destroys it. With native type-stripping (which does not register `require.extensions`), this workaround becomes unnecessary.
+
+### 1.3 Path Alias Resolution
+
+**Current mechanism**: `tsconfig-paths/register` loaded via `-r` flag reads `tsconfig.json` `paths` at runtime.
+
+**Aliases in use**:
+- `apps/app`: `~/` → `./src/*`, `^/` → `./*`
+- `apps/slackbot-proxy`: `~/` → `./src/*`
+
+**Consumers**: Extensive usage across server code — `project-dir-utils.ts`, `express-init.js`, `dev.js`, route files, services, etc.
+
+### 1.4 CJS/ESM Mixed Files
+
+**~60 `.js` files in `apps/app/src/server/`** use ESM `import` syntax (e.g., `import express from 'express'`) combined with `module.exports` or CJS `require()`. These work today because ts-node transpiles them to CJS. Notable files:
+- `src/server/crowi/dev.js` — ESM imports + `module.exports`
+- `src/server/crowi/express-init.js` — ESM imports + `module.exports`
+- `src/server/routes/*.js` — Mixed ESM/CJS patterns
+- `src/server/models/*.js` — Mixed ESM/CJS patterns
+
+**Neither `apps/app` nor `apps/slackbot-proxy` have `"type": "module"` in package.json.**
+
+Node.js 24 native type-stripping does **not** perform module system conversion. These files will fail with syntax errors if loaded as-is.
+
+### 1.5 Decorator Usage (Critical for slackbot-proxy)
+
+**`apps/slackbot-proxy`** heavily relies on TypeScript decorators via `@tsed/*` and TypeORM:
+- `@Service()`, `@Inject()`, `@Controller()`, `@Middleware()`
+- `@Entity()`, `@Column()`, `@PrimaryGeneratedColumn()`, `@ManyToOne()`
+- `@Get()`, `@Post()`, `@HeaderParams()`, `@BodyParams()`
+
+**Node.js native type-stripping does NOT support decorators** — not even with `--experimental-transform-types`. This is a hard blocker for `apps/slackbot-proxy`.
+
+**`apps/app`** does **not** use decorators in server code. It has one `enum` usage (`UploadStatus` in `multipart-uploader.ts`), which should be converted to a `const` object to remain compatible with native type-stripping without any flags.
+
+### 1.6 project-dir-utils.ts
+
+**File**: `apps/app/src/server/util/project-dir-utils.ts`
+
+Already checks for both `next.config.ts` and `next.config.js`:
+```typescript
+const isCurrentDirRoot = isServer() && (fs.existsSync('./next.config.ts') || fs.existsSync('./next.config.js'));
+```
+
+**Status**: ✅ Requirement 6 is already satisfied. No changes needed.
+
+### 1.7 dotenv-flow Integration
+
+`dotenv-flow/config` is loaded via `-r` flag in the composite `ts-node` script. Since it's a plain CJS module, `node -r dotenv-flow/config` will continue to work with Node.js 24 natively. No changes needed to dotenv-flow integration itself.
+
+### 1.8 @swc/core Dependency
+
+| Package | `@swc/core` Usage |
+|---|---|
+| Root `package.json` | `devDependency` — used by ts-node (SWC mode) and `@swc-node/register` |
+| `apps/pdf-converter` | Independent `devDependency` — uses `@swc-node/register/esm-register` |
+| `.vscode/launch.json` | "Debug: Current File" uses `@swc-node/register` |
+
+**Assessment**: `@swc/core` in root is shared between ts-node and the VSCode debugger. Removing ts-node alone does not justify removing `@swc/core` from root — the VSCode launch config and `@swc-node/register` still need it.
+
+---
+
+## 2. Requirement-to-Asset Map
+
+| Requirement | Current Assets | Gap |
+|---|---|---|
+| **R1: Remove ts-node from dev runtime** | `package.json` scripts, tsconfig `ts-node` sections | **Extend**: Replace `-r ts-node/register/transpile-only` with native TS execution |
+| **R2: Maintain path alias resolution** | `tsconfig-paths/register` via `-r` flag | **Unknown**: Need to validate `tsconfig-paths/register` works with Node.js 24 native TS, or find alternative |
+| **R3: Eliminate Next.js hook conflict** | `crowi/index.ts:557-566` workaround | **Simple removal**: Delete 6 lines of hook save/restore code |
+| **R4: CJS/ESM compatibility** | ~60 `.js` files with mixed syntax, transpiled by ts-node | **Missing**: These files need conversion to valid CJS or `.ts` |
+| **R5: dotenv-flow integration** | `-r dotenv-flow/config` in composite script | **No gap**: Works natively with `node -r` |
+| **R6: project-dir-utils config detection** | Already checks both `.ts` and `.js` | **No gap**: Already implemented |
+| **R7: Clean up dependencies** | `ts-node` in root, `tsconfig-paths` in root, `@swc/core` in root | **Constraint**: `@swc/core` needed by VSCode debug config; `tsconfig-paths` may still be needed |
+| **R8: CI compatibility** | CI uses Node.js 24.x, runs `launch-dev:ci`, `test`, `lint` | **Extend**: Update scripts; CI infra already compatible |
+
+### Gap Tags
+
+- **R2**: `Research Needed` — validate `tsconfig-paths/register` + Node.js 24 native TS compatibility
+- **R4**: `Missing` — ~60 `.js` files need conversion or the module system needs adjustment
+- **R7**: `Constraint` — `@swc/core` and `@swc-node/register` are used outside of ts-node
+- **slackbot-proxy decorators**: `Blocker` — decorators are not supported by native type-stripping
+
+---
+
+## 3. Implementation Approach Options
+
+### Option A: Minimal Change — Keep `tsconfig-paths`, Convert `.js` Files
+
+**Strategy**: Replace `ts-node/register/transpile-only` with nothing (rely on native TS), keep `tsconfig-paths/register` for path aliases, convert problematic `.js` files to `.ts`.
+
+**Changes required**:
+1. Update `apps/app/package.json` script: `"ts-node": "node -r tsconfig-paths/register -r dotenv-flow/config"`
+2. Update `apps/slackbot-proxy/package.json` similarly (but see blocker below)
+3. Remove `ts-node` sections from both `tsconfig.json` files
+4. Remove hook workaround from `crowi/index.ts`
+5. Convert ~60 `.js` files to `.ts` (or fix their CJS/ESM syntax)
+6. Convert the one `enum UploadStatus` to a `const` object + type union
+7. Remove `ts-node` from root `package.json`
+
+**Trade-offs**:
+- ✅ Minimal conceptual change — `tsconfig-paths/register` is well-understood
+- ✅ Path aliases continue working without code changes
+- ❌ Large file conversion effort (~60 `.js` → `.ts` files)
+- ❌ `tsconfig-paths/register` + native TS compatibility not officially validated
+- ❌ **Blocked for slackbot-proxy** due to decorators
+
+### Option B: Full Native — Use Node.js Subpath Imports
+
+**Strategy**: Replace both `ts-node` and `tsconfig-paths` with Node.js-native mechanisms. Use `package.json` `"imports"` field for path aliases.
+
+**Changes required**:
+1. Everything in Option A
+2. Replace `~/` and `^/` aliases with `#` prefixed subpath imports
+3. Update all import statements across the codebase
+4. Remove `tsconfig-paths` from root `devDependencies`
+5. Update `tsconfig.json` paths to match subpath imports
+
+**Trade-offs**:
+- ✅ Zero external runtime dependencies for TS execution
+- ✅ Uses officially supported Node.js mechanism
+- ❌ **Massive refactor**: hundreds of import statements to change
+- ❌ `#` prefix is less ergonomic than `~/`
+- ❌ Requires updating both `tsconfig.json` (for IDE/build) and `package.json` (for runtime)
+- ❌ Still blocked for slackbot-proxy
+
+### Option C: Hybrid — Phase by Package
+
+**Strategy**: Remove ts-node from `apps/app` first (using Option A approach), defer `apps/slackbot-proxy` until its decorator situation is resolved (e.g., tsed v7 migration or keeping ts-node only for slackbot-proxy).
+
+**Phase 1 — `apps/app`**:
+1. Replace `ts-node` script with `node -r tsconfig-paths/register -r dotenv-flow/config`
+2. Convert the one `enum UploadStatus` to a `const` object (no special flags needed)
+3. Convert `.js` files with mixed ESM/CJS to proper `.ts`
+4. Remove hook workaround
+5. Remove `ts-node` config from `apps/app/tsconfig.json`
+
+**Phase 2 — `apps/slackbot-proxy`** (deferred):
+- Keep ts-node for slackbot-proxy until decorator support is available
+- Or: migrate slackbot-proxy off decorators (separate project)
+- Or: use `@swc-node/register` as ts-node replacement (it supports decorators)
+
+**Phase 3 — Cleanup**:
+- Remove `ts-node` from root only when both apps are migrated
+- Evaluate `tsconfig-paths` removal after validation
+
+**Trade-offs**:
+- ✅ Delivers value incrementally
+- ✅ Resolves the Next.js hook conflict immediately
+- ✅ Does not block on slackbot-proxy's decorator dependency
+- ❌ ts-node remains in root `devDependencies` until Phase 2 completes
+- ❌ Two different TS execution patterns coexist temporarily
+
+---
+
+## 4. Implementation Complexity & Risk
+
+**Effort**: **M (3–7 days)**
+- Core script changes are straightforward (S)
+- `.js` → `.ts` file conversions add bulk (M)
+- slackbot-proxy decorator blocker may require investigation (pushes toward L if addressed)
+
+**Risk**: **Medium**
+- `tsconfig-paths/register` + native TS is not officially validated (mitigated by testing)
+- ~60 file conversions carry regression risk (mitigated by existing tests)
+- slackbot-proxy is a hard blocker without decorator support
+
+---
+
+## 5. Research Items for Design Phase
+
+1. **`tsconfig-paths/register` + Node.js 24 native TS**: Validate that `node -r tsconfig-paths/register file.ts` correctly resolves `~/` and `^/` aliases without ts-node present. This is the highest priority research item.
+
+2. **slackbot-proxy strategy**: Determine if `@swc-node/register` (already used by `apps/pdf-converter`) can replace ts-node for slackbot-proxy's decorator needs, or if slackbot-proxy should be excluded from scope.
+
+3. **`.js` file conversion scope**: Audit all ~60 `.js` files to determine which actually need conversion vs. which are pure CJS (valid without ts-node). Files using only `require`/`module.exports` need no changes.
+
+4. **VSCode launch.json**: The "Debug: Current File" configuration uses `@swc-node/register`. Evaluate if this should be updated to use native TS or left as-is.
+
+---
+
+## 6. Recommendations for Design Phase
+
+**Preferred approach**: **Option C (Hybrid)**
+- Scope Phase 1 to `apps/app` only, which resolves the core Next.js hook conflict
+- Validate `tsconfig-paths/register` compatibility early (spike/PoC)
+- Defer slackbot-proxy to a separate task or Phase 2
+- Audit `.js` files to determine actual conversion scope (many may be pure CJS)
+
+**Key decisions needed**:
+- Whether slackbot-proxy is in scope or deferred
+- Whether to keep `tsconfig-paths/register` or invest in an alternative
+
+**Already decided**:
+- Convert the one `enum UploadStatus` to a `const` object + type union — `--experimental-transform-types` is not needed

+ 101 - 0
.kiro/specs/remove-ts-node/requirements.md

@@ -0,0 +1,101 @@
+# Requirements Document
+
+## Introduction
+
+GROWI's development server (`apps/app` and `apps/slackbot-proxy`) currently uses `ts-node` to execute TypeScript at runtime. This creates an incompatibility with Next.js 15's `next.config.ts` transpilation, which destroys `require.extensions['.ts']` hooks registered by ts-node. Since GROWI targets Node.js 24 (`"node": "^24"`), which has built-in TypeScript type-stripping enabled by default, ts-node can be replaced with Node.js native TypeScript execution. This eliminates the hook conflict, reduces dependencies, and simplifies the runtime stack.
+
+**Scope**: Development and CI environments only. Production already runs compiled JavaScript (`dist/`) and is unaffected.
+
+**Affected packages**:
+- `apps/app` — uses `node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config`
+- `apps/slackbot-proxy` — uses the same pattern
+
+**Out of scope**:
+- `apps/pdf-converter` — already uses `@swc-node/register/esm-register` (no ts-node)
+- Production builds — already compile to JavaScript via `next build` / `tsc`
+- Vitest configurations — use `vite-tsconfig-paths` (unrelated to runtime ts-node)
+
+## Requirements
+
+### Requirement 1: Remove ts-node from Development Runtime
+
+**Objective:** As a developer, I want the dev server to run TypeScript using Node.js native type-stripping, so that the ts-node dependency and its configuration are no longer needed at runtime.
+
+#### Acceptance Criteria
+
+1. When the `pnpm run dev` command is executed in `apps/app`, the dev server shall start without loading `ts-node/register/transpile-only`.
+2. When the `pnpm run dev` command is executed in `apps/slackbot-proxy`, the dev server shall start without loading `ts-node/register/transpile-only`.
+3. When the `pnpm run launch-dev:ci` command is executed, the dev server shall start successfully without ts-node.
+4. The `ts-node` configuration section in `apps/app/tsconfig.json` shall be removed.
+5. The `ts-node` configuration section in `apps/slackbot-proxy/tsconfig.json` shall be removed.
+6. The `ts-node` package script in `apps/app/package.json` and `apps/slackbot-proxy/package.json` shall be replaced with an equivalent that uses Node.js native TypeScript execution.
+
+### Requirement 2: Maintain Path Alias Resolution
+
+**Objective:** As a developer, I want `~/` and `^/` path aliases to continue resolving correctly at runtime, so that all existing `import` and `require` statements work without modification.
+
+#### Acceptance Criteria
+
+1. When a TypeScript source file imports a module using the `~/` prefix (e.g., `import x from '~/utils/logger'`), the dev server shall resolve it to the corresponding file under `src/`.
+2. When a TypeScript source file imports a module using the `^/` prefix (e.g., `import x from '^/config/foo'`), the dev server shall resolve it to the corresponding file under the project root.
+3. While the dev server is running, the path alias resolution shall work for both `.ts` and `.js` files.
+4. The path alias resolution mechanism shall not depend on `require.extensions['.ts']` being registered, since Node.js native TypeScript execution does not register it.
+
+### Requirement 3: Eliminate Next.js 15 Hook Conflict
+
+**Objective:** As a developer, I want the Next.js hook conflict workaround to become unnecessary, so that the codebase is simpler and not reliant on brittle hook restoration logic.
+
+#### Acceptance Criteria
+
+1. When `nextApp.prepare()` completes, the dev server shall be able to `require()` TypeScript files without any explicit hook restoration code.
+2. The workaround code in `src/server/crowi/index.ts` that saves and restores `require.extensions['.ts']` shall be removed.
+3. When `next.config.ts` is loaded by Next.js, the dev server's ability to execute TypeScript files shall not be affected.
+
+### Requirement 4: Maintain CJS/ESM Compatibility for Mixed-Format Files
+
+**Objective:** As a developer, I want existing `.js` files that use ESM `import` syntax (transpiled by ts-node today) to continue working, so that no source file changes are required in those files.
+
+#### Acceptance Criteria
+
+1. When the dev server loads `src/server/crowi/dev.js` (which uses ESM `import` syntax in a `.js` file), the file shall execute correctly.
+2. While the dev server is running, files that mix ESM `import` syntax and CommonJS `module.exports` shall be handled without errors.
+3. If a `.js` file cannot be executed by Node.js native TypeScript support alone, the build system shall provide a fallback mechanism or the file shall be converted to `.ts`.
+
+### Requirement 5: Maintain dotenv-flow Integration
+
+**Objective:** As a developer, I want environment variables to continue loading via `dotenv-flow/config` at dev server startup, so that `.env` files are processed as before.
+
+#### Acceptance Criteria
+
+1. When the dev server starts, the `dotenv-flow/config` module shall be loaded before the application entry point executes.
+2. The environment variable loading behavior shall be identical to the current `node -r dotenv-flow/config` approach.
+
+### Requirement 6: Update project-dir-utils.ts Config Detection
+
+**Objective:** As a developer, I want `project-dir-utils.ts` to detect the project root correctly regardless of whether the config file is `.ts` or `.js`, so that runtime path resolution is accurate.
+
+#### Acceptance Criteria
+
+1. The `isCurrentDirRoot` check in `project-dir-utils.ts` shall accept both `next.config.ts` and `next.config.js`.
+2. When `next.config.ts` exists (and `next.config.js` does not), the `projectRoot` shall resolve to `process.cwd()`.
+
+### Requirement 7: Clean Up Unused Dependencies
+
+**Objective:** As a maintainer, I want ts-node and related unnecessary dependencies removed from the workspace, so that the dependency tree is smaller and maintenance burden is reduced.
+
+#### Acceptance Criteria
+
+1. The `ts-node` package shall be removed from the root `package.json` `devDependencies`.
+2. If `tsconfig-paths` is no longer used by any runtime script (only by Vitest via `vite-tsconfig-paths`), it shall be evaluated for removal from `devDependencies`.
+3. The `@swc/core` dependency shall be evaluated: if it was only required for ts-node's SWC mode and is not used elsewhere, it shall be removed.
+
+### Requirement 8: CI Pipeline Compatibility
+
+**Objective:** As a CI engineer, I want all CI jobs (`ci-app-launch-dev`, `ci-app-test`, `ci-app-lint`) to pass with the new TypeScript execution approach, so that the migration does not break the development workflow.
+
+#### Acceptance Criteria
+
+1. When `ci-app-launch-dev` runs, the dev server shall start and respond to health checks successfully.
+2. When `ci-app-test` runs, all existing tests shall pass without modification.
+3. When `ci-app-lint` runs, all lint checks shall pass without new errors.
+4. When `test-prod-node24 / build-prod` runs, the production build shall succeed.

+ 117 - 0
.kiro/specs/remove-ts-node/research.md

@@ -0,0 +1,117 @@
+# Research & Design Decisions: remove-ts-node
+
+## Summary
+- **Feature**: `remove-ts-node`
+- **Discovery Scope**: Extension (modifying existing dev/CI toolchain)
+- **Key Findings**:
+  - `tsconfig-paths/register` is fully independent of ts-node and works with Node.js 24 native TS via CJS `Module._resolveFilename` patching
+  - 52 of 64 `.js` files in `apps/app/src/server/` use ESM `import` syntax and require `.js` → `.ts` conversion
+  - `apps/slackbot-proxy` is blocked by decorator dependency — deferred to separate work
+
+## Research Log
+
+### tsconfig-paths/register + Node.js 24 Native TS Compatibility
+
+- **Context**: Gap analysis identified this as the highest-priority research item. Node.js 24 does not read `tsconfig.json`, so path aliases (`~/`, `^/`) need an independent mechanism.
+- **Sources Consulted**:
+  - [tsconfig-paths source code](https://github.com/dividab/tsconfig-paths) — `register.js`, `config-loader.js`, `match-path-sync.ts`
+  - [Node.js TypeScript documentation](https://nodejs.org/api/typescript.html)
+  - [tsconfig-paths npm](https://www.npmjs.com/package/tsconfig-paths) — README standalone usage
+- **Findings**:
+  - `tsconfig-paths/register` has **zero dependency on ts-node**. Dependencies: `json5`, `minimist`, `strip-bom` only.
+  - It patches `Module._resolveFilename` (CJS resolver) to intercept aliased imports before normal resolution.
+  - Node.js 24 native type-stripping does not modify `Module._resolveFilename` — it only registers an internal `.ts` extension handler for type erasure.
+  - Execution order with `node -r tsconfig-paths/register -r dotenv-flow/config file.ts`: (1) tsconfig-paths patches resolver, (2) dotenv-flow loads env, (3) Node strips types from `.ts` file, (4) aliased `require()` calls go through patched resolver.
+  - The `TS_NODE_PROJECT` / `TS_NODE_BASEURL` env var checks in tsconfig-paths are backward-compatibility shortcuts, not hard dependencies.
+  - GROWI's `apps/app/tsconfig.json` has `paths` but no `baseUrl`. tsconfig-paths handles this correctly — it uses `tsconfig.json`'s directory as the base and only resolves explicit path patterns.
+- **Implications**: `node -r tsconfig-paths/register -r dotenv-flow/config src/server/app.ts` is a valid drop-in replacement for the current `ts-node` composite script. No alternative path resolution mechanism needed.
+
+### .js File Audit: ESM/CJS Categorization
+
+- **Context**: ts-node currently transpiles `.js` files containing ESM `import` syntax to CJS. Without ts-node, Node.js 24 cannot do this conversion. Files must be assessed for conversion needs.
+- **Sources Consulted**: Direct file-by-file read of all 64 `.js` files in `apps/app/src/server/`
+- **Findings**:
+  - **12 files: Pure CJS** — use only `require()`/`module.exports`, no conversion needed
+    - `models/vo/s2s-message.js`, `middlewares/inject-currentuser-to-localvars.js`, `middlewares/auto-reconnect-to-s2s-msg-server.js`, `routes/user.js`, `routes/avoid-session-routes.js`, `util/apiResponse.js`, `util/apiPaginate.js`, `util/formUtil.js`, `util/getToday.js`, `util/express-validator/sanitizer.js`, `util/express-validator/validator.js` (empty), `service/slack-command-handler/slack-command-handler.js`
+  - **8 files: Pure ESM** — use only `import`/`export`, cleanest conversion candidates
+    - `models/serializers/*.js` (4 files), `models/slack-app-integration.js`, `models/user/index.js`, `routes/attachment/api.js`, `util/slack.js`
+  - **44 files: Mixed ESM/CJS** — ESM `import` at top + `module.exports` at bottom (some with inline `require()`)
+    - Routes (`routes/*.js`, `routes/apiv3/*.js`), services (`service/slack-command-handler/*.js`), middlewares, crowi (`dev.js`, `express-init.js`)
+- **Implications**:
+  - All 52 ESM/mixed files can be converted by simple `.js` → `.ts` rename. The ESM `import` syntax is already valid TypeScript. Inline `require()` calls are also valid in TypeScript with `esModuleInterop: true`.
+  - The 12 pure CJS files can remain as `.js` but renaming to `.ts` for consistency is acceptable since `require()`/`module.exports` is valid TypeScript.
+  - Recommended: rename all 64 files to `.ts` for uniformity. This eliminates the CJS/ESM ambiguity entirely.
+
+### Node.js 24 Type-Stripping Limitations
+
+- **Context**: Identify which TypeScript features used in `apps/app` are incompatible with native type-stripping.
+- **Findings**:
+  - **Enums**: 1 usage — `UploadStatus` in `src/server/service/file-uploader/multipart-uploader.ts`. Decision: convert to `const` object.
+  - **Decorators**: None in `apps/app` server code. (slackbot-proxy has heavy decorator use — out of scope)
+  - **Parameter properties**: Not found in server code.
+  - **`import =` syntax**: Not found.
+  - **JSX/TSX**: Server code does not use JSX.
+- **Implications**: With the `enum` → `const` conversion, all `apps/app` server code is compatible with native type-stripping without any flags.
+
+### apps/slackbot-proxy Decorator Blocker
+
+- **Context**: Requirements scope includes both `apps/app` and `apps/slackbot-proxy`.
+- **Findings**:
+  - slackbot-proxy uses `@tsed/*` framework decorators (`@Service`, `@Controller`, `@Get`, `@Post`, etc.) and TypeORM decorators (`@Entity`, `@Column`, etc.)
+  - Node.js 24 does not support decorators, even with `--experimental-transform-types`
+  - `@swc-node/register` (used by `apps/pdf-converter`) supports decorators and could replace ts-node for slackbot-proxy
+- **Implications**: slackbot-proxy migration is deferred to separate work. Phase 1 focuses on `apps/app` only.
+
+## Architecture Pattern Evaluation
+
+| Option | Description | Strengths | Risks / Limitations | Notes |
+|--------|-------------|-----------|---------------------|-------|
+| Keep tsconfig-paths/register | Continue using `-r tsconfig-paths/register` without ts-node | Zero import changes, proven CJS hook, minimal risk | Not officially validated with native TS (but mechanism is sound) | Recommended for Phase 1 |
+| Node.js subpath imports | Replace `~/`/`^/` with `#` prefixed imports | Native Node.js mechanism, no runtime deps | Massive refactor (hundreds of imports), `#` prefix ergonomics | Not recommended |
+| Custom ESM loader | Write `--import` loader hook for path resolution | Full control, ESM compatible | Maintenance burden, non-standard | Overkill for CJS server |
+
+## Design Decisions
+
+### Decision: Keep tsconfig-paths/register for path alias resolution
+- **Context**: `~/` and `^/` path aliases used extensively throughout server code
+- **Alternatives Considered**:
+  1. Node.js subpath imports — requires `#` prefix and massive import refactor
+  2. Custom loader hook — maintenance overhead
+  3. Keep tsconfig-paths/register — no code changes needed
+- **Selected Approach**: Keep `tsconfig-paths/register` loaded via `-r` flag
+- **Rationale**: Mechanism is CJS `Module._resolveFilename` patching, independent of ts-node. Proven pattern, zero import changes.
+- **Trade-offs**: External dependency remains, but it's lightweight (3 deps) and well-maintained
+- **Follow-up**: Validate with smoke test during implementation
+
+### Decision: Rename all .js → .ts in apps/app/src/server/
+- **Context**: 52 of 64 `.js` files use ESM `import` syntax that ts-node was transpiling. Remaining 12 are pure CJS.
+- **Alternatives Considered**:
+  1. Convert only 52 ESM/mixed files — leaves inconsistency
+  2. Convert all 64 to `.ts` — uniform codebase
+  3. Add `"type": "module"` to package.json — massive cascading changes
+- **Selected Approach**: Rename all 64 files to `.ts`
+- **Rationale**: Simplest approach. ESM `import` + `module.exports` is valid TypeScript. Pure CJS `require()` is also valid TypeScript. No code changes needed inside files — just rename.
+- **Trade-offs**: Git history shows rename (mitigated by `git mv`). 12 pure CJS files didn't strictly need it.
+- **Follow-up**: Verify all imports referencing these files resolve correctly after rename
+
+### Decision: Convert enum UploadStatus to const object
+- **Context**: Single `enum` usage blocks native type-stripping without `--experimental-transform-types`
+- **Selected Approach**: Replace `enum UploadStatus { ... }` with `const UploadStatus = { ... } as const` + type union
+- **Rationale**: Avoids experimental flag dependency. Functional behavior identical.
+
+### Decision: Defer apps/slackbot-proxy to separate work
+- **Context**: Decorator usage makes native type-stripping impossible
+- **Selected Approach**: Phase 1 covers `apps/app` only. slackbot-proxy keeps ts-node or migrates to `@swc-node/register` in a separate effort.
+- **Rationale**: Unblocks the primary goal (Next.js hook conflict resolution) without being held up by an unrelated constraint.
+
+## Risks & Mitigations
+- `tsconfig-paths/register` untested with native TS → Mitigated by smoke test in CI
+- 64 file renames carry git blame disruption → Mitigated by `git mv` and separate rename commit
+- Mixed ESM/CJS patterns may have edge cases → Mitigated by existing test suite coverage
+- ts-node remains in root `devDependencies` for slackbot-proxy → Acceptable temporary state
+
+## References
+- [tsconfig-paths GitHub](https://github.com/dividab/tsconfig-paths) — standalone CJS usage documentation
+- [Node.js TypeScript docs](https://nodejs.org/api/typescript.html) — native type-stripping limitations
+- [Node.js Running TypeScript Natively](https://nodejs.org/en/learn/typescript/run-natively) — official guide
+- [Amaro (Node.js TS engine)](https://github.com/nodejs/amaro) — SWC-based type erasure

+ 22 - 0
.kiro/specs/remove-ts-node/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "remove-ts-node",
+  "created_at": "2026-03-02T00:00:00.000Z",
+  "updated_at": "2026-03-02T04:00:00.000Z",
+  "language": "en",
+  "phase": "tasks-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": true,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}

+ 84 - 0
.kiro/specs/remove-ts-node/tasks.md

@@ -0,0 +1,84 @@
+# Implementation Plan
+
+- [ ] 1. Convert enum UploadStatus to const object
+- [ ] 1.1 (P) Replace the enum declaration with a const object and type union in the multipart uploader module
+  - Change `enum UploadStatus { BEFORE_INIT, IN_PROGRESS, COMPLETED, ABORTED }` to a `const` object with `as const` and a corresponding type alias
+  - Preserve numeric values (0, 1, 2, 3) to maintain identical runtime behavior
+  - Verify all consumers use named member access (`UploadStatus.BEFORE_INIT`) — no reverse numeric lookups
+  - _Requirements: 1.1, 1.2, 1.3_
+  - _Contracts: EnumToConstConversion Service_
+
+- [ ] 1.2 (P) Run existing unit tests for the multipart uploader to confirm the const conversion is behavior-preserving
+  - Execute the multipart-uploader spec covering all upload status transitions and error cases
+  - Confirm GCS and AWS multipart uploader subclasses pass without modification
+  - _Requirements: 8.2_
+
+- [ ] 2. Rename server-side .js files to .ts
+- [ ] 2.1 Rename all 64 `.js` files in `apps/app/src/server/` to `.ts` using `git mv`
+  - Rename the 52 files that use ESM `import` syntax (mandatory — these fail without ts-node transpilation)
+  - Rename the 12 pure CJS files for consistency (recommended — `require`/`module.exports` is valid TypeScript)
+  - Use `git mv` for each file to preserve git blame history
+  - No content changes inside any file — ESM `import` + `module.exports` is valid TypeScript with `esModuleInterop: true`
+  - Callers import these files via path aliases (`~/`, `^/`) or relative paths without extensions, so no caller modifications are needed
+  - _Requirements: 4.1, 4.2, 4.3_
+
+- [ ] 2.2 Verify typecheck and lint pass after renames
+  - Run `turbo run lint:typecheck --filter @growi/app` to confirm TypeScript accepts the renamed files
+  - Run `turbo run lint:biome --filter @growi/app` to confirm no lint regressions
+  - Fix any type errors that surface from stricter checking on newly-typed files (if any)
+  - _Requirements: 8.3_
+
+- [ ] 3. Update package.json scripts to use native Node.js TypeScript execution
+- [ ] 3.1 Replace the `ts-node` composite script with `node-dev` in `apps/app/package.json`
+  - Rename script key from `ts-node` to `node-dev`
+  - Change value from `node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config` to `node -r tsconfig-paths/register -r dotenv-flow/config`
+  - Update all 4 callers: `dev`, `launch-dev:ci`, `dev:migrate-mongo`, `repl` — change `pnpm run ts-node` to `pnpm run node-dev` (and `npm run ts-node` to `pnpm run node-dev` in `repl`)
+  - Verify `dotenv-flow/config` remains in the preload chain for environment variable loading
+  - Verify `tsconfig-paths/register` remains in the preload chain for path alias resolution
+  - _Requirements: 1.1, 1.2, 1.3, 1.6, 2.1, 2.2, 2.3, 2.4, 5.1, 5.2_
+  - _Contracts: PackageJsonScripts, PathAliasResolution_
+
+- [ ] 4. Remove the Next.js hook conflict workaround
+- [ ] 4.1 (P) Delete the `require.extensions['.ts']` save/restore block in the server startup
+  - Remove the code that saves `require.extensions['.ts']` before `nextApp.prepare()` and restores it afterward
+  - Remove the associated comments explaining the ts-node hook conflict
+  - Simplify the Next.js setup to just `this.nextApp = next({ dev }); await this.nextApp.prepare();`
+  - Node.js 24 native type-stripping uses an internal mechanism that Next.js cannot interfere with
+  - _Requirements: 3.1, 3.2, 3.3_
+  - _Contracts: HookWorkaroundRemoval_
+
+- [ ] 5. Clean up ts-node configuration
+- [ ] 5.1 (P) Remove the `ts-node` section from `apps/app/tsconfig.json`
+  - Delete the `"ts-node": { "transpileOnly": true, "swc": true, "compilerOptions": { "module": "CommonJS", "moduleResolution": "Node" } }` block
+  - The `module: "CommonJS"` and `moduleResolution: "Node"` overrides were ts-node-specific; the base config (`module: "ESNext"`, `moduleResolution: "Bundler"`) remains correct for the build pipeline
+  - `apps/slackbot-proxy/tsconfig.json` is out of scope (deferred)
+  - _Requirements: 1.4, 1.5_
+  - _Contracts: TsconfigCleanup_
+
+- [ ] 6. Validate the complete migration
+- [ ] 6.1 Run the full test suite to confirm no regressions
+  - Execute `turbo run test --filter @growi/app` to run all unit and integration tests
+  - Verify all tests pass without modification (path aliases, imports, and module resolution unchanged)
+  - _Requirements: 8.2_
+
+- [ ] 6.2 Run typecheck and lint to confirm build integrity
+  - Execute `turbo run lint:typecheck --filter @growi/app`
+  - Execute `turbo run lint:biome --filter @growi/app`
+  - _Requirements: 8.3_
+
+- [ ] 6.3 Verify dev server launch in CI mode
+  - Execute `pnpm run launch-dev:ci` and confirm the server starts and responds to health checks
+  - Confirms path alias resolution, dotenv-flow loading, and Next.js startup all work without ts-node
+  - _Requirements: 8.1, 2.1, 2.2, 2.3, 2.4, 5.1, 5.2, 6.1, 6.2_
+
+- [ ] 6.4 Verify production build is unaffected
+  - Execute `turbo run build --filter @growi/app` to confirm the production build succeeds
+  - Production build uses `tsc` and `next build`, which are independent of the dev runtime changes
+  - _Requirements: 8.4_
+
+## Deferred Requirements
+
+The following requirements are intentionally deferred due to the `apps/slackbot-proxy` decorator blocker:
+- **7.1** (Remove ts-node from root devDependencies): ts-node remains in root for slackbot-proxy
+- **7.2** (Evaluate tsconfig-paths removal): tsconfig-paths is actively used by the new `node-dev` script
+- **7.3** (Evaluate @swc/core removal): @swc/core is used by VSCode debug config and pdf-converter

+ 0 - 12
apps/app/src/models/cdn-resource.js

@@ -1,12 +0,0 @@
-/**
- * Value Object
- */
-class CdnResource {
-  constructor(name, url, outDir) {
-    this.name = name;
-    this.url = url;
-    this.outDir = outDir;
-  }
-}
-
-module.exports = CdnResource;