Jelajahi Sumber

Merge pull request #10920 from growilabs/support/upgrade-fixed-packages

support: Upgrade version-pinned packages and replace escape-string-regexp with RegExp.escape()
mergify[bot] 1 Minggu lalu
induk
melakukan
5a40fc0d23

+ 262 - 0
.kiro/specs/upgrade-fixed-packages/design.md

@@ -0,0 +1,262 @@
+# Design Document: upgrade-fixed-packages
+
+## Overview
+
+**Purpose**: This feature audits and upgrades version-pinned packages in `apps/app/package.json` that were frozen due to upstream bugs, ESM-only migrations, or licensing constraints. The build environment has shifted from webpack to Turbopack, and the runtime now targets Node.js 24 with stable `require(esm)` support, invalidating several original pinning reasons.
+
+**Users**: Maintainers and developers benefit from up-to-date dependencies with bug fixes, security patches, and reduced technical debt.
+
+**Impact**: Modifies `apps/app/package.json` dependency versions and comment blocks; touches source files where `escape-string-regexp` is replaced by native `RegExp.escape()`.
+
+### Goals
+- Verify each pinning reason against current upstream status
+- Upgrade packages where the original constraint no longer applies
+- Replace `escape-string-regexp` with native `RegExp.escape()` (Node.js 24)
+- Update or remove comment blocks to reflect current state
+- Produce audit documentation for future reference
+
+### Non-Goals
+- Replacing handsontable with an alternative library (license constraint remains; replacement is a separate initiative)
+- Upgrading `@keycloak/keycloak-admin-client` to v19+ (significant API breaking changes; deferred to separate task)
+- Major version upgrades of unrelated packages
+- Modifying the build pipeline or Turbopack configuration
+
+## Architecture
+
+This is a dependency maintenance task, not a feature implementation. No new components or architectural changes are introduced.
+
+### Existing Architecture Analysis
+
+The pinned packages fall into distinct categories by their usage context:
+
+| Category | Packages | Build Context |
+|----------|----------|---------------|
+| Server-only (tsc → CJS) | `escape-string-regexp`, `@aws-sdk/*`, `@keycloak/*` | Express server compiled by tsc |
+| Client-only (Turbopack) | `string-width` (via @growi/editor), `bootstrap` | Bundled by Turbopack/Vite |
+| Client + SSR | `next-themes` | Turbopack + SSR rendering |
+| License-pinned | `handsontable`, `@handsontable/react` | Client-only |
+
+Key enabler: Node.js ^24 provides stable `require(esm)` support, removing the fundamental CJS/ESM incompatibility that caused several pins.
+
+### Technology Stack
+
+| Layer | Choice / Version | Role in Feature | Notes |
+|-------|------------------|-----------------|-------|
+| Runtime | Node.js ^24 | Enables `require(esm)` and `RegExp.escape()` | ES2026 Stage 4 features available |
+| Build (client) | Turbopack (Next.js 16) | Bundles ESM-only packages without issues | No changes needed |
+| Build (server) | tsc (CommonJS output) | `require(esm)` handles ESM-only imports | Node.js 24 native support |
+| Package manager | pnpm v10 | Manages dependency resolution | No changes needed |
+
+## System Flows
+
+### Upgrade Verification Flow
+
+```mermaid
+flowchart TD
+    Start[Select package to upgrade] --> Update[Update version in package.json]
+    Update --> Install[pnpm install]
+    Install --> Build{turbo run build}
+    Build -->|Pass| Lint{turbo run lint}
+    Build -->|Fail| Revert[Revert package change]
+    Lint -->|Pass| Test{turbo run test}
+    Lint -->|Fail| Revert
+    Test -->|Pass| Verify[Verify .next/node_modules symlinks]
+    Test -->|Fail| Revert
+    Verify --> Next[Proceed to next package]
+    Revert --> Document[Document failure reason]
+    Document --> Next
+```
+
+Each package is upgraded and verified independently. Failures are isolated and reverted without affecting other upgrades.
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Action |
+|-------------|---------|------------|--------|
+| 1.1 | Bootstrap bug investigation | PackageAudit | Verify #39798 fixed in v5.3.4 |
+| 1.2 | next-themes issue investigation | PackageAudit | Verify #122 resolved; check v0.4.x compatibility |
+| 1.3 | @aws-sdk constraint verification | PackageAudit | Confirm mongodb constraint is on different package |
+| 1.4 | Document investigation results | AuditReport | Summary table in research.md |
+| 2.1 | ESM compatibility per package | PackageAudit | Assess escape-string-regexp, string-width, @keycloak |
+| 2.2 | Server build ESM support | PackageAudit | Verify Node.js 24 require(esm) for server context |
+| 2.3 | Client build ESM support | PackageAudit | Confirm Turbopack handles ESM-only packages |
+| 2.4 | Compatibility matrix | AuditReport | Table in research.md |
+| 3.1 | Handsontable license check | PackageAudit | Confirm v7+ still non-MIT |
+| 3.2 | Document pinning requirement | AuditReport | Note in audit summary |
+| 4.1 | Update package.json versions and comments | UpgradeExecution | Modify versions and comment blocks |
+| 4.2 | Build verification | UpgradeExecution | `turbo run build --filter @growi/app` |
+| 4.3 | Lint verification | UpgradeExecution | `turbo run lint --filter @growi/app` |
+| 4.4 | Test verification | UpgradeExecution | `turbo run test --filter @growi/app` |
+| 4.5 | Revert on failure | UpgradeExecution | Git revert per package |
+| 4.6 | Update comment blocks | UpgradeExecution | Remove or update comments |
+| 5.1 | Audit summary table | AuditReport | Final summary with decisions |
+| 5.2 | Document continued pinning | AuditReport | Reasons for remaining pins |
+| 5.3 | Document upgrade rationale | AuditReport | What changed upstream |
+
+## Components and Interfaces
+
+| Component | Domain | Intent | Req Coverage | Key Dependencies |
+|-----------|--------|--------|--------------|------------------|
+| PackageAudit | Investigation | Research upstream status for each pinned package | 1.1–1.4, 2.1–2.4, 3.1–3.2 | GitHub issues, npm registry |
+| UpgradeExecution | Implementation | Apply version changes and verify build | 4.1–4.6 | pnpm, turbo, tsc |
+| SourceMigration | Implementation | Replace escape-string-regexp with RegExp.escape() | 4.1 | 9 source files |
+| AuditReport | Documentation | Produce summary of all decisions | 5.1–5.3 | research.md |
+
+### Investigation Layer
+
+#### PackageAudit
+
+| Field | Detail |
+|-------|--------|
+| Intent | Investigate upstream status of each pinned package and determine upgrade feasibility |
+| Requirements | 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2 |
+
+**Responsibilities & Constraints**
+- Check upstream issue trackers for bug fix status
+- Verify ESM compatibility against Node.js 24 `require(esm)` and Turbopack
+- Confirm license status for handsontable
+- Produce actionable recommendation per package
+
+**Audit Decision Matrix**
+
+| Package | Current | Action | Target | Risk | Rationale |
+|---------|---------|--------|--------|------|-----------|
+| `bootstrap` | `=5.3.2` | Upgrade | `^5.3.4` | Low | Bug #39798 fixed in v5.3.4 |
+| `next-themes` | `^0.2.1` | Upgrade | `^0.4.4` | Medium | Original issue was misattributed; v0.4.x works with Pages Router |
+| `escape-string-regexp` | `^4.0.0` | Replace | Remove dep | Low | Native `RegExp.escape()` in Node.js 24 |
+| `string-width` | `=4.2.2` | Upgrade | `^7.0.0` | Low | Used only in ESM context (@growi/editor) |
+| `@aws-sdk/client-s3` | `3.454.0` | Relax | `^3.454.0` | Low | Pinning comment was misleading |
+| `@aws-sdk/s3-request-presigner` | `3.454.0` | Relax | `^3.454.0` | Low | Same as above |
+| `@keycloak/keycloak-admin-client` | `^18.0.0` | Defer | No change | N/A | API breaking changes; separate task |
+| `handsontable` | `=6.2.2` | Keep | No change | N/A | License constraint (non-MIT since v7) |
+| `@handsontable/react` | `=2.1.0` | Keep | No change | N/A | Requires handsontable >= 7 |
+
+### Implementation Layer
+
+#### UpgradeExecution
+
+| Field | Detail |
+|-------|--------|
+| Intent | Apply version changes incrementally with build verification |
+| Requirements | 4.1, 4.2, 4.3, 4.4, 4.5, 4.6 |
+
+**Responsibilities & Constraints**
+- Upgrade one package at a time to isolate failures
+- Run full verification suite (build, lint, test) after each change
+- Revert and document any package that causes failures
+- Update `// comments for dependencies` block to reflect new state
+
+**Upgrade Order** (lowest risk first):
+1. `@aws-sdk/*` — relax version range (no code changes)
+2. `string-width` — upgrade in @growi/editor (isolated ESM package)
+3. `bootstrap` — upgrade to ^5.3.4 (verify SCSS compilation)
+4. `escape-string-regexp` → `RegExp.escape()` — source code changes across 9 files
+5. `next-themes` — upgrade to ^0.4.x (review API changes across 12 files)
+
+**Implementation Notes**
+- After each upgrade, verify `.next/node_modules/` symlinks for Turbopack externalisation compliance (per `package-dependencies` rule)
+- For bootstrap: run `pnpm run pre:styles-commons` and `pnpm run pre:styles-components` to verify SCSS compilation
+- For next-themes: review v0.3.0 and v0.4.0 changelogs for breaking API changes before modifying code
+
+#### SourceMigration
+
+| Field | Detail |
+|-------|--------|
+| Intent | Replace all `escape-string-regexp` usage with native `RegExp.escape()` |
+| Requirements | 4.1 |
+
+**Files to Modify**:
+
+`apps/app/src/` (6 files):
+- `server/models/page.ts`
+- `server/service/page/index.ts`
+- `server/service/page-grant.ts`
+- `server/routes/apiv3/users.js`
+- `server/models/obsolete-page.js`
+- `features/openai/server/services/openai.ts`
+
+`packages/` (3 files):
+- `packages/core/src/utils/page-path-utils/` (2 files)
+- `packages/remark-lsx/src/server/routes/list-pages/index.ts`
+
+**Migration Pattern**:
+```typescript
+// Before
+import escapeStringRegexp from 'escape-string-regexp';
+const pattern = new RegExp(escapeStringRegexp(input));
+
+// After
+const pattern = new RegExp(RegExp.escape(input));
+```
+
+**Implementation Notes**
+- Remove `escape-string-regexp` from `apps/app/package.json` dependencies after migration
+- Remove from `packages/core/package.json` and `packages/remark-lsx/package.json` if listed
+- Verify `RegExp.escape()` TypeScript types are available (may need `@types/node` update or lib config)
+
+### Documentation Layer
+
+#### AuditReport
+
+| Field | Detail |
+|-------|--------|
+| Intent | Document all audit decisions for future maintainers |
+| Requirements | 5.1, 5.2, 5.3 |
+
+**Deliverables**:
+- Updated `// comments for dependencies` in package.json (only retained pins with current reasons)
+- Updated `// comments for defDependencies` (handsontable entries unchanged)
+- Summary in research.md with final decision per package
+
+**Updated Comment Blocks** (target state):
+
+```json
+{
+  "// comments for dependencies": {
+    "@keycloak/keycloak-admin-client": "19.0.0 or above exports only ESM. API breaking changes require separate migration effort.",
+    "next-themes": "(if upgrade fails) Document specific failure reason here"
+  },
+  "// comments for defDependencies": {
+    "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
+    "handsontable": "v7.0.0 or above is no longer MIT license."
+  }
+}
+```
+
+Note: The exact final state depends on which upgrades succeed. If all planned upgrades pass, only `@keycloak` and `handsontable` entries remain.
+
+## Testing Strategy
+
+### Build Verification (per package)
+- `turbo run build --filter @growi/app` — Turbopack client build + tsc server build
+- `ls apps/app/.next/node_modules/ | grep <package>` — Externalisation check
+- `pnpm run pre:styles-commons` — SCSS compilation (bootstrap only)
+
+### Lint Verification (per package)
+- `turbo run lint --filter @growi/app` — TypeScript type check + Biome
+
+### Unit/Integration Tests (per package)
+- `turbo run test --filter @growi/app` — Full test suite
+- For `RegExp.escape()` migration: run tests for page model, page service, page-grant service specifically
+
+### Regression Verification (final)
+- Full build + lint + test after all upgrades applied together
+- Verify `.next/node_modules/` symlink integrity via `check-next-symlinks.sh` (if available locally)
+
+## Migration Strategy
+
+```mermaid
+flowchart LR
+    Phase1[Phase 1: Low Risk] --> Phase2[Phase 2: Medium Risk]
+    Phase1 --> P1a[aws-sdk relax range]
+    Phase1 --> P1b[string-width upgrade]
+    Phase2 --> P2a[bootstrap upgrade]
+    Phase2 --> P2b[escape-string-regexp replace]
+    Phase2 --> P2c[next-themes upgrade]
+```
+
+- **Phase 1** (low risk): @aws-sdk range relaxation, string-width upgrade — minimal code changes
+- **Phase 2** (medium risk): bootstrap, escape-string-regexp replacement, next-themes — requires code review and/or source changes
+- Each upgrade is independently revertible
+- Deferred: @keycloak (high risk, separate task)
+- No change: handsontable (license constraint)

+ 75 - 0
.kiro/specs/upgrade-fixed-packages/requirements.md

@@ -0,0 +1,75 @@
+# Requirements Document
+
+## Introduction
+
+The `apps/app/package.json` file contains several packages whose versions are intentionally pinned due to ESM-only upgrades, upstream bugs, or licensing concerns. These pinning reasons were documented in `// comments for dependencies` and `// comments for defDependencies` comment blocks. Since the build environment has significantly changed (webpack → Turbopack), and upstream issues may have been resolved, a systematic audit is needed to determine which packages can now be safely upgraded.
+
+### Pinned Packages Inventory
+
+| # | Package | Current Version | Pinning Reason |
+|---|---------|----------------|----------------|
+| 1 | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` | `3.454.0` | Fix version above 3.186.0 required by mongodb@4.16.0 |
+| 2 | `@keycloak/keycloak-admin-client` | `^18.0.0` | 19.0.0+ exports only ESM |
+| 3 | `bootstrap` | `=5.3.2` | v5.3.3 has a bug (twbs/bootstrap#39798) |
+| 4 | `escape-string-regexp` | `^4.0.0` | 5.0.0+ exports only ESM |
+| 5 | `next-themes` | `^0.2.1` | 0.3.0 causes type error (pacocoursey/next-themes#122) |
+| 6 | `string-width` | `=4.2.2` | 5.0.0+ exports only ESM |
+| 7 | `@handsontable/react` | `=2.1.0` | v3 requires handsontable >= 7.0.0 |
+| 8 | `handsontable` | `=6.2.2` | v7.0.0+ is no longer MIT license |
+
+## Requirements
+
+### Requirement 1: Upstream Bug and Issue Investigation
+
+**Objective:** As a maintainer, I want to verify whether upstream bugs and issues that originally caused version pinning have been resolved, so that I can make informed upgrade decisions.
+
+#### Acceptance Criteria
+
+1. When investigating the bootstrap pinning, the audit process shall check the current status of https://github.com/twbs/bootstrap/issues/39798 and determine whether v5.3.3+ has fixed the reported bug.
+2. When investigating the next-themes pinning, the audit process shall check the current status of https://github.com/pacocoursey/next-themes/issues/122 and determine whether v0.3.0+ has resolved the type error.
+3. When investigating the @aws-sdk pinning, the audit process shall verify whether the mongodb version used in GROWI still requires the `>=3.186.0` constraint and whether the latest @aws-sdk versions are compatible.
+4. The audit process shall document the investigation result for each package, including: current upstream status, whether the original issue is resolved, and the recommended action (upgrade/keep/replace).
+
+### Requirement 2: ESM-Only Package Compatibility Assessment
+
+**Objective:** As a maintainer, I want to assess whether ESM-only versions of pinned packages are now compatible with the current Turbopack-based build environment, so that outdated CJS-only constraints can be removed.
+
+#### Acceptance Criteria
+
+1. When assessing ESM compatibility, the audit process shall evaluate each ESM-pinned package (`escape-string-regexp`, `string-width`, `@keycloak/keycloak-admin-client`) against the current build pipeline (Turbopack for client, tsc for server).
+2. When a package is used in server-side code (transpiled via tsc with `tsconfig.build.server.json`), the audit process shall verify whether the server build output format (CJS or ESM) supports importing ESM-only packages.
+3. When a package is used only in client-side code (bundled via Turbopack), the audit process shall confirm that Turbopack can resolve ESM-only packages without issues.
+4. The audit process shall produce a compatibility matrix showing each ESM-pinned package, its usage context (server/client/both), and whether upgrading to the ESM-only version is feasible.
+
+### Requirement 3: License Compliance Verification
+
+**Objective:** As a maintainer, I want to confirm that the handsontable/`@handsontable/react` licensing situation has not changed, so that I can determine whether these packages must remain pinned or can be replaced.
+
+#### Acceptance Criteria
+
+1. When evaluating handsontable, the audit process shall verify the current license of handsontable v7.0.0+ and confirm whether it remains non-MIT.
+2. If handsontable v7.0.0+ is still non-MIT, the audit process shall document that `handsontable` (`=6.2.2`) and `@handsontable/react` (`=2.1.0`) must remain pinned or an alternative library must be identified.
+3. If a MIT-licensed alternative to handsontable exists, the audit process shall note it as a potential replacement candidate (out of scope for this spec but documented for future work).
+
+### Requirement 4: Safe Upgrade Execution
+
+**Objective:** As a maintainer, I want to upgrade packages that are confirmed safe to update, so that the project benefits from bug fixes, security patches, and new features.
+
+#### Acceptance Criteria
+
+1. When upgrading a pinned package, the upgrade process shall update the version specifier in `apps/app/package.json` and remove or update the corresponding entry in the `// comments for dependencies` or `// comments for defDependencies` block.
+2. When a package is upgraded, the upgrade process shall verify that `turbo run build --filter @growi/app` completes successfully.
+3. When a package is upgraded, the upgrade process shall verify that `turbo run lint --filter @growi/app` completes without new errors.
+4. When a package is upgraded, the upgrade process shall verify that `turbo run test --filter @growi/app` passes without new failures.
+5. If a package upgrade causes build, lint, or test failures, the upgrade process shall revert that specific package change and document the failure reason.
+6. When all upgrades are complete, the `// comments for dependencies` and `// comments for defDependencies` blocks shall accurately reflect only the packages that remain pinned, with updated reasons if applicable.
+
+### Requirement 5: Audit Documentation
+
+**Objective:** As a maintainer, I want a clear record of the audit results, so that future maintainers understand which packages were evaluated and why decisions were made.
+
+#### Acceptance Criteria
+
+1. The audit process shall produce a summary table documenting each pinned package with: package name, previous version, new version (or "unchanged"), and rationale for the decision.
+2. When a package remains pinned, the documentation shall include the verified reason for continued pinning.
+3. When a package is upgraded, the documentation shall note what changed upstream that made the upgrade possible.

+ 183 - 0
.kiro/specs/upgrade-fixed-packages/research.md

@@ -0,0 +1,183 @@
+# Research & Design Decisions
+
+---
+**Purpose**: Capture discovery findings for the pinned package audit and upgrade initiative.
+**Usage**: Inform design.md decisions; provide evidence for future maintainers.
+---
+
+## Summary
+- **Feature**: `upgrade-fixed-packages`
+- **Discovery Scope**: Extension (auditing existing dependency constraints)
+- **Key Findings**:
+  - Bootstrap bug (#39798) fixed in v5.3.4 — safe to upgrade to latest 5.3.x
+  - next-themes original issue (#122) was resolved long ago; upgrade to v0.4.x feasible but has Next.js 16 `cacheComponents` caveat
+  - Node.js ^24 enables stable `require(esm)`, unlocking ESM-only package upgrades for server code
+  - `escape-string-regexp` can be replaced entirely by native `RegExp.escape()` (ES2026, Node.js 24)
+  - handsontable license situation unchanged — must remain pinned at 6.2.2
+  - @aws-sdk pinning comment is misleading; packages can be freely upgraded
+
+## Research Log
+
+### Bootstrap v5.3.3 Bug (#39798)
+- **Context**: bootstrap pinned at `=5.3.2` due to modal header regression in v5.3.3
+- **Sources Consulted**: https://github.com/twbs/bootstrap/issues/39798, https://github.com/twbs/bootstrap/pull/41336
+- **Findings**:
+  - Issue CLOSED on 2025-04-03
+  - Fixed in v5.3.4 via PR #41336 (Fix modal and offcanvas header collapse)
+  - Bug: `.modal-header` lost `justify-content: space-between`, causing content collapse
+  - Latest stable: v5.3.8 (August 2025)
+- **Implications**: Safe to upgrade from `=5.3.2` to `^5.3.4`. Skip v5.3.3 entirely. Recommend `^5.3.4` or pin to latest `=5.3.8`.
+
+### next-themes Type Error (#122)
+- **Context**: next-themes pinned at `^0.2.1` due to reported type error in v0.3.0
+- **Sources Consulted**: https://github.com/pacocoursey/next-themes/issues/122, https://github.com/pacocoursey/next-themes/issues/375
+- **Findings**:
+  - Issue #122 CLOSED on 2022-06-02 — was specific to an old beta version (v0.0.13-beta.3), not v0.3.0
+  - The pinning reason was based on incomplete information; v0.2.0+ already had the fix
+  - Latest: v0.4.6 (March 2025). Peers: `react ^16.8 || ^17 || ^18 || ^19`
+  - **Caveat**: Issue #375 reports a bug with Next.js 16's `cacheComponents` feature — stale theme values when cached components reactivate
+  - PR #377 in progress to fix via `useSyncExternalStore`
+  - Without `cacheComponents`, v0.4.6 works fine with Next.js 16
+- **Implications**: Upgrade to v0.4.x is feasible. GROWI uses Pages Router (not App Router), so `cacheComponents` is likely not relevant. Breaking API changes between v0.2 → v0.4 need review. Used in 12 files across apps/app.
+
+### ESM-only Package Compatibility (escape-string-regexp, string-width, @keycloak)
+- **Context**: Three packages pinned to CJS-compatible versions because newer versions are ESM-only
+- **Sources Consulted**: Node.js v22.12.0 release notes (require(esm) enabled by default), TC39 RegExp.escape Stage 4, sindresorhus ESM guidance, npm package pages
+- **Findings**:
+
+  **escape-string-regexp** (^4.0.0):
+  - Used in 6 server-side files + 3 shared package files (all server context)
+  - Node.js 24 has stable `require(esm)` — ESM-only v5 would work
+  - **Better**: `RegExp.escape()` is ES2026 Stage 4, natively available in Node.js 24 (V8 support)
+  - Can eliminate the dependency entirely
+
+  **string-width** (=4.2.2):
+  - Used only in `packages/editor/src/models/markdown-table.js`
+  - `@growi/editor` has `"type": "module"` and builds with Vite (ESM context)
+  - No server-side value imports (only type imports in `sync-ydoc.ts`, erased at compile)
+  - Safe to upgrade to v7.x
+
+  **@keycloak/keycloak-admin-client** (^18.0.0):
+  - Used in 1 server-side file: `features/external-user-group/server/service/keycloak-user-group-sync.ts`
+  - Latest: v26.5.5 (February 2026)
+  - `require(esm)` in Node.js 24 should handle it, but API has significant breaking changes (v18 → v26)
+  - Sub-path exports need verification
+  - Higher risk upgrade — API surface changes expected
+
+- **Implications**: string-width is the easiest upgrade. escape-string-regexp should be replaced by native `RegExp.escape()`. @keycloak requires careful API migration and is higher risk.
+
+### @aws-sdk Pinning Analysis
+- **Context**: @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner pinned at 3.454.0
+- **Sources Consulted**: mongodb package.json, npm registry, GROWI source code
+- **Findings**:
+  - Pinning comment says "required by mongodb@4.16.0" but is misleading
+  - mongodb@4.17.2 has `@aws-sdk/credential-providers: ^3.186.0` as **optional** dependency — a different package
+  - The S3 client packages are used directly by GROWI for file upload (server/service/file-uploader/aws/)
+  - Latest: @aws-sdk/client-s3@3.1014.0 (March 2026) — over 500 versions behind
+  - AWS SDK v3 follows semver; any 3.x should be compatible
+- **Implications**: Remove the misleading comment. Change from exact `3.454.0` to `^3.454.0` or update to latest. Low risk.
+
+### Handsontable License Status
+- **Context**: handsontable pinned at =6.2.2 (last MIT version), @handsontable/react at =2.1.0
+- **Sources Consulted**: handsontable.com/docs/software-license, npm, Hacker News discussion
+- **Findings**:
+  - v7.0.0+ (March 2019) switched from MIT to proprietary license — unchanged as of 2026
+  - Free "Hobby" license exists but restricted to non-commercial personal use
+  - Commercial use requires paid subscription
+  - MIT alternatives: AG Grid Community (most mature), Jspreadsheet CE, Univer (Apache 2.0)
+- **Implications**: Must remain pinned. No action possible without license purchase or library replacement. Library replacement is out of scope for this spec.
+
+## Design Decisions
+
+### Decision: Replace escape-string-regexp with native RegExp.escape()
+- **Context**: escape-string-regexp v5 is ESM-only; used in 9 files across server code
+- **Alternatives Considered**:
+  1. Upgrade to v5 with require(esm) support — works but adds unnecessary dependency
+  2. Replace with native `RegExp.escape()` — zero dependencies, future-proof
+- **Selected Approach**: Replace with `RegExp.escape()`
+- **Rationale**: Node.js 24 supports `RegExp.escape()` natively (ES2026 Stage 4). Eliminates a dependency entirely.
+- **Trade-offs**: Requires touching 9 files, but changes are mechanical (find-and-replace)
+- **Follow-up**: Verify `RegExp.escape()` is available in the project's Node.js 24 target
+
+### Decision: Upgrade string-width directly to v7.x
+- **Context**: Used only in @growi/editor (ESM package, Vite-bundled, client-only)
+- **Selected Approach**: Direct upgrade to latest v7.x
+- **Rationale**: Consumer is already ESM; zero CJS concern
+- **Trade-offs**: None significant; API is stable
+
+### Decision: Upgrade bootstrap to ^5.3.4
+- **Context**: Bug fixed in v5.3.4; latest is 5.3.8
+- **Selected Approach**: Change from `=5.3.2` to `^5.3.4`
+- **Rationale**: Original bug resolved; skip v5.3.3
+- **Trade-offs**: Need to verify GROWI's custom SCSS and modal usage against 5.3.4+ changes
+
+### Decision: Upgrade next-themes to latest 0.4.x
+- **Context**: Original issue was a misunderstanding; latest is v0.4.6
+- **Selected Approach**: Upgrade to `^0.4.4` (or latest)
+- **Rationale**: Issue #122 was specific to old beta, not v0.3.0. GROWI uses Pages Router, so cacheComponents bug is not relevant.
+- **Trade-offs**: Breaking API changes between v0.2 → v0.4 need review. 12 files import from next-themes.
+- **Follow-up**: Review v0.3.0 and v0.4.0 changelogs for breaking changes
+
+### Decision: Relax @aws-sdk version to caret range
+- **Context**: Pinning was based on misleading comment; packages are independent of mongodb constraint
+- **Selected Approach**: Change from `3.454.0` to `^3.454.0`
+- **Rationale**: AWS SDK v3 follows semver; the comment conflated credential-providers with S3 client
+- **Trade-offs**: Low risk. Conservative approach keeps minimum at 3.454.0.
+
+### Decision: Defer @keycloak upgrade (high risk)
+- **Context**: v18 → v26 has significant API breaking changes; only 1 file affected
+- **Selected Approach**: Document as upgradeable but defer to a separate task
+- **Rationale**: API migration requires Keycloak server compatibility testing; out of proportion for a batch upgrade task
+- **Trade-offs**: Remains on old version longer, but isolated to one feature
+
+### Decision: Keep handsontable pinned (license constraint)
+- **Context**: v7+ is proprietary; no free alternative that's drop-in
+- **Selected Approach**: No change. Document for future reference.
+- **Rationale**: License constraint is permanent unless library is replaced entirely
+- **Trade-offs**: None — this is a business/legal decision, not technical
+
+## Risks & Mitigations
+- **Bootstrap SCSS breakage**: v5.3.4+ may have SCSS variable changes → Run `pre:styles-commons` and `pre:styles-components` builds to verify
+- **next-themes API changes**: v0.2 → v0.4 has breaking changes → Review changelog; test all 12 consuming files
+- **RegExp.escape() availability**: Ensure Node.js 24 V8 includes it → Verify with simple runtime test
+- **@aws-sdk transitive dependency changes**: Newer AWS SDK may pull different transitive deps → Monitor bundle size
+- **Build regression**: Any upgrade could break Turbopack build → Follow incremental upgrade strategy with build verification per package
+
+## Future Considerations (Out of Scope)
+
+### transpilePackages cleanup in next.config.ts
+- **Context**: `next.config.ts` defines `getTranspilePackages()` listing 60+ ESM-only packages to force Turbopack to bundle them instead of externalising. The original comment says: "listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM".
+- **Relationship to require(esm)**: `transpilePackages` and `require(esm)` solve different problems. `transpilePackages` prevents Turbopack from externalising packages during SSR; `require(esm)` allows Node.js to load ESM packages via `require()` at runtime. With Node.js 24's stable `require(esm)`, externalised ESM packages *should* load correctly in SSR, meaning some `transpilePackages` entries may become unnecessary.
+- **Why not now**: (1) Turbopack's `esmExternals` handling is still `experimental`; (2) removing entries shifts packages from bundled to externalised, which means they appear in `.next/node_modules/` and must be classified as `dependencies` per the `package-dependencies` rule; (3) 60+ packages need individual verification. This is a separate investigation with a large blast radius.
+- **Recommendation**: Track as a separate task. Test by removing a few low-risk entries (e.g., `bail`, `ccount`, `zwitch`) and checking whether SSR still works with Turbopack externalisation + Node.js 24 `require(esm)`.
+
+## References
+- [Bootstrap issue #39798](https://github.com/twbs/bootstrap/issues/39798) — modal header regression, fixed in v5.3.4
+- [next-themes issue #122](https://github.com/pacocoursey/next-themes/issues/122) — type error, resolved in v0.2.0
+- [next-themes issue #375](https://github.com/pacocoursey/next-themes/issues/375) — Next.js 16 cacheComponents bug
+- [TC39 RegExp.escape() Stage 4](https://socket.dev/blog/tc39-advances-3-proposals-to-stage-4-regexp-escaping-float16array-and-redeclarable-global-eval) — ES2026
+- [Node.js require(esm) stability](https://joyeecheung.github.io/blog/2025/12/30/require-esm-in-node-js-from-experiment-to-stability/) — stable since Node.js 22.12.0
+- [Handsontable license change](https://handsontable.com/docs/javascript-data-grid/software-license/) — proprietary since v7.0.0
+
+## Final Audit Summary (2026-03-23)
+
+| Package | Previous Version | New Version | Action | Rationale |
+|---------|-----------------|-------------|--------|-----------|
+| `@aws-sdk/client-s3` | `3.454.0` | `^3.1014.0` | Upgraded | Pinning comment was misleading; S3 client is independent of mongodb constraint |
+| `@aws-sdk/s3-request-presigner` | `3.454.0` | `^3.1014.0` | Upgraded | Same as above |
+| `bootstrap` | `=5.3.2` | `^5.3.8` | Upgraded | Bug #39798 fixed in v5.3.4; SCSS compilation verified |
+| `escape-string-regexp` | `^4.0.0` | Removed | Replaced | Native `RegExp.escape()` (ES2026, Node.js 24) eliminates the dependency |
+| `string-width` | `=4.2.2` | `^7.0.0` | Upgraded | Used only in @growi/editor (ESM context, Vite-bundled) |
+| `next-themes` | `^0.2.1` | `^0.4.6` | Upgraded | Original issue #122 was misattributed; only change needed: type import path |
+| `@keycloak/keycloak-admin-client` | `^18.0.0` | Unchanged | Deferred | API breaking changes (v18→v26) require separate migration effort |
+| `handsontable` | `=6.2.2` | Unchanged | Kept | v7.0.0+ is proprietary (non-MIT license) |
+| `@handsontable/react` | `=2.1.0` | Unchanged | Kept | Requires handsontable >= 7.0.0 |
+
+### Additional Changes
+
+- Added `RegExp.escape()` TypeScript type declarations in `apps/app/src/@types/`, `packages/core/src/@types/`, and `packages/remark-lsx/src/@types/` (awaiting TypeScript built-in support)
+- Updated `tsconfig.build.client.json` to include `src/@types/**/*.d.ts` for Next.js build compatibility
+- Updated `generate-children-regexp.spec.ts` test expectations for `RegExp.escape()` output (escapes spaces as `\x20`)
+- Removed `escape-string-regexp` from `transpilePackages` in `next.config.ts`
+- Updated `bootstrap` version across 5 packages: apps/app, packages/editor, packages/core-styles, packages/preset-themes, apps/slackbot-proxy
+- Updated `// comments for dependencies` to retain only `@keycloak` entry with updated reason

+ 22 - 0
.kiro/specs/upgrade-fixed-packages/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "upgrade-fixed-packages",
+  "created_at": "2026-03-23T00:00:00Z",
+  "updated_at": "2026-03-23T00:00:00Z",
+  "language": "en",
+  "phase": "implementation-complete",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": true,
+      "approved": true
+    }
+  },
+  "ready_for_implementation": true
+}

+ 89 - 0
.kiro/specs/upgrade-fixed-packages/tasks.md

@@ -0,0 +1,89 @@
+# Implementation Plan
+
+- [x] 1. Pre-implementation verification
+- [x] 1.1 Verify RegExp.escape() availability and TypeScript support
+  - Confirm `RegExp.escape()` is available at runtime in the project's Node.js 24 target
+  - Check whether TypeScript recognizes `RegExp.escape()` — may need `lib` config update or `@types/node` update
+  - If unavailable, fall back to upgrading `escape-string-regexp` to v5 with `require(esm)` instead
+  - _Requirements: 2.2_
+
+- [x] 1.2 Review next-themes v0.3.0 and v0.4.0 breaking API changes
+  - Read changelogs for v0.3.0 and v0.4.0 releases to identify breaking changes
+  - Map breaking changes to the 12 consuming files in apps/app
+  - Determine migration effort and document required code changes
+  - Confirm GROWI's Pages Router usage is unaffected by the cacheComponents bug (issue #375)
+  - _Requirements: 1.2_
+
+- [x] 2. Low-risk package upgrades
+- [x] 2.1 (P) Relax @aws-sdk version range
+  - Change `@aws-sdk/client-s3` from `3.454.0` to `^3.1014.0` in apps/app/package.json
+  - Change `@aws-sdk/s3-request-presigner` from `3.454.0` to `^3.1014.0`
+  - Update the misleading `"@aws-skd/*"` comment to reflect the actual reason or remove it
+  - Run `pnpm install` and verify build with `turbo run build --filter @growi/app`
+  - Run `turbo run test --filter @growi/app` to confirm no regressions
+  - _Requirements: 1.3, 4.1, 4.2, 4.4_
+
+- [x] 2.2 (P) Upgrade string-width in @growi/editor
+  - Update `string-width` from `=4.2.2` to `^7.0.0` in packages/editor/package.json
+  - Verify @growi/editor builds successfully (Vite, ESM context)
+  - Run `turbo run build --filter @growi/app` to confirm downstream build passes
+  - Run `turbo run test --filter @growi/app` to confirm no regressions
+  - Remove the `string-width` comment from apps/app/package.json `// comments for dependencies`
+  - _Requirements: 2.1, 2.3, 4.1, 4.2, 4.4_
+
+- [x] 3. Upgrade bootstrap to ^5.3.8
+  - Change `bootstrap` from `=5.3.2` to `^5.3.8` in apps/app/package.json and all other packages
+  - Run `pnpm install` to resolve the new version
+  - Run `pnpm run pre:styles-commons` and `pnpm run pre:styles-components` to verify SCSS compilation
+  - Run `turbo run build --filter @growi/app` to confirm Turbopack build passes
+  - Run `turbo run lint --filter @growi/app` to check for type or lint errors
+  - Run `turbo run test --filter @growi/app` to confirm no regressions
+  - Visually inspect modal headers if a dev server is available (original bug was modal header layout)
+  - Remove the `bootstrap` comment from `// comments for dependencies`
+  - If build or SCSS fails, revert and document the failure reason
+  - _Requirements: 1.1, 4.1, 4.2, 4.3, 4.4, 4.5_
+
+- [x] 4. Replace escape-string-regexp with native RegExp.escape()
+- [x] 4.1 Migrate all source files from escape-string-regexp to RegExp.escape()
+  - Replace `import escapeStringRegexp from 'escape-string-regexp'` and corresponding calls with `RegExp.escape()` in each file
+  - Files in apps/app/src: page.ts, page/index.ts, page-grant.ts, users.js, obsolete-page.js, openai.ts (6 files)
+  - Files in packages: core/src/utils/page-path-utils (2 files), remark-lsx/src/server/routes/list-pages/index.ts (1 file)
+  - Ensure each replacement preserves the exact same escaping behavior
+  - _Requirements: 4.1_
+
+- [x] 4.2 Remove escape-string-regexp dependency and verify
+  - Remove `escape-string-regexp` from apps/app/package.json dependencies
+  - Remove from packages/core and packages/remark-lsx package.json if listed
+  - Remove the `escape-string-regexp` comment from `// comments for dependencies`
+  - Remove `escape-string-regexp` entry from `transpilePackages` in next.config.ts
+  - Run `pnpm install` to update lockfile
+  - Run `turbo run build --filter @growi/app` to verify build
+  - Run `turbo run lint --filter @growi/app` to verify no type errors
+  - Run `turbo run test --filter @growi/app` to verify no regressions
+  - If RegExp.escape() has TypeScript issues, add type declaration or adjust lib config
+  - _Requirements: 2.1, 2.2, 4.1, 4.2, 4.3, 4.4, 4.5_
+
+- [x] 5. Upgrade next-themes to ^0.4.x
+- [x] 5.1 Update next-themes and adapt consuming code
+  - Change `next-themes` from `^0.2.1` to `^0.4.6` in apps/app/package.json
+  - Apply required API migration changes across the 12 consuming files identified in design
+  - Pay attention to any renamed exports, changed hook signatures, or provider prop changes
+  - Ensure `useTheme()` and `ThemeProvider` usage is compatible with v0.4.x API
+  - _Requirements: 1.2, 4.1_
+
+- [x] 5.2 Verify next-themes upgrade
+  - Run `turbo run build --filter @growi/app` to confirm build passes
+  - Run `turbo run lint --filter @growi/app` to check for type errors (original pinning was about types)
+  - Run `turbo run test --filter @growi/app` to confirm no regressions
+  - Remove the `next-themes` comment from `// comments for dependencies`
+  - If build or type errors occur, investigate whether the issue is the same as #122 or a new problem
+  - If upgrade fails, revert and document the reason; keep the pin with an updated comment
+  - _Requirements: 4.2, 4.3, 4.4, 4.5, 4.6_
+
+- [x] 6. Finalize audit documentation and comment blocks
+  - Verify `// comments for dependencies` block contains only packages that remain pinned (@keycloak if unchanged)
+  - Verify `// comments for defDependencies` block is accurate (handsontable entries unchanged)
+  - Update comment text to reflect current reasons where applicable
+  - Produce a final summary table in research.md documenting: package name, previous version, new version or "unchanged", and rationale
+  - Confirm all requirements are satisfied by reviewing the checklist against actual changes made
+  - _Requirements: 3.1, 3.2, 4.6, 5.1, 5.2, 5.3_

+ 0 - 1
apps/app/next.config.ts

@@ -28,7 +28,6 @@ const getTranspilePackages = (): string[] => {
     'decode-named-character-reference',
     'devlop',
     'fault',
-    'escape-string-regexp',
     'hastscript',
     'html-void-elements',
     'is-absolute-url',

+ 6 - 12
apps/app/package.json

@@ -56,17 +56,12 @@
     "version:premajor": "pnpm version premajor --preid=RC"
   },
   "// comments for dependencies": {
-    "@aws-skd/*": "fix version above 3.186.0 that is required by mongodb@4.16.0",
-    "@keycloak/keycloak-admin-client": "19.0.0 or above exports only ESM.",
-    "bootstrap": "v5.3.3 has a bug. refs: https://github.com/twbs/bootstrap/issues/39798",
-    "escape-string-regexp": "5.0.0 or above exports only ESM",
-    "next-themes": "0.3.0 causes a type error: https://github.com/pacocoursey/next-themes/issues/122",
-    "string-width": "5.0.0 or above exports only ESM."
+    "@keycloak/keycloak-admin-client": "19.0.0 or above exports only ESM. API breaking changes require separate migration effort."
   },
   "dependencies": {
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
-    "@aws-sdk/client-s3": "3.454.0",
-    "@aws-sdk/s3-request-presigner": "3.454.0",
+    "@aws-sdk/client-s3": "^3.1014.0",
+    "@aws-sdk/s3-request-presigner": "^3.1014.0",
     "@azure/identity": "^4.4.1",
     "@azure/openai": "^2.0.0",
     "@azure/storage-blob": "^12.16.0",
@@ -129,7 +124,7 @@
     "axios-retry": "^3.2.4",
     "babel-plugin-superjson-next": "^0.4.2",
     "body-parser": "^1.20.3",
-    "bootstrap": "=5.3.2",
+    "bootstrap": "^5.3.8",
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
@@ -152,7 +147,6 @@
     "dotenv-flow": "^3.2.0",
     "downshift": "^8.2.3",
     "ejs": "^3.1.10",
-    "escape-string-regexp": "^4.0.0",
     "expose-gc": "^1.0.0",
     "express": "^4.20.0",
     "express-bunyan-logger": "^1.3.3",
@@ -205,7 +199,7 @@
     "next": "^16.2.1",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
-    "next-themes": "^0.2.1",
+    "next-themes": "^0.4.6",
     "nocache": "^4.0.0",
     "node-cron": "^3.0.2",
     "nodemailer": "^6.9.15",
@@ -269,7 +263,7 @@
     "sanitize-filename": "^1.6.3",
     "simplebar-react": "^2.3.6",
     "socket.io": "^4.7.5",
-    "string-width": "=4.2.2",
+    "string-width": "^7.0.0",
     "superjson": "^2.2.2",
     "swagger-jsdoc": "^6.2.8",
     "swr": "^2.3.2",

+ 1 - 2
apps/app/src/features/openai/server/services/openai.ts

@@ -13,7 +13,6 @@ import {
 } from '@growi/core';
 import { deepEquals } from '@growi/core/dist/utils';
 import { isGlobPatternPath } from '@growi/core/dist/utils/page-path-utils';
-import escapeStringRegexp from 'escape-string-regexp';
 import createError from 'http-errors';
 import mongoose, { type HydratedDocument, type Types } from 'mongoose';
 import type { OpenAI } from 'openai';
@@ -78,7 +77,7 @@ const convertPathPatternsToRegExp = (
   return pagePathPatterns.map((pagePathPattern) => {
     if (isGlobPatternPath(pagePathPattern)) {
       const trimedPagePathPattern = pagePathPattern.replace('/*', '');
-      const escapedPagePathPattern = escapeStringRegexp(trimedPagePathPattern);
+      const escapedPagePathPattern = RegExp.escape(trimedPagePathPattern);
       // https://regex101.com/r/x5KIZL/1
       return new RegExp(`^${escapedPagePathPattern}($|/)`);
     }

+ 1 - 2
apps/app/src/server/models/obsolete-page.js

@@ -7,7 +7,6 @@ import {
 import { isUserPage } from '@growi/core/dist/utils/page-path-utils';
 import { removeHeadingSlash } from '@growi/core/dist/utils/path-utils';
 import { differenceInYears } from 'date-fns/differenceInYears';
-import escapeStringRegexp from 'escape-string-regexp';
 
 import { Comment } from '~/features/comment/server/models/comment';
 import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
@@ -688,7 +687,7 @@ export const getPageSchema = (crowi) => {
     const regexpList = pathList.map((path) => {
       const pathWithTrailingSlash = pathUtils.addTrailingSlash(path);
       return new RegExp(
-        `^${escapeStringRegexp(pathWithTrailingSlash)}_{1,2}template$`,
+        `^${RegExp.escape(pathWithTrailingSlash)}_{1,2}template$`,
       );
     });
 

+ 7 - 8
apps/app/src/server/models/page.ts

@@ -10,7 +10,6 @@ import {
   normalizePath,
 } from '@growi/core/dist/utils/path-utils';
 import assert from 'assert';
-import escapeStringRegexp from 'escape-string-regexp';
 import type mongoose from 'mongoose';
 import type {
   AnyObject,
@@ -348,7 +347,7 @@ export class PageQueryBuilder {
     const pathNormalized = normalizePath(path);
     const pathWithTrailingSlash = addTrailingSlash(path);
 
-    const startsPattern = escapeStringRegexp(pathWithTrailingSlash);
+    const startsPattern = RegExp.escape(pathWithTrailingSlash);
 
     this.query = this.query.and({
       $or: [
@@ -373,7 +372,7 @@ export class PageQueryBuilder {
 
     const pathWithTrailingSlash = addTrailingSlash(path);
 
-    const startsPattern = escapeStringRegexp(pathWithTrailingSlash);
+    const startsPattern = RegExp.escape(pathWithTrailingSlash);
 
     this.query = this.query.and({ path: new RegExp(`^${startsPattern}`) });
 
@@ -409,7 +408,7 @@ export class PageQueryBuilder {
       return this;
     }
 
-    const startsPattern = escapeStringRegexp(path);
+    const startsPattern = RegExp.escape(path);
 
     this.query = this.query.and({ path: new RegExp(`^${startsPattern}`) });
 
@@ -424,7 +423,7 @@ export class PageQueryBuilder {
       return this;
     }
 
-    const startsPattern = escapeStringRegexp(str);
+    const startsPattern = RegExp.escape(str);
 
     this.query = this.query.and({
       path: new RegExp(`^(?!${startsPattern}).*$`),
@@ -440,7 +439,7 @@ export class PageQueryBuilder {
       return this;
     }
 
-    const startsPattern = escapeStringRegexp(path);
+    const startsPattern = RegExp.escape(path);
 
     this.query = this.query.and({
       path: { $not: new RegExp(`^${startsPattern}(/|$)`) },
@@ -455,7 +454,7 @@ export class PageQueryBuilder {
       return this;
     }
 
-    const match = escapeStringRegexp(str);
+    const match = RegExp.escape(str);
 
     this.query = this.query.and({ path: new RegExp(`^(?=.*${match}).*$`) });
 
@@ -468,7 +467,7 @@ export class PageQueryBuilder {
       return this;
     }
 
-    const match = escapeStringRegexp(str);
+    const match = RegExp.escape(str);
 
     this.query = this.query.and({ path: new RegExp(`^(?!.*${match}).*$`) });
 

+ 1 - 2
apps/app/src/server/routes/apiv3/users.js

@@ -2,7 +2,6 @@ import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
-import escapeStringRegexp from 'escape-string-regexp';
 import express from 'express';
 import { body, query } from 'express-validator';
 import path from 'pathe';
@@ -336,7 +335,7 @@ module.exports = (crowi) => {
 
       // Search from input
       const searchText = req.query.searchText || '';
-      const searchWord = new RegExp(escapeStringRegexp(searchText));
+      const searchWord = new RegExp(RegExp.escape(searchText));
       // Sort
       const { sort, sortOrder } = req.query;
       const sortOutput = {

+ 1 - 5
apps/app/src/server/service/page-grant.ts

@@ -6,7 +6,6 @@ import {
   PageGrant,
 } from '@growi/core';
 import { pagePathUtils, pageUtils, pathUtils } from '@growi/core/dist/utils';
-import escapeStringRegexp from 'escape-string-regexp';
 import mongoose, { type HydratedDocument } from 'mongoose';
 
 import type { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
@@ -590,10 +589,7 @@ class PageGrantService implements IPageGrantService {
     };
 
     const commonCondition = {
-      path: new RegExp(
-        `^${escapeStringRegexp(addTrailingSlash(targetPath))}`,
-        'i',
-      ),
+      path: new RegExp(`^${RegExp.escape(addTrailingSlash(targetPath))}`, 'i'),
       isEmpty: false,
     };
 

+ 9 - 18
apps/app/src/server/service/page/index.ts

@@ -18,7 +18,6 @@ import type {
 } from '@growi/core/dist/interfaces';
 import { PageGrant } from '@growi/core/dist/interfaces';
 import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
-import escapeStringRegexp from 'escape-string-regexp';
 import type EventEmitter from 'events';
 import type { Cursor, HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
@@ -941,7 +940,7 @@ class PageService implements IPageService {
   }
 
   private isRenamingToUnderTarget(fromPath: string, toPath: string): boolean {
-    const pathToTest = escapeStringRegexp(addTrailingSlash(fromPath));
+    const pathToTest = RegExp.escape(addTrailingSlash(fromPath));
     const pathToBeTested = toPath;
 
     return new RegExp(`^${pathToTest}`, 'i').test(pathToBeTested);
@@ -1245,10 +1244,7 @@ class PageService implements IPageService {
     const batchStream = createBatchStream(BULK_REINDEX_SIZE);
 
     const newPagePathPrefix = newPagePathSanitized;
-    const pathRegExp = new RegExp(
-      `^${escapeStringRegexp(targetPage.path)}`,
-      'i',
-    );
+    const pathRegExp = new RegExp(`^${RegExp.escape(targetPage.path)}`, 'i');
 
     const renameDescendants = this.renameDescendants.bind(this);
     const pageEvent = this.pageEvent;
@@ -1304,10 +1300,7 @@ class PageService implements IPageService {
     const batchStream = createBatchStream(BULK_REINDEX_SIZE);
 
     const newPagePathPrefix = newPagePathSanitized;
-    const pathRegExp = new RegExp(
-      `^${escapeStringRegexp(targetPage.path)}`,
-      'i',
-    );
+    const pathRegExp = new RegExp(`^${RegExp.escape(targetPage.path)}`, 'i');
 
     const renameDescendants = this.renameDescendants.bind(this);
     const pageEvent = this.pageEvent;
@@ -1892,7 +1885,7 @@ class PageService implements IPageService {
     const batchStream = createBatchStream(BULK_REINDEX_SIZE);
 
     const newPagePathPrefix = newPagePathSanitized;
-    const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
+    const pathRegExp = new RegExp(`^${RegExp.escape(page.path)}`, 'i');
 
     const duplicateDescendants = this.duplicateDescendants.bind(this);
     const pageEvent = this.pageEvent;
@@ -1948,7 +1941,7 @@ class PageService implements IPageService {
     const batchStream = createBatchStream(BULK_REINDEX_SIZE);
 
     const newPagePathPrefix = newPagePathSanitized;
-    const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
+    const pathRegExp = new RegExp(`^${RegExp.escape(page.path)}`, 'i');
 
     const duplicateDescendants = this.duplicateDescendants.bind(this);
     const pageEvent = this.pageEvent;
@@ -3968,7 +3961,7 @@ class PageService implements IPageService {
     const ancestorPaths = paths.flatMap((p) => collectAncestorPaths(p, []));
     // targets' descendants
     const pathAndRegExpsToNormalize: (RegExp | string)[] = paths.map(
-      (p) => new RegExp(`^${escapeStringRegexp(addTrailingSlash(p))}`, 'i'),
+      (p) => new RegExp(`^${RegExp.escape(addTrailingSlash(p))}`, 'i'),
     );
     // include targets' path
     pathAndRegExpsToNormalize.push(...paths);
@@ -4179,7 +4172,7 @@ class PageService implements IPageService {
           const parentId = parent._id;
 
           // Build filter
-          const parentPathEscaped = escapeStringRegexp(
+          const parentPathEscaped = RegExp.escape(
             parent.path === '/' ? '' : parent.path,
           ); // adjust the path for RegExp
           const filter: any = {
@@ -5148,9 +5141,7 @@ class PageService implements IPageService {
     const wasOnTree = exPage.parent != null || isTopPage(exPage.path);
     const shouldBeOnTree = currentPage.grant !== PageGrant.GRANT_RESTRICTED;
     const isChildrenExist = await Page.count({
-      path: new RegExp(
-        `^${escapeStringRegexp(addTrailingSlash(currentPage.path))}`,
-      ),
+      path: new RegExp(`^${RegExp.escape(addTrailingSlash(currentPage.path))}`),
       parent: { $ne: null },
     });
 
@@ -5282,7 +5273,7 @@ class PageService implements IPageService {
     const shouldBeOnTree = grant !== PageGrant.GRANT_RESTRICTED;
     const isChildrenExist = await Page.count({
       path: new RegExp(
-        `^${escapeStringRegexp(addTrailingSlash(clonedPageData.path))}`,
+        `^${RegExp.escape(addTrailingSlash(clonedPageData.path))}`,
       ),
       parent: { $ne: null },
     });

+ 1 - 1
apps/app/src/stores-universal/use-next-themes.tsx

@@ -1,7 +1,7 @@
 import { ColorScheme } from '@growi/core';
 import { isClient } from '@growi/core/dist/utils';
+import type { ThemeProviderProps, UseThemeProps } from 'next-themes';
 import { ThemeProvider, useTheme } from 'next-themes';
-import type { ThemeProviderProps, UseThemeProps } from 'next-themes/dist/types';
 
 import { useForcedColorScheme } from '~/states/global';
 

+ 1 - 1
apps/app/tsconfig.build.client.json

@@ -1,7 +1,7 @@
 {
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "./tsconfig.json",
-  "include": [".next/types/**/*.ts"],
+  "include": [".next/types/**/*.ts", "src/@types/**/*.d.ts"],
   "compilerOptions": {
     "strict": false,
     "strictNullChecks": true,

+ 2 - 3
apps/slackbot-proxy/package.json

@@ -68,8 +68,7 @@
   },
   "// comments for devDependencies": {
     "@tsed/*": "v6.133.1 causes 'TypeError: Cannot read properties of undefined (reading 'prototype')' with `@Middleware()`",
-    "@tsed/core,exceptions": "force package to local node_modules in tsconfig.json since pnpm reads wrong hoisted tsed version (https://github.com/pnpm/pnpm/issues/7158)",
-    "bootstrap": "v5.3.3 has a bug. refs: https://github.com/twbs/bootstrap/issues/39798"
+    "@tsed/core,exceptions": "force package to local node_modules in tsconfig.json since pnpm reads wrong hoisted tsed version (https://github.com/pnpm/pnpm/issues/7158)"
   },
   "devDependencies": {
     "@popperjs/core": "^2.11.8",
@@ -77,7 +76,7 @@
     "@tsed/exceptions": "=6.43.0",
     "@tsed/json-mapper": "=6.43.0",
     "@types/bunyan": "^1.8.11",
-    "bootstrap": "=5.3.2",
+    "bootstrap": "^5.3.8",
     "browser-bunyan": "^1.6.3",
     "morgan": "^1.10.0"
   }

+ 2 - 4
packages/core-styles/package.json

@@ -17,11 +17,9 @@
     "lint": "npm-run-all -p lint:*"
   },
   "dependencies": {},
-  "// comments for defDependencies": {
-    "bootstrap": "v5.3.3 has a bug. refs: https://github.com/twbs/bootstrap/issues/39798"
-  },
+  "// comments for defDependencies": {},
   "devDependencies": {
-    "bootstrap": "=5.3.2"
+    "bootstrap": "^5.3.8"
   },
   "peerDependencies": {
     "@popperjs/core": "^2.11.8"

+ 2 - 5
packages/core/package.json

@@ -69,12 +69,9 @@
     "lint": "npm-run-all -p lint:*",
     "test": "vitest run --coverage"
   },
-  "// comments for dependencies": {
-    "escape-string-regexp": "5.0.0 or above exports only ESM"
-  },
+  "// comments for dependencies": {},
   "dependencies": {
-    "bson-objectid": "^2.0.4",
-    "escape-string-regexp": "^4.0.0"
+    "bson-objectid": "^2.0.4"
   },
   "devDependencies": {
     "@types/express": "^4",

+ 9 - 0
packages/core/src/index.ts

@@ -1,2 +1,11 @@
 export * from './consts';
 export * from './interfaces';
+
+// Type declaration for RegExp.escape() (ES2026, Stage 4)
+// Available natively in Node.js 24+ (V8 13.x+)
+// Can be removed once TypeScript adds built-in support
+declare global {
+  interface RegExpConstructor {
+    escape(str: string): string;
+  }
+}

+ 2 - 2
packages/core/src/utils/page-path-utils/generate-children-regexp.spec.ts

@@ -18,7 +18,7 @@ describe('generateChildrenRegExp', () => {
     },
     {
       path: '/parent (with brackets)',
-      expected: '^\\/parent \\(with brackets\\)(\\/[^/]+)\\/?$',
+      expected: '^\\/parent\\x20\\(with\\x20brackets\\)(\\/[^/]+)\\/?$',
       validPaths: [
         '/parent (with brackets)/child',
         '/parent (with brackets)/test',
@@ -30,7 +30,7 @@ describe('generateChildrenRegExp', () => {
     },
     {
       path: '/parent[with square]',
-      expected: '^\\/parent\\[with square\\](\\/[^/]+)\\/?$',
+      expected: '^\\/parent\\[with\\x20square\\](\\/[^/]+)\\/?$',
       validPaths: ['/parent[with square]/child', '/parent[with square]/test'],
       invalidPaths: [
         '/parent[with square]',

+ 1 - 3
packages/core/src/utils/page-path-utils/generate-children-regexp.ts

@@ -1,5 +1,3 @@
-import escapeStringRegexp from 'escape-string-regexp';
-
 import { isTopPage } from './is-top-page';
 
 /**
@@ -12,5 +10,5 @@ export const generateChildrenRegExp = (path: string): RegExp => {
 
   // https://regex101.com/r/mrDJrx/1
   // ex. /parent/any_child OR /any_level1
-  return new RegExp(`^${escapeStringRegexp(path)}(\\/[^/]+)\\/?$`);
+  return new RegExp(`^${RegExp.escape(path)}(\\/[^/]+)\\/?$`);
 };

+ 4 - 9
packages/core/src/utils/page-path-utils/index.ts

@@ -1,5 +1,3 @@
-import escapeStringRegexp from 'escape-string-regexp';
-
 import { isValidObjectId } from '../objectid-utils';
 import { addTrailingSlash } from '../path-utils';
 import { isTopPage as _isTopPage } from './is-top-page';
@@ -149,7 +147,7 @@ export const convertToNewAffiliationPath = (
   if (newPath == null) {
     throw new Error('Please input the new page path');
   }
-  const pathRegExp = new RegExp(`^${escapeStringRegexp(oldPath)}`, 'i');
+  const pathRegExp = new RegExp(`^${RegExp.escape(oldPath)}`, 'i');
   return childPath.replace(pathRegExp, newPath);
 };
 
@@ -239,8 +237,8 @@ export const isEitherOfPathAreaOverlap = (
   const path1WithSlash = addTrailingSlash(path1);
   const path2WithSlash = addTrailingSlash(path2);
 
-  const path1Area = new RegExp(`^${escapeStringRegexp(path1WithSlash)}`, 'i');
-  const path2Area = new RegExp(`^${escapeStringRegexp(path2WithSlash)}`, 'i');
+  const path1Area = new RegExp(`^${RegExp.escape(path1WithSlash)}`, 'i');
+  const path2Area = new RegExp(`^${RegExp.escape(path2WithSlash)}`, 'i');
 
   if (path1Area.test(path2) || path2Area.test(path1)) {
     return true;
@@ -266,10 +264,7 @@ export const isPathAreaOverlap = (
 
   const pathWithSlash = addTrailingSlash(pathToTest);
 
-  const pathAreaToTest = new RegExp(
-    `^${escapeStringRegexp(pathWithSlash)}`,
-    'i',
-  );
+  const pathAreaToTest = new RegExp(`^${RegExp.escape(pathWithSlash)}`, 'i');
   if (pathAreaToTest.test(pathToBeTested)) {
     return true;
   }

+ 3 - 5
packages/editor/package.json

@@ -24,9 +24,7 @@
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
-  "// comments for devDependencies": {
-    "string-width": "5.0.0 or above exports only ESM."
-  },
+  "// comments for devDependencies": {},
   "devDependencies": {
     "@codemirror/autocomplete": "^6.18.4",
     "@codemirror/commands": "^6.8.0",
@@ -52,7 +50,7 @@
     "@uiw/codemirror-theme-kimbie": "^4.23.8",
     "@uiw/codemirror-themes": "^4.23.8",
     "@uiw/react-codemirror": "^4.23.8",
-    "bootstrap": "=5.3.2",
+    "bootstrap": "^5.3.8",
     "cm6-theme-basic-light": "^0.2.0",
     "cm6-theme-material-dark": "^0.2.0",
     "cm6-theme-nord": "^0.2.0",
@@ -67,7 +65,7 @@
     "react-hook-form": "^7.45.4",
     "react-toastify": "^9.1.3",
     "reactstrap": "^9.2.2",
-    "string-width": "=4.2.2",
+    "string-width": "^7.0.0",
     "simplebar-react": "^2.3.6",
     "socket.io": "^4.7.5",
     "socket.io-client": "^4.7.5",

+ 1 - 1
packages/preset-themes/package.json

@@ -31,7 +31,7 @@
   "devDependencies": {
     "@growi/core": "workspace:^",
     "@growi/core-styles": "workspace:^",
-    "bootstrap": "=5.3.2",
+    "bootstrap": "^5.3.8",
     "sass": "^1.55.0"
   },
   "peerDependencies": {

+ 1 - 4
packages/remark-lsx/package.json

@@ -29,14 +29,11 @@
     "lint": "run-p lint:*",
     "test": "vitest run --coverage"
   },
-  "// comments for dependencies": {
-    "escape-string-regexp": "5.0.0 or above exports only ESM"
-  },
+  "// comments for dependencies": {},
   "dependencies": {
     "@growi/core": "workspace:^",
     "@growi/remark-growi-directive": "workspace:^",
     "@growi/ui": "workspace:^",
-    "escape-string-regexp": "^4.0.0",
     "express": "^4.20.0",
     "express-validator": "^6.14.0",
     "http-errors": "^2.0.0",

+ 12 - 4
packages/remark-lsx/src/server/routes/list-pages/index.spec.ts

@@ -186,7 +186,9 @@ describe('listPages', () => {
       // setup
       const pagePath = '/parent';
       const optionsFilter = '^child';
-      const expectedRegex = /^\/parent\/child/;
+      const expectedRegex = new RegExp(
+        `^${RegExp.escape('/parent/')}${RegExp.escape('child')}`,
+      );
 
       // when
       addFilterCondition(queryMock, pagePath, optionsFilter);
@@ -199,7 +201,9 @@ describe('listPages', () => {
       // setup
       const pagePath = '/parent';
       const optionsFilter = 'child';
-      const expectedRegex = /^\/parent\/.*child/;
+      const expectedRegex = new RegExp(
+        `^${RegExp.escape('/parent/')}.*${RegExp.escape('child')}`,
+      );
 
       // when
       addFilterCondition(queryMock, pagePath, optionsFilter);
@@ -225,7 +229,9 @@ describe('listPages', () => {
       // setup
       const pagePath = '/parent';
       const optionsFilter = 'child';
-      const expectedRegex = /^\/parent\/.*child/;
+      const expectedRegex = new RegExp(
+        `^${RegExp.escape('/parent/')}.*${RegExp.escape('child')}`,
+      );
 
       // when
       addFilterCondition(queryMock, pagePath, optionsFilter, true);
@@ -313,7 +319,9 @@ describe('when excludedPaths is handled', () => {
     await handler(reqMock, resMock);
 
     // check if the logic generates the correct regex: ^\/(user|tmp)(\/|$)
-    const expectedRegex = /^\/(user|tmp)(\/|$)/;
+    const expectedRegex = new RegExp(
+      `^\\/(${RegExp.escape('user')}|${RegExp.escape('tmp')})(\\/|$)`,
+    );
     expect(queryMock.and).toHaveBeenCalledWith([
       {
         path: { $not: expectedRegex },

+ 4 - 5
packages/remark-lsx/src/server/routes/list-pages/index.ts

@@ -1,7 +1,6 @@
 import type { IUser } from '@growi/core';
 import { OptionParser } from '@growi/core/dist/remark-plugins';
 import { pathUtils } from '@growi/core/dist/utils';
-import escapeStringRegexp from 'escape-string-regexp';
 import type { Request, Response } from 'express';
 import createError, { isHttpError } from 'http-errors';
 
@@ -31,16 +30,16 @@ export function addFilterCondition(
     );
   }
 
-  const pagePathForRegexp = escapeStringRegexp(addTrailingSlash(pagePath));
+  const pagePathForRegexp = RegExp.escape(addTrailingSlash(pagePath));
 
   let filterPath: RegExp;
   try {
     if (optionsFilter.charAt(0) === '^') {
       // move '^' to the first of path
-      const escapedFilter = escapeStringRegexp(optionsFilter.slice(1));
+      const escapedFilter = RegExp.escape(optionsFilter.slice(1));
       filterPath = new RegExp(`^${pagePathForRegexp}${escapedFilter}`);
     } else {
-      const escapedFilter = escapeStringRegexp(optionsFilter);
+      const escapedFilter = RegExp.escape(optionsFilter);
       filterPath = new RegExp(`^${pagePathForRegexp}.*${escapedFilter}`);
     }
   } catch (err) {
@@ -100,7 +99,7 @@ export const listPages = ({
       if (excludedPaths.length > 0) {
         const escapedPaths = excludedPaths.map((p) => {
           const cleanPath = p.startsWith('/') ? p.substring(1) : p;
-          return escapeStringRegexp(cleanPath);
+          return RegExp.escape(cleanPath);
         });
 
         const regex = new RegExp(`^\\/(${escapedPaths.join('|')})(\\/|$)`);

File diff ditekan karena terlalu besar
+ 288 - 289
pnpm-lock.yaml


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini