Przeglądaj źródła

upgrade next.js to v15

Yuki Takei 1 miesiąc temu
rodzic
commit
2ca6f053a0

+ 44 - 2
.kiro/specs/reduce-modules-loaded/design.md

@@ -407,16 +407,58 @@ const nextConfig = {
 
 | Blocker | Severity | Mitigation |
 |---------|----------|------------|
-| `next-superjson` SWC plugin broken in v15 | Critical | Research alternatives: manual superjson in getServerSideProps, or use `superjson` directly without SWC plugin |
+| `next-superjson` SWC plugin broken in v15 | Critical | **Resolved** — custom webpack loader (`superjson-ssr-loader.js`) replaces the SWC plugin with a simple regex-based source transform; see SuperJSON Migration section below |
 | `I18NextHMRPlugin` (webpack plugin) | Medium | Only affects dev HMR for i18n; can use `--webpack` flag for dev |
 | React 19 peer dependency | Low | Pages Router has React 18 backward compat in v15 |
 | `@next/font` removal | Low | Codemod available; switch to `next/font` |
 
+##### SuperJSON Migration: Custom Webpack Loader Approach
+
+The `next-superjson` SWC plugin is replaced by a custom webpack loader that achieves the same transparent auto-wrapping of `getServerSideProps` without any SWC/Babel dependency.
+
+**Architecture**:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ Build Time (webpack)                                            │
+│                                                                 │
+│  .page.tsx files ──► superjson-ssr-loader.js ──► bundled output │
+│                      (auto-wraps getServerSideProps             │
+│                       with withSuperJSONProps)                  │
+└─────────────────────────────────────────────────────────────────┘
+
+┌─────────────────────────────────────────────────────────────────┐
+│ Runtime                                                         │
+│                                                                 │
+│  Server: getServerSideProps → superjson.serialize(props)        │
+│  Client: _app.page.tsx → deserializeSuperJSONProps(pageProps)   │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**Components**:
+
+| File | Role |
+|------|------|
+| `src/utils/superjson-ssr-loader.js` | Webpack loader — regex-based source transform that auto-wraps `getServerSideProps` exports |
+| `src/pages/utils/superjson-ssr.ts` | Runtime helpers — `withSuperJSONProps()` (server) and `deserializeSuperJSONProps()` (client) |
+| `_app.page.tsx` | Centralized client-side deserialization via `deserializeSuperJSONProps()` |
+| `next.config.js` | Loader registration targeting `.page.{ts,tsx}` files |
+
+**Why this approach over per-page wrapping**:
+- Zero per-page file changes (38 fewer files modified vs manual wrapping)
+- New pages automatically get superjson serialization without manual wrapping
+- Closer to the original `next-superjson` DX (config-only, transparent to page authors)
+- Webpack loader API is stable across Next.js versions — no SWC ABI fragility
+
+**Why not `next-superjson` v1.0.8**:
+- v1.0.8 achieves "v15 support" by pinning `@swc/core@1.4.17` (March 2024) and running a separate SWC compilation pass — fragile binary pinning with double compilation
+- Depends on unmaintained `next-superjson-plugin` v0.6.3 WASM binary
+- See `research.md` for detailed assessment
+
 **Implementation Notes**
 - Run codemod first: `npx @next/codemod@canary upgrade latest`
 - Test with `--webpack` flag to isolate bundler-related issues from framework issues
 - The `bundlePagesRouterDependencies: true` setting is the highest-value v15 feature for this spec — it automatically bundles server-side deps, which combined with `serverExternalPackages` provides fine-grained control
-- Research `next-superjson` alternatives during Phase 1 to have a mitigation ready
 
 ## Testing Strategy
 

+ 151 - 0
.kiro/specs/reduce-modules-loaded/research.md

@@ -135,6 +135,153 @@
 - **Risk**: Module count reduction is insufficient from config-only changes → **Mitigation**: Bundle analysis will reveal if server module leakage is the primary cause, guiding whether upgrade is needed
 - **Risk**: I18NextHMRPlugin has no Turbopack equivalent → **Mitigation**: Use `--webpack` flag for dev until alternative is available; Turbopack adoption is Phase 2b
 
+## Phase 3: Next.js 15+ Feature Evaluation (Task 9.1)
+
+### Context
+
+Phase 2 achieved significant module reduction (initial: 2,704 → 895, -67%) through dynamic imports, null-loader expansion, and dependency replacement. Phase 3 evaluates whether upgrading to Next.js 15 provides additional meaningful optimization via `bundlePagesRouterDependencies` and `serverExternalPackages`.
+
+### Current State
+
+| Component | Version | Notes |
+|-----------|---------|-------|
+| Next.js | 14.2.35 | Pages Router, Webpack 5 |
+| React | 18.2.0 | |
+| next-superjson | 1.0.7 | SWC plugin wrapper for superjson serialization |
+| Node.js | 24.13.1 | Exceeds v15 minimum (18.18.0) |
+
+### Next.js 15 Features Relevant to Module Reduction
+
+#### 1. `bundlePagesRouterDependencies` (Stable)
+
+- **What it does**: Enables automatic server-side dependency bundling for Pages Router, matching App Router default behavior. All server-side dependencies are bundled into the server output instead of using native Node.js `require()` at runtime.
+- **Impact**: Improved cold start (pre-resolved deps), smaller deployment footprint via tree-shaking of server bundles. Does NOT directly reduce client-side initial module count (our primary KPI), but improves overall server-side build efficiency.
+- **Configuration**: `bundlePagesRouterDependencies: true` in `next.config.js`
+- **Risk**: Low — Next.js maintains an auto-exclude list for packages with native bindings (mongoose, mongodb, express, @aws-sdk/*, sharp are all auto-excluded)
+
+#### 2. `serverExternalPackages` (Stable)
+
+- **What it does**: Opt-out specific packages from server-side bundling when `bundlePagesRouterDependencies` is enabled. These packages use native `require()`.
+- **Auto-excluded packages relevant to GROWI**: `mongoose`, `mongodb`, `express`, `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, `sharp`, `pino`, `ts-node`, `typescript`, `webpack`
+- **GROWI packages that may need manual addition**: `passport`, `ldapjs`, `nodemailer`, `multer`, `redis`, `connect-redis`, `@elastic/elasticsearch*`
+- **Configuration**: `serverExternalPackages: ['passport', ...]`
+
+#### 3. Turbopack (Stable for Dev in v15)
+
+- **Status**: Turbopack Dev is stable in Next.js 15. Default bundler in Next.js 16.
+- **Benefits**: Automatic import optimization (eliminates need for `optimizePackageImports`), 14x faster cold starts, 28x faster HMR
+- **GROWI blockers**: Does NOT support `webpack()` config. GROWI's null-loader rules, I18NextHMRPlugin, source-map-loader, and ChunkModuleStatsPlugin all require webpack config.
+- **Mitigation**: Can run with `--webpack` flag in dev to keep Webpack while upgrading Next.js. Turbopack adoption deferred to separate task.
+
+#### 4. Improved Tree-shaking and Module Resolution
+
+- **Better dead code elimination** in both Webpack and Turbopack modes
+- **SWC improvements** for barrel file optimization
+- **Not directly measurable** without upgrading — included as potential secondary benefit
+
+### `next-superjson` Compatibility Assessment
+
+#### Current Architecture
+
+GROWI uses `withSuperjson()` in `next.config.js` (line 184) which:
+1. Injects a custom webpack loader targeting `pages/` directory files
+2. The loader runs `@swc/core` with `next-superjson-plugin` to AST-transform each page
+3. **Auto-wraps** `getServerSideProps` with `withSuperJSONProps()` (serializes props via superjson)
+4. **Auto-wraps** page default export with `withSuperJSONPage()` (deserializes props on client)
+
+Custom serializers registered in `_app.page.tsx`:
+- `registerTransformerForObjectId()` — handles MongoDB ObjectId serialization (no mongoose dependency)
+- `registerPageToShowRevisionWithMeta()` — handles page revision data (called in `[[...path]]` page)
+
+#### Compatibility Options
+
+| Option | Approach | Risk | Effort |
+|--------|----------|------|--------|
+| A. Upgrade `next-superjson` to 1.0.8 | Claims v15 support via frozen `@swc/core@1.4.17` | Medium — fragile SWC binary pinning; underlying plugin unmaintained | Minimal (version bump) |
+| B. Use `superjson-next@0.7.x` fork | Community SWC plugin with native v15 support | Medium — third-party fork, SWC plugins inherently fragile | Low (config change) |
+| C. Manual superjson (per-page wrapping) | Remove plugin; use helper functions + wrap each page's `getServerSideProps` | Low — no SWC plugin dependency | Medium (create helpers, wrap 38 pages) |
+| **D. Custom webpack loader (Recommended)** | **Remove plugin; use a simple regex-based webpack loader to auto-wrap `getServerSideProps`** | **Low — no SWC/Babel dependency, webpack loader API is stable** | **Low (create loader + config, no per-page changes)** |
+
+#### Detailed Assessment of Option A: `next-superjson` v1.0.8 / v2.0.0
+
+Both v1.0.8 and v2.0.0 were published on the same day (Oct 18, 2025). They share identical code — the only difference is the peer dependency declaration (`next >= 10` vs `next >= 16`).
+
+**How v1.0.8 achieves "Next.js 15 support"**:
+1. Does NOT use Next.js's built-in SWC plugin system
+2. Registers a custom webpack/turbopack loader targeting `pages/` directory files
+3. This loader uses a **bundled `@swc/core` pinned to v1.4.17 (March 2024)** to run a separate SWC compilation
+4. The pinned SWC loads the `next-superjson-plugin` v0.6.3 WASM binary
+
+**Risks**:
+- **Double SWC compilation**: Page files are compiled by both Next.js's SWC and the plugin's frozen SWC — potential for conflicts and performance overhead
+- **Pinned binary fragility**: `@swc/core@1.4.17` is from early 2024; SWC plugin ABI is notoriously unstable across versions
+- **Unmaintained upstream**: The `next-superjson-plugin` v0.6.3 WASM binary comes from `blitz-js/next-superjson-plugin`, which has unmerged PRs and open issues for v15 compatibility
+- **Low adoption**: Published Oct 2025, minimal community usage
+
+**Conclusion**: The "support" is a fragile workaround, not genuine compatibility. Rejected.
+
+#### Recommended: Option D — Custom Webpack Loader
+
+**Why**: Achieves the same zero-page-change DX as the original `next-superjson` plugin, but without any SWC/Babel dependency. The loader is a simple regex-based source transform (~15 lines) that auto-wraps `getServerSideProps` exports with `withSuperJSONProps()`. Webpack's loader API is stable across versions, making this future-proof.
+
+**How it works**:
+1. A webpack loader targets `.page.{ts,tsx}` files
+2. If the file exports `getServerSideProps`, the loader:
+   - Prepends `import { withSuperJSONProps } from '~/pages/utils/superjson-ssr'`
+   - Renames `export const getServerSideProps` → `const __getServerSideProps__`
+   - Appends `export const getServerSideProps = __withSuperJSONProps__(__getServerSideProps__)`
+3. Deserialization is centralized in `_app.page.tsx` (same as Option C)
+
+**Migration plan**:
+1. Create `withSuperJSONProps()` and `deserializeSuperJSONProps()` helpers in `src/pages/utils/superjson-ssr.ts`
+2. Create `src/utils/superjson-ssr-loader.js` — simple regex-based webpack loader
+3. Add loader rule in `next.config.js` webpack config (targets `.page.{ts,tsx}` files)
+4. Add centralized deserialization in `_app.page.tsx`
+5. Remove `next-superjson` dependency and `withSuperjson()` from `next.config.js`
+6. Keep `superjson` as direct dependency; keep all `registerCustom` calls unchanged
+
+**Advantages over Option C**:
+- Zero per-page file changes (38 fewer files modified)
+- Diff is ~20 lines total instead of ~660 lines
+- Closer to the original `next-superjson` DX (config-only, transparent to page authors)
+- New pages automatically get superjson serialization without manual wrapping
+
+**Scope**: 3 files changed (loader, next.config.js, _app.page.tsx) + 1 new file (superjson-ssr.ts with helpers)
+
+### Breaking Changes Affecting GROWI (Pages Router)
+
+| Change | Impact | Action Required |
+|--------|--------|-----------------|
+| Node.js ≥ 18.18.0 required | None — GROWI uses Node 24.x | No action |
+| `@next/font` → `next/font` | None — GROWI does not use `@next/font` | No action |
+| `swcMinify` enabled by default | Low — already effective | No action |
+| `next/dynamic` `suspense` prop removed | Verify — GROWI uses `next/dynamic` extensively | Check all `dynamic()` calls for `suspense` prop |
+| `eslint-plugin-react-hooks` v5.0.0 | Low — may trigger new lint warnings | Run lint after upgrade |
+| Config renames (`experimental.bundlePagesExternals` → `bundlePagesRouterDependencies`) | None — GROWI doesn't use the experimental names | No action |
+| `next/image` `Content-Disposition` changed | None — GROWI uses standard `next/image` | No action |
+| Async Request APIs (cookies, headers) | None — App Router only | No action |
+| React 19 peer dependency | None — Pages Router supports React 18 backward compat | Stay on React 18 |
+
+### Decision: Proceed with Next.js 15 Upgrade
+
+**Rationale**:
+1. **`bundlePagesRouterDependencies` + `serverExternalPackages`** provide proper server-side dependency bundling, completing the optimization work started in Phase 2
+2. **Breaking changes for Pages Router are minimal** — no async API changes, no React 19 requirement
+3. **`next-superjson` blocker is resolved** via custom webpack loader (Option D) — zero per-page changes, same transparent DX as original plugin
+4. **No Turbopack migration needed** — continue using Webpack with `--webpack` flag in dev
+5. **Phase 2 results (initial: 895 modules)** are already strong; v15 features provide server-side improvements and lay groundwork for future Turbopack adoption
+
+**Expected benefits**:
+- Server-side bundle optimization via `bundlePagesRouterDependencies`
+- Proper `serverExternalPackages` support (replaces null-loader workaround for some packages)
+- Modern Next.js foundation for future improvements (Turbopack, App Router migration path)
+- Elimination of fragile SWC plugin dependency (`next-superjson`) — replaced by simple webpack loader with no external dependencies
+
+**Risks and mitigations**:
+- `I18NextHMRPlugin` — Keep using Webpack bundler in dev (`--webpack` flag if needed)
+- Test regressions — Full test suite + typecheck + lint + build verification
+- Superjson serialization — Test all page routes for correct data serialization/deserialization
+
 ## References
 - [Next.js v15 Upgrade Guide](https://nextjs.org/docs/app/guides/upgrading/version-15) — Breaking changes inventory
 - [Turbopack API Reference](https://nextjs.org/docs/app/api-reference/turbopack) — Supported features and known gaps
@@ -142,3 +289,7 @@
 - [Package Bundling Guide (Pages Router)](https://nextjs.org/docs/pages/guides/package-bundling) — bundlePagesRouterDependencies, serverExternalPackages
 - [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) — Benchmarks and approach
 - [next-superjson GitHub](https://github.com/remorses/next-superjson) — Compatibility status
+- [next-superjson-plugin GitHub](https://github.com/blitz-js/next-superjson-plugin) — SWC plugin (unmaintained)
+- [superjson-next fork](https://github.com/serg-and/superjson-next) — Community fork with v15 support
+- [Next.js 15.5 Blog Post](https://nextjs.org/blog/next-15-5) — Latest features
+- [server-external-packages.jsonc](https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/server-external-packages.jsonc) — Auto-excluded server packages

+ 29 - 6
.kiro/specs/reduce-modules-loaded/tasks.md

@@ -239,16 +239,39 @@ The following loop repeats until the user declares completion:
 
 ## Phase 3: Next.js Version Upgrade Evaluation (Deferred)
 
-- [ ] 9.1 Document Next.js 15+ feature evaluation
-  - Document which Next.js 15+ features (`bundlePagesRouterDependencies`, `serverExternalPackages`, Turbopack, improved tree-shaking) are relevant to further module reduction
-  - Assess the `next-superjson` compatibility blocker and identify mitigation options
+- [x] 9.1 Document Next.js 15+ feature evaluation
+  - Documented all Next.js 15+ features relevant to module reduction: `bundlePagesRouterDependencies`, `serverExternalPackages`, Turbopack (stable dev), improved tree-shaking
+  - Assessed `next-superjson` blocker: 4 options evaluated (A: v1.0.8 upgrade, B: superjson-next fork, C: manual per-page wrapping, D: custom webpack loader)
+  - **Option A (`next-superjson` v1.0.8) rejected** — achieves "v15 support" via fragile `@swc/core@1.4.17` pinning with double SWC compilation; depends on unmaintained WASM binary
+  - **Option D (custom webpack loader) selected** — zero-dependency regex-based source transform; same transparent DX as original plugin with no per-page changes
+  - Breaking changes for Pages Router are minimal (no async API changes, React 18 backward compat confirmed)
+  - **Decision: Proceed with Next.js 15 upgrade** — benefits outweigh minimal risk
+  - Full evaluation documented in `research.md` under "Phase 3: Next.js 15+ Feature Evaluation"
   - _Requirements: 1.1, 1.2, 1.3, 5.1, 5.4_
 
-- [ ] 9.2 Execute Next.js 15 upgrade (conditional on 9.1 decision)
+- [ ] 9.2 Execute Next.js 15 upgrade
+  - Migrated from `next-superjson` SWC plugin to custom webpack loader approach:
+    - Created `withSuperJSONProps()` and `deserializeSuperJSONProps()` in `src/pages/utils/superjson-ssr.ts` (10 tests)
+    - Created `src/utils/superjson-ssr-loader.js` — regex-based webpack loader that auto-wraps `getServerSideProps` exports
+    - Added loader rule in `next.config.js` targeting `.page.{ts,tsx}` files
+    - Added centralized deserialization in `_app.page.tsx`
+    - Removed `next-superjson` dependency and `withSuperjson()` from `next.config.js`
+    - **Zero per-page file changes** — loader transparently handles all 38 pages
+  - Upgraded Next.js 14.2.35 → 15.5.12, `@next/bundle-analyzer` 14.1.3 → 15.5.12
+  - Updated peer deps in `@growi/presentation`, `@growi/remark-lsx`, `@growi/ui` to `^14 || ^15`
   - _Requirements: 5.2, 5.3_
 
-- [ ] 9.3 Enable v15-specific module optimization features
+- [x] 9.3 Enable v15-specific module optimization features
+  - Added `bundlePagesRouterDependencies: true` to `next.config.js` — bundles server-side dependencies for Pages Router, matching App Router behavior
+  - Added `serverExternalPackages: ['handsontable']` — legacy `handsontable@6.2.2` requires unavailable `@babel/polyfill`; client-only via dynamic import, kept external on server
+  - Auto-excluded packages (mongoose, mongodb, express, sharp, and 68 others) handled by Next.js built-in list
+  - `serverExternalPackages` replaces `experimental.serverComponentsExternalPackages` (now stable in v15)
+  - Production build passes with new configuration
   - _Requirements: 3.4, 5.2_
 
-- [ ] 9.4 Run full regression test suite after upgrade
+- [x] 9.4 Run full regression test suite after upgrade
+  - Type checking: Zero errors (tsgo --noEmit)
+  - Biome lint: 1,791 files checked, no errors
+  - Tests: 127 test files, 1,375 tests — all passed
+  - Production build: Passes with `bundlePagesRouterDependencies: true` + `serverExternalPackages`
   - _Requirements: 5.3, 6.2, 6.3_

+ 2 - 1
apps/app/next-env.d.ts

@@ -1,5 +1,6 @@
 /// <reference types="next" />
 /// <reference types="next/image-types/global" />
+/// <reference path="./.next/types/routes.d.ts" />
 
 // NOTE: This file should not be edited
-// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
+// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

+ 18 - 5
apps/app/next.config.js

@@ -7,7 +7,6 @@
 
 const path = require('node:path');
 
-const { withSuperjson } = require('next-superjson');
 const {
   PHASE_PRODUCTION_BUILD,
   PHASE_PRODUCTION_SERVER,
@@ -104,6 +103,14 @@ module.exports = (phase) => {
     pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
     i18n,
 
+    // Bundle server-side dependencies for Pages Router (Next.js 15+)
+    // Matches App Router behavior: all deps are bundled except those in serverExternalPackages
+    // Auto-excluded: mongoose, mongodb, express, sharp, and 68 other packages with native bindings
+    bundlePagesRouterDependencies: true,
+    serverExternalPackages: [
+      'handsontable', // Legacy v6.2.2 requires @babel/polyfill which is unavailable; client-only via dynamic import
+    ],
+
     // for build
     typescript: {
       tsconfigPath: 'tsconfig.build.client.json',
@@ -116,6 +123,14 @@ module.exports = (phase) => {
 
     /** @param config {import('next').NextConfig} */
     webpack(config, options) {
+      // Auto-wrap getServerSideProps with superjson serialization (replaces next-superjson SWC plugin)
+      if (options.isServer) {
+        config.module.rules.push({
+          test: /\.page\.(tsx|ts)$/,
+          use: [path.resolve(__dirname, 'src/utils/superjson-ssr-loader.js')],
+        });
+      }
+
       if (!options.isServer) {
         // Avoid "Module not found: Can't resolve 'fs'"
         // See: https://stackoverflow.com/a/68511591
@@ -168,9 +183,7 @@ module.exports = (phase) => {
     },
   };
 
-  // production server
-  // Skip withSuperjson() in production server phase because the pages directory
-  // doesn't exist in the production build and withSuperjson() tries to find it
+  // production server — skip bundle analyzer
   if (phase === PHASE_PRODUCTION_SERVER) {
     return nextConfig;
   }
@@ -181,5 +194,5 @@ module.exports = (phase) => {
       (process.env.ANALYZE === 'true' || process.env.ANALYZE === '1'),
   });
 
-  return withBundleAnalyzer(withSuperjson()(nextConfig));
+  return withBundleAnalyzer(nextConfig);
 };

+ 2 - 3
apps/app/package.json

@@ -171,10 +171,9 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
-    "next": "^14.2.35",
+    "next": "^15.0.0",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
-    "next-superjson": "^1.0.7",
     "next-themes": "^0.2.1",
     "nocache": "^4.0.0",
     "node-cron": "^3.0.2",
@@ -271,7 +270,7 @@
     "@handsontable/react": "=2.1.0",
     "@headless-tree/core": "^1.5.3",
     "@headless-tree/react": "^1.5.3",
-    "@next/bundle-analyzer": "^14.1.3",
+    "@next/bundle-analyzer": "^15.0.0",
     "@popperjs/core": "^2.11.8",
     "@tanstack/react-virtual": "^3.13.12",
     "@testing-library/jest-dom": "^6.5.0",

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

@@ -1,3 +1,4 @@
+/// <reference types="multer" />
 import assert from 'node:assert';
 import fs from 'node:fs';
 import { Readable, Transform, Writable } from 'node:stream';

+ 7 - 1
apps/app/src/pages/_app.page.tsx

@@ -24,6 +24,7 @@ import { isCommonInitialProps } from './common-props';
 import { getLocaleAtServerSide } from './utils/locale';
 import { useNextjsRoutingPageRegister } from './utils/nextjs-routing-utils';
 import { registerTransformerForObjectId } from './utils/objectid-transformer';
+import { deserializeSuperJSONProps } from './utils/superjson-ssr';
 
 import '~/styles/prebuilt/vendor.css';
 import '~/styles/style-app.scss';
@@ -58,11 +59,16 @@ type GrowiAppProps = AppProps<CombinedCommonProps> & {
 
 const GrowiAppSubstance = ({
   Component,
-  pageProps,
+  pageProps: rawPageProps,
   userLocale,
 }: GrowiAppProps): JSX.Element => {
   const router = useRouter();
 
+  // Deserialize superjson-serialized props from getServerSideProps
+  const pageProps = deserializeSuperJSONProps(
+    rawPageProps,
+  ) as CombinedCommonProps;
+
   // Hydrate global atoms with server-side data
   useHydrateGlobalInitialAtoms(
     isCommonInitialProps(pageProps) ? pageProps : undefined,

+ 173 - 0
apps/app/src/pages/utils/superjson-ssr.spec.ts

@@ -0,0 +1,173 @@
+import type { GetServerSidePropsContext } from 'next';
+import superjson from 'superjson';
+import { describe, expect, it } from 'vitest';
+
+import { deserializeSuperJSONProps, withSuperJSONProps } from './superjson-ssr';
+
+// Helper to create a minimal GetServerSidePropsContext
+const createMockContext = (): GetServerSidePropsContext =>
+  ({
+    req: {} as never,
+    res: {} as never,
+    params: {},
+    query: {},
+    resolvedUrl: '/',
+    locale: 'en',
+  }) as GetServerSidePropsContext;
+
+describe('withSuperJSONProps', () => {
+  it('should serialize Date objects in props', async () => {
+    const date = new Date('2026-01-15T00:00:00.000Z');
+    const gssp = async () => ({
+      props: { createdAt: date, name: 'test' },
+    });
+
+    const wrapped = withSuperJSONProps(gssp);
+    const result = await wrapped(createMockContext());
+
+    expect(result).toHaveProperty('props');
+    const props = (result as { props: Record<string, unknown> }).props;
+
+    // Date should be serialized as ISO string
+    expect(props.createdAt).toBe('2026-01-15T00:00:00.000Z');
+    // Plain string should pass through
+    expect(props.name).toBe('test');
+    // _superjson metadata should be present
+    expect(props._superjson).toBeDefined();
+  });
+
+  it('should pass through redirect results unchanged', async () => {
+    const gssp = async () => ({
+      redirect: { destination: '/login', permanent: false },
+    });
+
+    const wrapped = withSuperJSONProps(gssp);
+    const result = await wrapped(createMockContext());
+
+    expect(result).toEqual({
+      redirect: { destination: '/login', permanent: false },
+    });
+  });
+
+  it('should pass through notFound results unchanged', async () => {
+    const gssp = async () => ({
+      notFound: true as const,
+    });
+
+    const wrapped = withSuperJSONProps(gssp);
+    const result = await wrapped(createMockContext());
+
+    expect(result).toEqual({ notFound: true });
+  });
+
+  it('should handle plain JSON props (no special types)', async () => {
+    const gssp = async () => ({
+      props: { count: 42, items: ['a', 'b'] },
+    });
+
+    const wrapped = withSuperJSONProps(gssp);
+    const result = await wrapped(createMockContext());
+
+    const props = (result as { props: Record<string, unknown> }).props;
+    expect(props.count).toBe(42);
+    expect(props.items).toEqual(['a', 'b']);
+    // No _superjson metadata when no special types
+    expect(props._superjson).toBeUndefined();
+  });
+
+  it('should handle Map and Set in props', async () => {
+    const gssp = async () => ({
+      props: { tags: new Set(['a', 'b']), lookup: new Map([['k', 'v']]) },
+    });
+
+    const wrapped = withSuperJSONProps(gssp);
+    const result = await wrapped(createMockContext());
+
+    const props = (result as { props: Record<string, unknown> }).props;
+    // Should have superjson metadata for Set and Map
+    expect(props._superjson).toBeDefined();
+  });
+
+  it('should handle Promise-wrapped props', async () => {
+    const gssp = async () => ({
+      props: Promise.resolve({ value: 'deferred' }),
+    });
+
+    const wrapped = withSuperJSONProps(gssp);
+    const result = await wrapped(createMockContext());
+
+    const props = (result as { props: Record<string, unknown> }).props;
+    expect(props.value).toBe('deferred');
+  });
+});
+
+describe('deserializeSuperJSONProps', () => {
+  it('should deserialize Date from superjson-serialized props', () => {
+    const original = {
+      createdAt: new Date('2026-01-15T00:00:00.000Z'),
+      name: 'test',
+    };
+    const { json, meta } = superjson.serialize(original);
+    const serializedProps: Record<string, unknown> = {
+      ...(json as Record<string, unknown>),
+      _superjson: meta,
+    };
+
+    const result = deserializeSuperJSONProps(serializedProps);
+
+    expect(result.createdAt).toBeInstanceOf(Date);
+    expect((result.createdAt as Date).toISOString()).toBe(
+      '2026-01-15T00:00:00.000Z',
+    );
+    expect(result.name).toBe('test');
+    // _superjson should be stripped
+    expect(result).not.toHaveProperty('_superjson');
+  });
+
+  it('should pass through plain props without _superjson metadata', () => {
+    const props = { count: 42, items: ['a', 'b'] };
+    const result = deserializeSuperJSONProps(props);
+    expect(result).toEqual({ count: 42, items: ['a', 'b'] });
+  });
+
+  it('should handle Map and Set deserialization', () => {
+    const original = {
+      tags: new Set(['a', 'b']),
+      lookup: new Map([['k', 'v']]),
+    };
+    const { json, meta } = superjson.serialize(original);
+    const serializedProps: Record<string, unknown> = {
+      ...(json as Record<string, unknown>),
+      _superjson: meta,
+    };
+
+    const result = deserializeSuperJSONProps(serializedProps);
+
+    expect(result.tags).toBeInstanceOf(Set);
+    expect(result.lookup).toBeInstanceOf(Map);
+    expect(result.tags).toEqual(new Set(['a', 'b']));
+    expect(result.lookup).toEqual(new Map([['k', 'v']]));
+  });
+
+  it('should work with withSuperJSONProps round-trip', async () => {
+    const date = new Date('2026-06-15T12:00:00.000Z');
+    const gssp = async () => ({
+      props: { createdAt: date, name: 'round-trip', count: 5 },
+    });
+
+    const wrapped = withSuperJSONProps(gssp);
+    const result = await wrapped(createMockContext());
+    const serializedProps = (result as { props: Record<string, unknown> })
+      .props;
+
+    const deserialized = deserializeSuperJSONProps(serializedProps);
+
+    expect(deserialized.createdAt).toBeInstanceOf(Date);
+    expect((deserialized.createdAt as Date).toISOString()).toBe(
+      '2026-06-15T12:00:00.000Z',
+    );
+    expect(deserialized.name).toBe('round-trip');
+    expect(deserialized.count).toBe(5);
+    expect(deserialized).not.toHaveProperty('_superjson');
+  });
+});

+ 69 - 0
apps/app/src/pages/utils/superjson-ssr.ts

@@ -0,0 +1,69 @@
+import type {
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  GetServerSidePropsResult,
+} from 'next';
+import type { SuperJSONResult } from 'superjson';
+import superjson from 'superjson';
+
+/**
+ * Wraps a getServerSideProps function to serialize its return props via superjson.
+ *
+ * Handles redirect/notFound pass-through, and supports Promise-wrapped props.
+ * Adds `_superjson` metadata to props when non-JSON types (Date, Map, Set, etc.) are present.
+ */
+export function withSuperJSONProps<P extends Record<string, unknown>>(
+  gssp: GetServerSideProps<P>,
+): GetServerSideProps<P> {
+  return async (context: GetServerSidePropsContext) => {
+    const result: GetServerSidePropsResult<P> = await gssp(context);
+
+    // Pass through redirect and notFound results unchanged
+    if ('redirect' in result || 'notFound' in result) {
+      return result;
+    }
+
+    if (!('props' in result) || result.props == null) {
+      return result;
+    }
+
+    // Resolve potentially Promise-wrapped props
+    const resolvedProps = await result.props;
+
+    const { json, meta } = superjson.serialize(resolvedProps);
+
+    // Spread the serialized JSON and add _superjson metadata if present
+    const props = { ...(json as Record<string, unknown>) } as P;
+    if (meta != null) {
+      (props as Record<string, unknown>)._superjson = meta;
+    }
+
+    return { ...result, props };
+  };
+}
+
+/**
+ * Deserializes props that were serialized by withSuperJSONProps.
+ *
+ * If `_superjson` metadata is present, restores original types (Date, Map, Set, etc.).
+ * If no metadata is present, returns props unchanged.
+ */
+export function deserializeSuperJSONProps<T extends Record<string, unknown>>(
+  props: T,
+): T {
+  const metaField = (props as Record<string, unknown>)._superjson as
+    | SuperJSONResult['meta']
+    | undefined;
+
+  if (metaField == null) {
+    return props;
+  }
+
+  // Extract _superjson from the props to get the pure JSON
+  const { _superjson: _, ...json } = props as Record<string, unknown>;
+
+  return superjson.deserialize({
+    json: json as SuperJSONResult['json'],
+    meta: metaField,
+  }) as T;
+}

+ 32 - 0
apps/app/src/utils/superjson-ssr-loader.js

@@ -0,0 +1,32 @@
+/**
+ * Webpack loader that auto-wraps getServerSideProps with withSuperJSONProps.
+ *
+ * Replaces the `next-superjson` SWC plugin with a zero-dependency source transform.
+ * Targets `.page.{ts,tsx}` files that export `getServerSideProps`.
+ *
+ * Transform:
+ *   export const getServerSideProps: ... = async (ctx) => { ... };
+ * becomes:
+ *   import { withSuperJSONProps as __withSuperJSONProps__ } from '~/pages/utils/superjson-ssr';
+ *   const __getServerSideProps__: ... = async (ctx) => { ... };
+ *   export const getServerSideProps = __withSuperJSONProps__(__getServerSideProps__);
+ */
+module.exports = function superjsonSsrLoader(source) {
+  if (!/export\s+const\s+getServerSideProps\b/.test(source)) {
+    return source;
+  }
+
+  const importLine =
+    "import { withSuperJSONProps as __withSuperJSONProps__ } from '~/pages/utils/superjson-ssr';\n";
+
+  const renamed = source.replace(
+    /export\s+const\s+getServerSideProps\b/,
+    'const __getServerSideProps__',
+  );
+
+  return (
+    importLine +
+    renamed +
+    '\nexport const getServerSideProps = __withSuperJSONProps__(__getServerSideProps__);\n'
+  );
+};

+ 1 - 1
packages/presentation/package.json

@@ -61,7 +61,7 @@
     "unist-util-visit": "^5.0.0"
   },
   "peerDependencies": {
-    "next": "^14",
+    "next": "^14 || ^15",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   }

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

@@ -56,7 +56,7 @@
     "unist-util-visit": "^5.0.0"
   },
   "peerDependencies": {
-    "next": "^14",
+    "next": "^14 || ^15",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   }

+ 1 - 1
packages/ui/package.json

@@ -47,7 +47,7 @@
     "reactstrap": "^9.2.2"
   },
   "peerDependencies": {
-    "next": "^14",
+    "next": "^14 || ^15",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   }

Plik diff jest za duży
+ 331 - 227
pnpm-lock.yaml


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików