From 76a6d0c31a77c48d2d3c761e7f8384f336fdd0d2 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Mon, 19 Oct 2026 11:40:50 +0000 Subject: [PATCH 01/11] build: upgrade to SvelteKit 3.1.0 and adapter-vercel 7.0.1 SvelteKit 3 no longer reads svelte.config.js, and adapter-vercel 7 requires Kit 3, so both are upgraded together. - Pin @sveltejs/kit 3.1.0 and @sveltejs/adapter-vercel 7.0.1 - Move the svelte.config.js options into sveltekit() in vite.config.ts; pass dynamicCompileOptions directly because Kit 3 removed config.vitePlugin (the sv migrate output still nests it there) - Extend $app/tsconfig, which provides the compiler options that were set by hand, and keep $app/types in the overridden "types" build, unit tests and lint pass. svelte-check reports 20 errors that belong to the next phase (type export moves, $app/environment, $app/stores, resolve() typings, and generic Actions params). Co-Authored-By: Claude Opus 5.5 --- package.json | 4 +- pnpm-lock.yaml | 108 ++++++++++++++--------------------------------- svelte.config.js | 39 ----------------- tsconfig.json | 13 +----- vite.config.ts | 35 ++++++++++++++- 5 files changed, 69 insertions(+), 130 deletions(-) delete mode 100644 svelte.config.js diff --git a/package.json b/package.json index 4ee8d36dd..c0e8b8141 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "@eslint/eslintrc": "3.3.7", "@playwright/test": "1.64.1", "@quramy/prisma-fabbrica": "2.3.3", - "@sveltejs/adapter-vercel": "6.3.4", - "@sveltejs/kit": "2.70.3", + "@sveltejs/adapter-vercel": "7.0.1", + "@sveltejs/kit": "3.1.0", "@sveltejs/vite-plugin-svelte": "7.3.1", "@tailwindcss/forms": "0.5.11", "@tailwindcss/vite": "4.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c7c2d7ff..a022ceb6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,11 +235,11 @@ importers: specifier: 2.3.3 version: 2.3.3(@prisma/client@5.22.0(prisma@5.22.0))(typescript@6.0.3) '@sveltejs/adapter-vercel': - specifier: 6.3.4 - version: 6.3.4(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(supports-color@7.2.0) + specifier: 7.0.1 + version: 7.0.1(@sveltejs/kit@3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(supports-color@7.2.0) '@sveltejs/kit': - specifier: 2.70.3 - version: 2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) + specifier: 3.1.0 + version: 3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) '@sveltejs/vite-plugin-svelte': specifier: 7.3.1 version: 7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) @@ -329,7 +329,7 @@ importers: version: 5.0.3(svelte@5.57.2(@typescript-eslint/types@8.71.2)) sveltekit-superforms: specifier: 3.0.0 - version: 3.0.0(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(@types/json-schema@7.0.15)(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3) + version: 3.0.0(@sveltejs/kit@3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(@types/json-schema@7.0.15)(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3) tailwindcss: specifier: 4.3.3 version: 4.3.3 @@ -1826,32 +1826,26 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.10': - resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} - peerDependencies: - acorn: ^8.9.0 - '@sveltejs/acorn-typescript@1.0.13': resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-vercel@6.3.4': - resolution: {integrity: sha512-MxEgTqIlmNg4qy5Rz4vwDKs2vNUrsL+DjcfegUVr/spPWSvWrSYFTUPY7tLbHsi8RxdUqpxC9UN/yzkOzZeMGA==} - engines: {node: '>=20.0'} + '@sveltejs/adapter-vercel@7.0.1': + resolution: {integrity: sha512-BnIGkQACaEeiXYcC5idodcUylp9Jyyx9aHIb7sqcGWfj8X3nJ57y8abmhFuAkg3oipLhaEfRqwwNtbuVhEaLhQ==} peerDependencies: - '@sveltejs/kit': ^2.4.0 + '@sveltejs/kit': ^3.0.0-next.0 - '@sveltejs/kit@2.70.3': - resolution: {integrity: sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==} - engines: {node: '>=18.13'} + '@sveltejs/kit@3.1.0': + resolution: {integrity: sha512-JTWlq2HQlkqx6vIoxhC0R6sc4WVqbQoRyEmr5YlqKmiYbzY1U3awtSWTmjMLlS056a49AmkQR6rnNANXIE8B0Q==} + engines: {node: '>=22.17'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.3.3 || ^6.0.0 - vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + '@sveltejs/vite-plugin-svelte': ^7.0.0 + svelte: ^5.57.1 + typescript: ^6.0.0 + vite: ^8.0.12 peerDependenciesMeta: '@opentelemetry/api': optional: true @@ -1981,9 +1975,6 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -2575,9 +2566,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -2655,9 +2646,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} - devalue@5.9.4: resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==} @@ -3142,10 +3130,6 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - known-css-properties@0.37.0: resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} @@ -3668,10 +3652,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} @@ -3940,9 +3920,6 @@ packages: engines: {node: '>=10'} hasBin: true - set-cookie-parser@3.1.0: - resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} - setprototypeof@1.1.1: resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} @@ -5526,38 +5503,29 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.10(acorn@8.17.0)': - dependencies: - acorn: 8.17.0 - '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': dependencies: acorn: 8.18.0 - '@sveltejs/adapter-vercel@6.3.4(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(supports-color@7.2.0)': + '@sveltejs/adapter-vercel@7.0.1(@sveltejs/kit@3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(supports-color@7.2.0)': dependencies: - '@sveltejs/kit': 2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) + '@sveltejs/kit': 3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) '@vercel/nft': 1.10.2(supports-color@7.2.0) - esbuild: 0.28.1 transitivePeerDependencies: - encoding - rollup - supports-color - '@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15))': + '@sveltejs/kit@3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15))': dependencies: + '@rolldown/pluginutils': 1.0.1 '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.10(acorn@8.17.0) '@sveltejs/vite-plugin-svelte': 7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) - '@types/cookie': 0.6.0 - acorn: 8.17.0 - cookie: 0.7.2 - devalue: 5.8.1 + cookie: 2.0.1 + devalue: 5.9.4 esm-env: 1.2.2 - kleur: 4.1.5 - magic-string: 0.30.21 + magic-string: 1.4.3 mrmime: 2.0.1 - set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.57.2(@typescript-eslint/types@8.71.2) vite: 8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15) @@ -5667,8 +5635,6 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/cookie@0.6.0': {} - '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -6071,15 +6037,15 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.3(supports-color@7.2.0) '@rollup/pluginutils': 5.4.0 - acorn: 8.17.0 - acorn-import-attributes: 1.9.5(acorn@8.17.0) + acorn: 8.18.0 + acorn-import-attributes: 1.9.5(acorn@8.18.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 glob: 13.0.6 graceful-fs: 4.2.11 node-gyp-build: 4.8.4 - picomatch: 4.0.4 + picomatch: 4.0.7 resolve-from: 5.0.0 transitivePeerDependencies: - encoding @@ -6316,10 +6282,6 @@ snapshots: abbrev@3.0.1: {} - acorn-import-attributes@1.9.5(acorn@8.17.0): - dependencies: - acorn: 8.17.0 - acorn-import-attributes@1.9.5(acorn@8.18.0): dependencies: acorn: 8.18.0 @@ -6502,7 +6464,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie@0.7.2: {} + cookie@2.0.1: {} cross-spawn@7.0.6: dependencies: @@ -6561,8 +6523,6 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.8.1: {} - devalue@5.9.4: {} dlv@1.1.3: @@ -7155,8 +7115,6 @@ snapshots: kleur@3.0.3: {} - kleur@4.1.5: {} - known-css-properties@0.37.0: {} lefthook-android-arm64@2.2.2: @@ -7610,8 +7568,6 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.4: {} - picomatch@4.0.5: {} picomatch@4.0.7: {} @@ -7852,8 +7808,6 @@ snapshots: semver@7.8.5: {} - set-cookie-parser@3.1.0: {} - setprototypeof@1.1.1: {} shebang-command@2.0.0: @@ -7970,9 +7924,9 @@ snapshots: transitivePeerDependencies: - '@typescript-eslint/types' - sveltekit-superforms@3.0.0(@sveltejs/kit@2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(@types/json-schema@7.0.15)(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3): + sveltekit-superforms@3.0.0(@sveltejs/kit@3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(@types/json-schema@7.0.15)(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3): dependencies: - '@sveltejs/kit': 2.70.3(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) + '@sveltejs/kit': 3.1.0(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2(@typescript-eslint/types@8.71.2))(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)))(svelte@5.57.2(@typescript-eslint/types@8.71.2))(typescript@6.0.3)(vite@8.3.4(@types/node@25.9.9)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.15)) devalue: 5.9.4 esm-env: 1.2.2 memoize-weak: 1.0.2 diff --git a/svelte.config.js b/svelte.config.js deleted file mode 100644 index 322d27072..000000000 --- a/svelte.config.js +++ /dev/null @@ -1,39 +0,0 @@ -import adapter from '@sveltejs/adapter-vercel'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // See: - // https://svelte.dev/docs/kit/adapter-vercel - adapter: adapter({ - regions: ['hnd1'], // Tokyo, Japan. - memory: 3008, // To avoid OOM errors on /workbooks/{slug} - maxDuration: 30, - }), - // Module paths use package.json#imports (#lib, #features); SvelteKit 3 deprecates kit.alias - }, - - compilerOptions: { - runes: true, - }, - - // [Work around] - // SuperDebug.svelte: Cannot use `export let` in runes mode — use `$props()` instead. - // - // See: - // https://github.com/ciscoheat/sveltekit-superforms/issues/306#issuecomment-1891015986 - vitePlugin: { - dynamicCompileOptions({ filename }) { - if (filename.includes('node_modules')) { - return { runes: undefined }; // or false, check what works - } - }, - }, -}; - -export default config; diff --git a/tsconfig.json b/tsconfig.json index 91c9443c9..294a1483e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,11 @@ { - "extends": "./.svelte-kit/tsconfig.json", + "extends": "$app/tsconfig", "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, "incremental": true, - "isolatedModules": true, - "moduleResolution": "bundler", "outDir": "build", - "resolveJsonModule": true, - "skipLibCheck": true, "sourceMap": true, "strict": true, - "types": ["node", "vitest/globals"], // Note: "node" is required because "types" is replaced (not merged) when extending tsconfig - "verbatimModuleSyntax": true + "types": ["$app/types", "node", "vitest/globals"] // Note: "types" is replaced (not merged) when extending tsconfig, so keep "$app/types" and add "node" }, // Module paths are resolved by package.json#imports (#lib, #features), not path aliases // diff --git a/vite.config.ts b/vite.config.ts index 0d77361b3..992ce6b26 100755 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,42 @@ +import adapter from '@sveltejs/adapter-vercel'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vitest/config'; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], + plugins: [ + tailwindcss(), + // SvelteKit 3 takes the former svelte.config.js options here + sveltekit({ + // Consult https://svelte.dev/docs/kit/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + // See: + // https://svelte.dev/docs/kit/adapter-vercel + adapter: adapter({ + regions: ['hnd1'], // Tokyo, Japan. + memory: 3008, // To avoid OOM errors on /workbooks/{slug} + maxDuration: 30, + }), + + compilerOptions: { + runes: true, + }, + + // [Work around] + // SuperDebug.svelte: Cannot use `export let` in runes mode — use `$props()` instead. + // + // See: + // https://github.com/ciscoheat/sveltekit-superforms/issues/306#issuecomment-1891015986 + dynamicCompileOptions({ filename }) { + if (filename.includes('node_modules')) { + return { runes: undefined }; // or false, check what works + } + }, + }), + ], optimizeDeps: { exclude: ['@prisma/client'], }, From a046e2680f958c8cd452ce9f981faba234d5b3a7 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Mon, 19 Oct 2026 12:01:58 +0000 Subject: [PATCH 02/11] refactor: migrate route types and navigation state to SvelteKit 3 Fix the svelte-check errors left by the Kit 3 upgrade and move the remaining Kit 2 APIs to their Kit 3 forms. - Import Handle from @sveltejs/kit/hooks and SubmitFunction from $app/forms; import Actions from ./$types where params are used - Replace $app/environment with $app/env, including the test mocks - Move KanbanBoard from $app/stores to $app/state and from replaceState() to a shallow goto(), so filter changes keep the board's local state; widen the kanban URL helpers to accept the read-only page.url - Type buildWorkbooksUrl() as a /workbooks route with search so resolve() accepts it, use { replace: true } instead of the deprecated replaceState option, and drop the @ts-expect-error comments that Kit 3's route types no longer need - Add E2E tests for keeping the kanban state while replacing the filter URL and for restoring saved workbook filters without a history entry svelte-check, lint, unit tests, build, and the related E2E specs pass. Co-Authored-By: Claude Opus 5.5 --- e2e/workbook_order.spec.ts | 29 +++++++++++++++++++ e2e/workbooks_list.spec.ts | 21 ++++++++++++++ src/features/auth/server/auth.test.ts | 2 +- src/features/auth/server/auth.ts | 2 +- .../stores/active_contest_type.svelte.test.ts | 4 +-- .../votes/components/VotableGrade.svelte | 1 - .../stores/replenishment_workbook.svelte.ts | 4 +-- .../stores/replenishment_workbook.test.ts | 4 +-- .../workbooks/utils/workbook_url_params.ts | 4 +-- src/hooks.server.ts | 2 +- src/lib/actions/handle_dropdown.ts | 2 +- src/lib/components/AuthForm.svelte | 4 +-- src/lib/components/TagForm.svelte | 1 - src/lib/stores/local_storage_helper.svelte.ts | 4 +-- src/routes/(admin)/tasks/+page.svelte | 3 +- .../(admin)/tasks/[task_id]/+page.server.ts | 4 ++- .../_components/TaskTableForImport.svelte | 3 +- .../order/_components/KanbanBoard.svelte | 17 +++++++---- .../(admin)/workbooks/order/_utils/kanban.ts | 10 +++---- src/routes/users/[username]/+page.svelte | 1 - src/routes/votes/[slug]/+page.svelte | 3 +- src/routes/workbooks/+page.svelte | 9 ++---- .../active_problem_list_tab.svelte.test.ts | 4 +-- 23 files changed, 92 insertions(+), 46 deletions(-) diff --git a/e2e/workbook_order.spec.ts b/e2e/workbook_order.spec.ts index 34751385a..b4ba4c1ef 100644 --- a/e2e/workbook_order.spec.ts +++ b/e2e/workbook_order.spec.ts @@ -215,6 +215,35 @@ test.describe('workbook order page', () => { expect(categories.split(',')).not.toContain(SolutionCategory.DATA_STRUCTURE); expect(categories.split(',')).toContain(SolutionCategory.GRAPH); }); + + test('keeps kanban state while replacing filter URL', async ({ page }) => { + await page.goto( + `${ORDER_URL}?tab=solution&categories=${SolutionCategory.PENDING},${SolutionCategory.GRAPH},${SolutionCategory.DATA_STRUCTURE}`, + ); + await expect(page.getByRole('heading', { name: 'データ構造' })).toBeVisible({ + timeout: TIMEOUT, + }); + + const historyLength = await page.evaluate(() => history.length); + const graphCards = await getCardsInColumn(page, SolutionCategory.GRAPH); + // Unsaved drag results live only in the board's local state, so a data reload would discard them + const dataRequests: string[] = []; + + page.on('request', (request) => { + if (request.url().includes('__data.json')) { + dataRequests.push(request.url()); + } + }); + + await page.getByRole('button', { name: 'データ構造' }).click(); + await expect(page.getByRole('heading', { name: 'データ構造' })).not.toBeVisible(); + await expect(page).not.toHaveURL(new RegExp(SolutionCategory.DATA_STRUCTURE)); + await expect(page).toHaveURL(new RegExp(`categories=[^&]*${SolutionCategory.GRAPH}`)); + + expect(await page.evaluate(() => history.length)).toBe(historyLength); + expect(dataRequests).toEqual([]); + expect(await getCardsInColumn(page, SolutionCategory.GRAPH)).toEqual(graphCards); + }); }); test.describe('API error handling', () => { diff --git a/e2e/workbooks_list.spec.ts b/e2e/workbooks_list.spec.ts index 6a773e7eb..6fdd50a63 100644 --- a/e2e/workbooks_list.spec.ts +++ b/e2e/workbooks_list.spec.ts @@ -225,6 +225,27 @@ test.describe('logged-in user (general)', () => { timeout: TIMEOUT, }); }); + + test('restores saved workbook filters without adding a history entry', async ({ page }) => { + await page.goto(`${WORKBOOK_LIST_URL}?tab=${TAB_SOLUTION}&categories=${CATEGORY_GRAPH}`); + await expect(page.getByRole('tab', { name: '解法別' })).toHaveAttribute( + 'aria-selected', + 'true', + { timeout: TIMEOUT }, + ); + + await page.goto('/'); + await expect(page).toHaveURL('/', { timeout: TIMEOUT }); + + await page.getByRole('link', { name: '問題集', exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`categories=${CATEGORY_GRAPH}`), { + timeout: TIMEOUT, + }); + + // The restore replaces the bare /workbooks entry, so going back returns to the previous page + await page.goBack(); + await expect(page).toHaveURL('/', { timeout: TIMEOUT }); + }); }); test('toggling replenishment workbooks shows/hides the section when it exists', async ({ diff --git a/src/features/auth/server/auth.test.ts b/src/features/auth/server/auth.test.ts index dfaa4fefb..ee759bdd8 100644 --- a/src/features/auth/server/auth.test.ts +++ b/src/features/auth/server/auth.test.ts @@ -4,7 +4,7 @@ import type { RequestEvent } from '@sveltejs/kit'; import { Roles } from '@prisma/client'; // dev = false so `secure: !dev` resolves to true (the production-relevant cookie flag) -vi.mock('$app/environment', () => ({ dev: false })); +vi.mock('$app/env', () => ({ dev: false })); vi.mock('./session', () => ({ SESSION_COOKIE_NAME: 'auth_session', diff --git a/src/features/auth/server/auth.ts b/src/features/auth/server/auth.ts index b2e84047b..f768e32a6 100644 --- a/src/features/auth/server/auth.ts +++ b/src/features/auth/server/auth.ts @@ -1,4 +1,4 @@ -import { dev } from '$app/environment'; +import { dev } from '$app/env'; import type { RequestEvent } from '@sveltejs/kit'; import { diff --git a/src/features/tasks/stores/active_contest_type.svelte.test.ts b/src/features/tasks/stores/active_contest_type.svelte.test.ts index c6b1c998d..139f43200 100644 --- a/src/features/tasks/stores/active_contest_type.svelte.test.ts +++ b/src/features/tasks/stores/active_contest_type.svelte.test.ts @@ -9,12 +9,12 @@ import { } from '#features/tasks/stores/active_contest_type.svelte.js'; // WHY: a single dynamic mock is the only way to toggle `browser` per describe. Every vi.mock is -// hoisted and the last registration for a module wins, so a second vi.mock of `$app/environment` +// hoisted and the last registration for a module wins, so a second vi.mock of `$app/env` // would silently clamp the whole file to one branch. // Defaults to false so the singleton constructed at import time stays SSR-safe (no localStorage). const browserState = vi.hoisted(() => ({ value: false })); -vi.mock('$app/environment', () => ({ +vi.mock('$app/env', () => ({ get browser() { return browserState.value; }, diff --git a/src/features/votes/components/VotableGrade.svelte b/src/features/votes/components/VotableGrade.svelte index f92442054..d721ae804 100644 --- a/src/features/votes/components/VotableGrade.svelte +++ b/src/features/votes/components/VotableGrade.svelte @@ -62,7 +62,6 @@ // Use task_id as a deterministic component ID to avoid SSR/hydration mismatches. const componentId = taskResult.task_id; - // @ts-expect-error svelte-check TS2554: AppTypes declaration merging causes RouteId to resolve as string, requiring params. Runtime behavior is correct. const editProfileHref = `${resolve(EDIT_PROFILE_PAGE)}?tab=atcoder`; let selectedVoteGrade = $state(); diff --git a/src/features/workbooks/stores/replenishment_workbook.svelte.ts b/src/features/workbooks/stores/replenishment_workbook.svelte.ts index 6eb7fa62c..4338224e2 100644 --- a/src/features/workbooks/stores/replenishment_workbook.svelte.ts +++ b/src/features/workbooks/stores/replenishment_workbook.svelte.ts @@ -1,6 +1,6 @@ // See: -// https://svelte.dev/docs/kit/$app-environment#browser -import { browser } from '$app/environment'; +// https://svelte.dev/docs/kit/$app-env#browser +import { browser } from '$app/env'; // See: // https://svelte.dev/docs/svelte/stores diff --git a/src/features/workbooks/stores/replenishment_workbook.test.ts b/src/features/workbooks/stores/replenishment_workbook.test.ts index 07ba8c782..1758fda11 100644 --- a/src/features/workbooks/stores/replenishment_workbook.test.ts +++ b/src/features/workbooks/stores/replenishment_workbook.test.ts @@ -8,12 +8,12 @@ import { } from '#features/workbooks/stores/replenishment_workbook.svelte.js'; // WHY: a single dynamic mock is the only way to toggle `browser` per describe. Every vi.mock is -// hoisted and the last registration for a module wins, so a second vi.mock of `$app/environment` +// hoisted and the last registration for a module wins, so a second vi.mock of `$app/env` // would silently clamp the whole file to one branch. // Defaults to false so the singleton constructed at import time stays SSR-safe (no localStorage). const browserState = vi.hoisted(() => ({ value: false })); -vi.mock('$app/environment', () => ({ +vi.mock('$app/env', () => ({ get browser() { return browserState.value; }, diff --git a/src/features/workbooks/utils/workbook_url_params.ts b/src/features/workbooks/utils/workbook_url_params.ts index 7a04ddcf1..90f183930 100644 --- a/src/features/workbooks/utils/workbook_url_params.ts +++ b/src/features/workbooks/utils/workbook_url_params.ts @@ -60,13 +60,13 @@ export function parseWorkBookCategory(params: URLSearchParams): SelectedSolution * @param tab - Active tab * @param grade - Selected grade (only appended when tab === CURRICULUM) * @param category - Selected category (only appended when tab === SOLUTION) - * @returns URL string suitable for use with goto() + * @returns URL string suitable for use with goto(); typed as a route ID with search so `resolve()` accepts it */ export function buildWorkbooksUrl( tab: WorkBookTab, grade?: TaskGrade, category?: SelectedSolutionCategory, -): string { +): `/workbooks?${string}` { const params = new URLSearchParams(); params.set('tab', tab); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 03e158386..c73efc0fc 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,7 +1,7 @@ // See: // https://github.com/joysofcode/sveltekit-deploy // https://tech-blog.rakus.co.jp/entry/20230209/sveltekit -import type { Handle } from '@sveltejs/kit'; +import type { Handle } from '@sveltejs/kit/hooks'; import { createAuthRequest } from '#features/auth/server/auth.js'; diff --git a/src/lib/actions/handle_dropdown.ts b/src/lib/actions/handle_dropdown.ts index 0405bea88..cf6889a64 100644 --- a/src/lib/actions/handle_dropdown.ts +++ b/src/lib/actions/handle_dropdown.ts @@ -1,5 +1,5 @@ import { writable, type Writable } from 'svelte/store'; -import { browser } from '$app/environment'; +import { browser } from '$app/env'; const activeDropdownId: Writable = writable(null); diff --git a/src/lib/components/AuthForm.svelte b/src/lib/components/AuthForm.svelte index 2e280efe8..977f5c363 100644 --- a/src/lib/components/AuthForm.svelte +++ b/src/lib/components/AuthForm.svelte @@ -42,11 +42,9 @@ const { form, message, errors, submitting, enhance } = formProperties; - // @ts-expect-error svelte-check TS2554: AppTypes declaration merging causes RouteId to resolve as string, requiring params. Runtime behavior is correct. const homeHref = resolve(HOME_PAGE); - // @ts-expect-error svelte-check TS2554: same declaration merging issue const forgotPasswordHref = resolve(FORGOT_PASSWORD_PAGE); - // @ts-expect-error svelte-check TS2554: AppTypes declaration merging causes RouteId to resolve as string, requiring params. Runtime behavior is correct. + // @ts-expect-error alternativePageLink is a runtime pathname such as '/signup?redirectTo=...'; Kit 3 types only known route IDs, but resolve() returns group-less static paths as is const alternativeHref = $derived(resolve(alternativePageLink)); let isSubmitting = $state(false); diff --git a/src/lib/components/TagForm.svelte b/src/lib/components/TagForm.svelte index 582111585..88b4951d3 100644 --- a/src/lib/components/TagForm.svelte +++ b/src/lib/components/TagForm.svelte @@ -29,7 +29,6 @@ //export const isAdmin: boolean; // Admin権限がある場合は、編集リンクを表示する - // @ts-expect-error svelte-check TS2554: AppTypes declaration merging causes RouteId to resolve as string, requiring params. Runtime behavior is correct. const tagsHref = resolve('/(admin)/tags'); diff --git a/src/lib/stores/local_storage_helper.svelte.ts b/src/lib/stores/local_storage_helper.svelte.ts index b730691b6..8c93775e4 100644 --- a/src/lib/stores/local_storage_helper.svelte.ts +++ b/src/lib/stores/local_storage_helper.svelte.ts @@ -1,6 +1,6 @@ // See: -// https://svelte.dev/docs/kit/$app-environment#browser -import { browser } from '$app/environment'; +// https://svelte.dev/docs/kit/$app-env#browser +import { browser } from '$app/env'; export function useLocalStorage(key: string, initialValue: T) { return new LocalStorageWrapper(key, initialValue); diff --git a/src/routes/(admin)/tasks/+page.svelte b/src/routes/(admin)/tasks/+page.svelte index b5f620711..5e1801813 100644 --- a/src/routes/(admin)/tasks/+page.svelte +++ b/src/routes/(admin)/tasks/+page.svelte @@ -1,6 +1,5 @@ diff --git a/src/routes/votes/[slug]/+page.svelte b/src/routes/votes/[slug]/+page.svelte index 288728c8d..0f1b65482 100644 --- a/src/routes/votes/[slug]/+page.svelte +++ b/src/routes/votes/[slug]/+page.svelte @@ -27,7 +27,6 @@ const redirectTo = page.url.pathname; - // @ts-expect-error svelte-check TS2554: AppTypes declaration merging causes RouteId to resolve as string, requiring params. Runtime behavior is correct. const editProfileHref = `${resolve(EDIT_PROFILE_PAGE)}?tab=atcoder`; const totalVotes = $derived( @@ -42,7 +41,7 @@
diff --git a/src/routes/workbooks/+page.svelte b/src/routes/workbooks/+page.svelte index 0d4bd83a9..2a29c8ffa 100644 --- a/src/routes/workbooks/+page.svelte +++ b/src/routes/workbooks/+page.svelte @@ -50,8 +50,8 @@ if (saved) { const savedUrl = new URL(saved, window.location.origin); - // @ts-expect-error svelte-check TS2554: AppTypes declaration merging causes RouteId to resolve as string, requiring params. Runtime behavior is correct. - goto(resolve(savedUrl.pathname + savedUrl.search), { replaceState: true }); + // The saved value always comes from buildWorkbooksUrl, so only its query is restored + goto(resolve(`/workbooks?${savedUrl.searchParams}`), { replace: true }); } }); @@ -61,7 +61,7 @@ }); // Each lambda closes over the reactive `data` binding at call time - const TAB_URL_BUILDERS: Record string> = { + const TAB_URL_BUILDERS: Record ReturnType> = { [WorkBookTab.CURRICULUM]: () => buildWorkbooksUrl(WorkBookTab.CURRICULUM, data.selectedGrade), [WorkBookTab.SOLUTION]: () => buildWorkbooksUrl(WorkBookTab.SOLUTION, undefined, data.selectedCategory), @@ -69,17 +69,14 @@ }; function handleTabChange(tab: WorkBookTab) { - // @ts-expect-error svelte-check TS2554: same declaration merging issue goto(resolve(TAB_URL_BUILDERS[tab]())); } function handleGradeChange(grade: TaskGrade) { - // @ts-expect-error svelte-check TS2554: same declaration merging issue goto(resolve(buildWorkbooksUrl(WorkBookTab.CURRICULUM, grade))); } function handleCategoryChange(category: SelectedSolutionCategory) { - // @ts-expect-error svelte-check TS2554: same declaration merging issue goto(resolve(buildWorkbooksUrl(WorkBookTab.SOLUTION, undefined, category))); } diff --git a/src/test/lib/stores/active_problem_list_tab.svelte.test.ts b/src/test/lib/stores/active_problem_list_tab.svelte.test.ts index 99f0da1c7..204790be3 100644 --- a/src/test/lib/stores/active_problem_list_tab.svelte.test.ts +++ b/src/test/lib/stores/active_problem_list_tab.svelte.test.ts @@ -8,12 +8,12 @@ import { } from '#lib/stores/active_problem_list_tab.svelte.js'; // WHY: a single dynamic mock is the only way to toggle `browser` per describe. Every vi.mock is -// hoisted and the last registration for a module wins, so a second vi.mock of `$app/environment` +// hoisted and the last registration for a module wins, so a second vi.mock of `$app/env` // would silently clamp the whole file to one branch. // Defaults to false so the singleton constructed at import time stays SSR-safe (no localStorage). const browserState = vi.hoisted(() => ({ value: false })); -vi.mock('$app/environment', () => ({ +vi.mock('$app/env', () => ({ get browser() { return browserState.value; }, From 2c420dac3c8a31f39530a8059f701763241a5573 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Mon, 19 Oct 2026 12:15:32 +0000 Subject: [PATCH 03/11] test(e2e): cover forms, auth and error pages on SvelteKit 3 Pin the SvelteKit 3 behavior of form actions, logout, session cookies and error pages before the release. - signin: keep form values after a failed enhanced login, return the fail() status (400) for an enhanced submission, land on home when logging out without a session, and keep the session cookie attributes - workbook_edit: keep the nested workbook tasks after a failed edit - account_transfer: reject a non-admin user (page and direct POST) and show the validation message for identical account names - error_pages: render the 404 and 403 error pages The fail() status and logout tests fail on SvelteKit 2, so they detect the Kit 3 behavior change. Co-Authored-By: Claude Opus 5.5 --- e2e/account_transfer.spec.ts | 66 ++++++++++++++++++++++++++++++++++++ e2e/error_pages.spec.ts | 35 +++++++++++++++++++ e2e/signin.spec.ts | 66 ++++++++++++++++++++++++++++++++++++ e2e/workbook_edit.spec.ts | 22 +++++++++++- 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 e2e/account_transfer.spec.ts create mode 100644 e2e/error_pages.spec.ts diff --git a/e2e/account_transfer.spec.ts b/e2e/account_transfer.spec.ts new file mode 100644 index 000000000..449ef384c --- /dev/null +++ b/e2e/account_transfer.spec.ts @@ -0,0 +1,66 @@ +import { test, expect, type Page } from '@playwright/test'; + +import { loginAsAdmin, loginAsUser } from './helpers/auth'; + +const TIMEOUT = 60 * 1000; +const ACCOUNT_TRANSFER_URL = '/account_transfer'; +// Users that do not exist, so even a broken guard could not copy real answers +const MISSING_SOURCE_USER = 'e2e_missing_source'; +const MISSING_DESTINATION_USER = 'e2e_missing_destination'; + +// Mirrors SvelteKit's ActionResult — $app/forms is not resolvable in Playwright. +type ActionRedirect = { type: 'redirect'; status: number; location: string }; +type ActionResultLike = ActionRedirect | { type: 'success' | 'failure' | 'error' }; + +async function postAccountTransfer( + page: Page, + sourceUserName: string, + destinationUserName: string, +): Promise { + const text = await page.evaluate( + async ({ url, body }) => { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }); + + return response.text(); + }, + { + url: ACCOUNT_TRANSFER_URL, + body: new URLSearchParams({ sourceUserName, destinationUserName }).toString(), + }, + ); + + return JSON.parse(text) as ActionResultLike; +} + +test.describe('access control', () => { + test('rejects account transfer by a non-admin user', async ({ page }) => { + await loginAsUser(page); + + await page.goto(ACCOUNT_TRANSFER_URL); + await expect(page).not.toHaveURL(new RegExp(`${ACCOUNT_TRANSFER_URL}$`), { timeout: TIMEOUT }); + + const result = await postAccountTransfer(page, MISSING_SOURCE_USER, MISSING_DESTINATION_USER); + expect(result.type).toBe('redirect'); + expect(result).toHaveProperty('location', expect.stringMatching(/\/login/)); + }); +}); + +test.describe('admin', () => { + test('shows a validation message when the account names are the same', async ({ page }) => { + await loginAsAdmin(page); + await page.goto(ACCOUNT_TRANSFER_URL); + + await page.getByLabel('旧アカウント名').fill(MISSING_SOURCE_USER); + await page.getByLabel('新アカウント名').fill(MISSING_SOURCE_USER); + await page.getByRole('button', { name: 'コピー', exact: true }).click(); + + await expect( + page.getByText('新アカウント名は、旧アカウント名とは異なるものを指定してください'), + ).toBeVisible({ timeout: TIMEOUT }); + await expect(page.getByLabel('旧アカウント名')).toHaveValue(MISSING_SOURCE_USER); + }); +}); diff --git a/e2e/error_pages.spec.ts b/e2e/error_pages.spec.ts new file mode 100644 index 000000000..26e721472 --- /dev/null +++ b/e2e/error_pages.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; + +import { loginAsUser } from './helpers/auth'; + +import { FORBIDDEN, NOT_FOUND } from '../src/lib/constants/http-response-status-codes'; + +const TIMEOUT = 60 * 1000; +const ERROR_PAGE_HEADING = 'エラーが発生しました'; + +test.describe('error page', () => { + test('shows the not found error for an unknown route', async ({ page }) => { + const response = await page.goto('/e2e-route-that-does-not-exist'); + + expect(response?.status()).toBe(NOT_FOUND); + await expect(page.getByRole('heading', { name: ERROR_PAGE_HEADING })).toBeVisible({ + timeout: TIMEOUT, + }); + await expect(page.getByRole('heading', { name: String(NOT_FOUND) })).toBeVisible(); + }); + + test('shows the forbidden error when a general user opens an admin-only page', async ({ + page, + }) => { + await loginAsUser(page); + // The create page's load throws error(403) for non-admin users + const response = await page.goto('/workbooks/create'); + + expect(response?.status()).toBe(FORBIDDEN); + await expect(page.getByRole('heading', { name: ERROR_PAGE_HEADING })).toBeVisible({ + timeout: TIMEOUT, + }); + await expect(page.getByRole('heading', { name: String(FORBIDDEN) })).toBeVisible(); + await expect(page.getByText('アクセス権限がありません。')).toBeVisible(); + }); +}); diff --git a/e2e/signin.spec.ts b/e2e/signin.spec.ts index b5c928489..61b081cb2 100644 --- a/e2e/signin.spec.ts +++ b/e2e/signin.spec.ts @@ -1,11 +1,16 @@ import { test, expect, Page } from '@playwright/test'; import { PRODUCT_CATCH_PHRASE } from '../src/lib/constants/product-info.ts'; +import { BAD_REQUEST } from '../src/lib/constants/http-response-status-codes.ts'; // Set timeout limit const UP_TO_ONE_MINUTE = 60 * 1000; // 60 sec * 1000 ms // Sample username and password for testing const USERNAME = 'guest'; const PASSWORD = 'Ch0kuda1'; +const WRONG_PASSWORD = 'Wr0ngPassword'; +const LOGIN_FAILURE_MESSAGE = + 'ログインできませんでした。登録したユーザ名 / パスワードとなるように修正してください。'; +const SESSION_COOKIE_NAME = 'auth_session'; // TODO: Make available in global.setup.ts async function login(page: Page, username: string, password: string): Promise { @@ -56,3 +61,64 @@ test('logout', async ({ page }) => { await login(page, USERNAME, PASSWORD); await logout(page, USERNAME); }); + +test('redirects to home when logging out without a session', async ({ page }) => { + await login(page, USERNAME, PASSWORD); + await page.goto('/workbooks'); + await expect(page).toHaveURL(/\/workbooks/, { timeout: UP_TO_ONE_MINUTE }); + // Without a session the logout action fails; the form posts to another page, so SvelteKit 3 navigates there + await page.context().clearCookies(); + + await logout(page, USERNAME); +}); + +test('keeps the session cookie attributes after login', async ({ page }) => { + await login(page, USERNAME, PASSWORD); + + const sessionCookie = (await page.context().cookies()).find( + (cookie) => cookie.name === SESSION_COOKIE_NAME, + ); + expect(sessionCookie).toMatchObject({ path: '/', httpOnly: true, secure: true, sameSite: 'Lax' }); + + // The session must survive a navigation to a page that requires it + await page.goto('/users/edit'); + await expect(page).toHaveURL('/users/edit', { timeout: UP_TO_ONE_MINUTE }); +}); + +test.describe('login failure', () => { + test('keeps form values after a failed enhanced submission', async ({ page }) => { + await page.goto('/login'); + await page.locator('input[name="username"]').fill(USERNAME); + await page.locator('input[name="password"]').fill(WRONG_PASSWORD); + await page.getByRole('button', { name: 'ログイン', exact: true }).click(); + + await expect(page.getByText(LOGIN_FAILURE_MESSAGE)).toBeVisible({ timeout: UP_TO_ONE_MINUTE }); + await expect(page).toHaveURL('/login'); + await expect(page.locator('input[name="username"]')).toHaveValue(USERNAME); + }); + + test('returns the fail status for an enhanced submission', async ({ page }) => { + await page.goto('/login'); + + const result = await page.evaluate( + async ({ username, password }) => { + const body = new FormData(); + body.set('username', username); + body.set('password', password); + // Same headers as use:enhance, so SvelteKit answers with an ActionResult + const response = await fetch('/login', { + method: 'POST', + headers: { accept: 'application/json', 'x-sveltekit-action': 'true' }, + body, + }); + + return { status: response.status, body: await response.text() }; + }, + { username: USERNAME, password: WRONG_PASSWORD }, + ); + + // SvelteKit 2 answered 200 here; SvelteKit 3 uses the status passed to fail() + expect(result.status).toBe(BAD_REQUEST); + expect(JSON.parse(result.body)).toMatchObject({ type: 'failure', status: BAD_REQUEST }); + }); +}); diff --git a/e2e/workbook_edit.spec.ts b/e2e/workbook_edit.spec.ts index 8fd401f04..8ffbf7884 100644 --- a/e2e/workbook_edit.spec.ts +++ b/e2e/workbook_edit.spec.ts @@ -1,6 +1,6 @@ import { test, expect, type Page } from '@playwright/test'; -import { loginAsUser } from './helpers/auth'; +import { loginAsAdmin, loginAsUser } from './helpers/auth'; import { FORBIDDEN } from '../src/lib/constants/http-response-status-codes'; @@ -56,3 +56,23 @@ test.describe('workbook edit page access control (load)', () => { await expect(page).toHaveURL(/\/login/, { timeout: TIMEOUT }); }); }); + +test.describe('workbook edit form', () => { + test('keeps nested workbook tasks after a failed submission', async ({ page }) => { + await loginAsAdmin(page); + await page.goto(WORKBOOK_EDIT_URL); + + const taskRows = page.getByRole('table', { name: 'Workbook tasks' }).locator('tbody tr'); + await expect(taskRows.first()).toBeVisible({ timeout: TIMEOUT }); + const taskRowTexts = await taskRows.allTextContents(); + + // A two-character title fails server validation, so nothing is saved + await page.locator('input[name="title"]').fill('ab'); + await page.getByRole('button', { name: '更新', exact: true }).click(); + + await expect(page.getByText('3文字以上入力してください')).toBeVisible({ timeout: TIMEOUT }); + await expect(page).toHaveURL(WORKBOOK_EDIT_URL); + await expect(page.locator('input[name="title"]')).toHaveValue('ab'); + await expect(taskRows).toHaveText(taskRowTexts); + }); +}); From b5e822aa5dccc90d089a6f7336f51deef4eaf06c Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Mon, 19 Oct 2026 12:27:24 +0000 Subject: [PATCH 04/11] docs: align guides and e2e helpers with SvelteKit 3 Co-Authored-By: Claude Sonnet 5.5 --- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- docs/guides/agent-rules/auth.md | 2 +- docs/guides/agent-rules/sveltekit.md | 9 +++++---- docs/guides/agent-rules/testing-e2e.md | 2 ++ docs/guides/agent-rules/testing.md | 4 ++-- docs/guides/architecture.md | 6 +++--- e2e/signin.spec.ts | 13 ++++++++++++- eslint.config.mjs | 2 +- src/app.d.ts | 2 +- 10 files changed, 29 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8002e436f..b69e5d741 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ Before adding a function, search `src/lib/utils/`, `src/lib/services/`, `src/fea ## Architecture -SvelteKit 2 + Svelte 5 Runes + TypeScript | PostgreSQL + Prisma | Flowbite Svelte + Tailwind 4 | Vitest + Playwright | oxlint + ESLint +SvelteKit 3 + Svelte 5 Runes + TypeScript | PostgreSQL + Prisma | Flowbite Svelte + Tailwind 4 | Vitest + Playwright | oxlint + ESLint Layers: `prisma/` | `src/**/server/` | `src/**/zod/` | `src/**/types/`, `src/**/fixtures/` | `src/**/services/` | `src/**/utils/` | `src/**/stores/` | `src/routes/` | `src/**/*.svelte`. Each layer's constraints are in the layer table of `coding-style.md`; read it before writing logic. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2c076e81..893e015d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ - [Node.js](https://nodejs.org): v24.x - 汎用フレームワーク - [Svelte](https://svelte.dev/): v5.x。Runes mode で運用中 - - [SvelteKit](https://svelte.dev/): v2.x + - [SvelteKit](https://svelte.dev/docs/kit): v3.x。設定は `vite.config.ts` の `sveltekit()` に書く(`svelte.config.js` は無い) - UIライブラリ - [Flowbite Svelte](https://flowbite-svelte.com/) - コンポーネントライブラリ - [Flowbite](https://flowbite.com/)も利用可能 diff --git a/docs/guides/agent-rules/auth.md b/docs/guides/agent-rules/auth.md index 7bcac6d04..be1ac6c45 100644 --- a/docs/guides/agent-rules/auth.md +++ b/docs/guides/agent-rules/auth.md @@ -57,4 +57,4 @@ After login/signup, redirect users back to their original intended page instead - Never expose session secrets in client code - Use HTTPS in production - Validate all user inputs server-side -- **SvelteKit's `redirect()` does not validate origin** — always validate user-provided redirect destinations with `isSameOriginRedirect()` +- **SvelteKit 3's `redirect()` throws for an external location unless `external` is passed** — a hostile user-provided destination would turn into a 500, so always validate it with `isSameOriginRedirect()` and fall back to the default page diff --git a/docs/guides/agent-rules/sveltekit.md b/docs/guides/agent-rules/sveltekit.md index 4335aae94..f0c33f694 100644 --- a/docs/guides/agent-rules/sveltekit.md +++ b/docs/guides/agent-rules/sveltekit.md @@ -14,15 +14,16 @@ paths: ## User-Provided Redirect Destinations -**SvelteKit's `redirect()` does NOT validate origin.** If the redirect destination comes from user input (URL query params, form data), always validate with `isSameOriginRedirect(redirectTo, url.origin)` from `src/lib/utils/url.ts` before redirecting. +**SvelteKit 3's `redirect()` throws for a location outside the current origin unless `external` is passed**, so a hostile destination becomes a 500 instead of a safe fallback. If the redirect destination comes from user input (URL query params, form data), always validate with `isSameOriginRedirect(redirectTo, url.origin)` from `src/lib/utils/url.ts` and fall back to the default page. ## Internal Navigation: `resolve()` -All internal navigation must use `resolve()` from `$app/paths`: +All internal navigation must use `resolve()` from `$app/paths`. An argument starting with `/` is a route ID (groups such as `(admin)` are stripped, `[param]` is filled); otherwise it is a pathname without the leading slash. The type accepts only known route IDs and pathnames, optionally followed by `?…` or `#…` — never a plain `string`. - **Parameterized** (preferred): `resolve('/workbooks/[slug]', { slug: workbook.slug })` -- **Static**: `resolve('/')` + `@ts-expect-error` (AppTypes merging quirk) -- **With search/hash**: `resolve(pathname + search + hash)` (concatenate inside, not outside) +- **Static**: `resolve('/votes')`, `resolve('/(admin)/tags')` — write the route ID including its group; no `@ts-expect-error` +- **With search/hash**: ``resolve(`/workbooks?${params}`)`` (concatenate inside, not outside); a URL builder returns a template literal type such as `` `/workbooks?${string}` `` so `resolve()` accepts it +- **Runtime pathname** (e.g. a prop holding `/signup?redirectTo=…`): the type cannot be proven, so add `@ts-expect-error` with the reason; this works only for group-less static paths - **External**: no `resolve()`, use `rel="noreferrer external"` ## HTTP Cache Headers (`setHeaders`) diff --git a/docs/guides/agent-rules/testing-e2e.md b/docs/guides/agent-rules/testing-e2e.md index 71e826e21..8fdd67876 100644 --- a/docs/guides/agent-rules/testing-e2e.md +++ b/docs/guides/agent-rules/testing-e2e.md @@ -129,6 +129,8 @@ SvelteKit's response format for form actions differs by outcome: - `redirect()` → HTTP 200 + JSON body `{"type":"redirect","status":307,"location":"..."}` - `error(N)` → HTTP status N +- `fail(N)` → HTTP status N + JSON body `{"type":"failure","status":N,...}` (SvelteKit 3; SvelteKit 2 answered 200) +- A form whose `action` points to another page navigates there after submission, even when the action fails (SvelteKit 3) Parse the response body with a typed deserialize helper — not raw `JSON.parse`. `$app/forms` (`deserialize`) is not resolvable outside SvelteKit, so define a local `ActionResultLike` type and a `deserializeActionResult(text)` wrapper in the E2E helper (see `e2e/workbook_edit.spec.ts` for reference). Assert `type` and `location` from the deserialized result for redirect checks, and `response.status` for error checks. diff --git a/docs/guides/agent-rules/testing.md b/docs/guides/agent-rules/testing.md index 6d3e8f446..3d33eb072 100644 --- a/docs/guides/agent-rules/testing.md +++ b/docs/guides/agent-rules/testing.md @@ -127,12 +127,12 @@ For mutable module-level `const` objects (override maps), mutate directly in `be ### Browser Toggle per Describe -**Never register `vi.mock('$app/environment')` twice in one file** — the second hoisted call silently overwrites the first, pinning the whole file to one value. Use one dynamic mock with a `vi.hoisted` flag: +**Never register `vi.mock('$app/env')` twice in one file** — the second hoisted call silently overwrites the first, pinning the whole file to one value. Use one dynamic mock with a `vi.hoisted` flag: ```typescript // @vitest-environment jsdom const browserState = vi.hoisted(() => ({ value: false })); -vi.mock('$app/environment', () => ({ +vi.mock('$app/env', () => ({ get browser() { return browserState.value; }, diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md index 4b7f08bcd..9704ebce6 100644 --- a/docs/guides/architecture.md +++ b/docs/guides/architecture.md @@ -75,7 +75,7 @@ feature 内は技術的な役割で分ける。必要なディレクトリだけ - `utils/`: 副作用のない関数。テストを隣接配置する。 - `stores/`: Svelte Runes を使う `.svelte.ts`。 -`features/{feature}/server/` は `src/lib/server/` と違い、SvelteKit が client import を自動では禁止しない。規約と推移的な server-only dependency により client から分離する。複数 feature で使うサーバコードは `src/lib/server/` へ移す。 +SvelteKit 3 は、`src/routes`・`static` 内を除くすべての `server/` ディレクトリと、ファイル名に `server` セグメントを持つファイル(`server.ts`・`*.server.ts`)を server-only として扱い、client から import すると build で失敗させる。`features/{feature}/server/` も対象になる。複数 feature で使うサーバコードは `src/lib/server/` へ移す。 ### 3. src/lib @@ -141,9 +141,9 @@ include: [ ### 6. subpath imports(`#lib`・`#features`) -`src/lib` は `package.json#imports` の `#lib/*`、`src/features` は `#features/*` で参照する。SvelteKit 3 は組み込みの `$lib` を削除し、`kit.alias` も非推奨にするため、`svelte.config.js` に alias は置かない。 +`src/lib` は `package.json#imports` の `#lib/*`、`src/features` は `#features/*` で参照する。SvelteKit 3 は組み込みの `$lib` を削除し、`alias` オプションも非推奨にしたため、`vite.config.ts` の `sveltekit()` に alias は置かない。 拡張子は明示する。TS モジュールは `.js`、コンポーネントは `.svelte`、runes モジュール(`.svelte.ts`)は `.svelte.js` と書く。`vi.mock` の specifier も同じ形にする。 -`$lib` は Kit 2 が組み込みで解決してしまうため、oxlint と ESLint の `no-restricted-imports` で禁止している。lint が検出するのは import・export 宣言(`import type`・`export … from` を含む)と `.ts` の動的 `import()` で、`vi.mock('$lib/...')` と `.svelte` 内の動的 `import()` は検出しない。`$features` と `@/` は alias が無いので、import すると型チェックで失敗する。 +`$lib`・`$features`・`@/` は alias が無いので、import すると型チェックと build で失敗する。`$lib` は Kit 2 の習慣で書かれやすいため、oxlint と ESLint の `no-restricted-imports` でも禁止し、`#lib` への書き換えを案内している。lint が検出するのは import・export 宣言(`import type`・`export … from` を含む)と `.ts` の動的 `import()` で、`vi.mock('$lib/...')` と `.svelte` 内の動的 `import()` は検出しない。 import の specifier をまとめて変えたときは、`rg -n '\$lib|\$features|@/' src e2e prisma` で残りが無いことも確かめる。 ```typescript diff --git a/e2e/signin.spec.ts b/e2e/signin.spec.ts index 61b081cb2..4d51b2e16 100644 --- a/e2e/signin.spec.ts +++ b/e2e/signin.spec.ts @@ -12,6 +12,14 @@ const LOGIN_FAILURE_MESSAGE = 'ログインできませんでした。登録したユーザ名 / パスワードとなるように修正してください。'; const SESSION_COOKIE_NAME = 'auth_session'; +// Mirrors SvelteKit's ActionResult — $app/forms is not resolvable in Playwright. +type ActionFailure = { type: 'failure'; status: number }; +type ActionResultLike = ActionFailure | { type: 'success' | 'redirect' | 'error' }; + +function deserializeActionResult(text: string): ActionResultLike { + return JSON.parse(text) as ActionResultLike; +} + // TODO: Make available in global.setup.ts async function login(page: Page, username: string, password: string): Promise { // Navigate to login page @@ -119,6 +127,9 @@ test.describe('login failure', () => { // SvelteKit 2 answered 200 here; SvelteKit 3 uses the status passed to fail() expect(result.status).toBe(BAD_REQUEST); - expect(JSON.parse(result.body)).toMatchObject({ type: 'failure', status: BAD_REQUEST }); + expect(deserializeActionResult(result.body)).toMatchObject({ + type: 'failure', + status: BAD_REQUEST, + }); }); }); diff --git a/eslint.config.mjs b/eslint.config.mjs index cd19da504..bb38ab825 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -80,7 +80,7 @@ export default [ 'svelte/no-useless-mustaches': 'warn', 'svelte/prefer-writable-derived': 'warn', // New in 3.6.0 - prefer $derived over $state+$effect 'svelte/valid-prop-names-in-kit-pages': 'warn', // Allow props other than data/errors in pages - // Kit 2 still resolves $lib, so new imports would only break on Kit 3; oxlint covers JS/TS files + // Points Kit 2 habits to #lib with a clearer message than the type error; oxlint covers JS/TS files 'no-restricted-imports': [ 'error', { diff --git a/src/app.d.ts b/src/app.d.ts index 30b104fb9..e459f6e35 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,4 @@ -// See https://kit.svelte.dev/docs/types#app +// See https://svelte.dev/docs/kit/types#app import type { Roles } from '@prisma/client'; From 65f948cb3a079b84e2de6a56bfbafaf772fe7bf9 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Mon, 19 Oct 2026 12:35:57 +0000 Subject: [PATCH 05/11] test(e2e): tighten Kit 3 form and kanban checks after cross-review - workbook_edit: delete a task row before the failed submission, so the test fails if the table falls back to the workbook's original tasks (confirmed with a temporary onUpdate mutation) - workbook_order: rename the shallow-routing test to what it checks (no data reload and no history entry) - architecture.md: separate the filename and server/ directory rules for server-only modules, and split the subpath imports section into use / don't use / extensions with OK and NG examples Co-Authored-By: Claude Opus 5.5 --- docs/guides/architecture.md | 16 +++++++++++----- e2e/workbook_edit.spec.ts | 15 +++++++++++++-- e2e/workbook_order.spec.ts | 2 +- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md index 9704ebce6..7a1c38a64 100644 --- a/docs/guides/architecture.md +++ b/docs/guides/architecture.md @@ -75,7 +75,7 @@ feature 内は技術的な役割で分ける。必要なディレクトリだけ - `utils/`: 副作用のない関数。テストを隣接配置する。 - `stores/`: Svelte Runes を使う `.svelte.ts`。 -SvelteKit 3 は、`src/routes`・`static` 内を除くすべての `server/` ディレクトリと、ファイル名に `server` セグメントを持つファイル(`server.ts`・`*.server.ts`)を server-only として扱い、client から import すると build で失敗させる。`features/{feature}/server/` も対象になる。複数 feature で使うサーバコードは `src/lib/server/` へ移す。 +SvelteKit 3 は server-only のモジュールを client から import すると build で失敗させる。ファイル名が `server.*`・`*.server.*` のモジュールは場所を問わず server-only になる。`server/` ディレクトリ内のモジュールは、`src/routes`・`static` 内を除いて server-only になり、`features/{feature}/server/` も対象になる。複数 feature で使うサーバコードは `src/lib/server/` へ移す。 ### 3. src/lib @@ -141,17 +141,23 @@ include: [ ### 6. subpath imports(`#lib`・`#features`) -`src/lib` は `package.json#imports` の `#lib/*`、`src/features` は `#features/*` で参照する。SvelteKit 3 は組み込みの `$lib` を削除し、`alias` オプションも非推奨にしたため、`vite.config.ts` の `sveltekit()` に alias は置かない。 -拡張子は明示する。TS モジュールは `.js`、コンポーネントは `.svelte`、runes モジュール(`.svelte.ts`)は `.svelte.js` と書く。`vi.mock` の specifier も同じ形にする。 -`$lib`・`$features`・`@/` は alias が無いので、import すると型チェックと build で失敗する。`$lib` は Kit 2 の習慣で書かれやすいため、oxlint と ESLint の `no-restricted-imports` でも禁止し、`#lib` への書き換えを案内している。lint が検出するのは import・export 宣言(`import type`・`export … from` を含む)と `.ts` の動的 `import()` で、`vi.mock('$lib/...')` と `.svelte` 内の動的 `import()` は検出しない。 -import の specifier をまとめて変えたときは、`rg -n '\$lib|\$features|@/' src e2e prisma` で残りが無いことも確かめる。 +- **使う:** `src/lib` は `#lib/*`、`src/features` は `#features/*`(`package.json#imports` で定義)。 +- **使わない:** `$lib`・`$features`・`@/`。SvelteKit 3 は組み込みの `$lib` を削除し、`alias` オプションも非推奨にしたため、どれも解決されず型チェックと build で失敗する。`vite.config.ts` の `sveltekit()` にも alias を足さない。 +- **拡張子は明示する:** TS モジュールは `.js`、コンポーネントは `.svelte`、runes モジュール(`.svelte.ts`)は `.svelte.js`。`vi.mock` の specifier も同じ形にする。 ```typescript +// OK import { formatDate } from '#lib/utils/format.js'; import WorkBookForm from '#features/workbooks/components/shared/WorkBookForm.svelte'; import type { PageServerLoad } from './$types'; + +// NG: SvelteKit 3 では解決されない +import { formatDate } from '$lib/utils/format'; ``` +`$lib` は SvelteKit 2 の習慣で書かれやすいため、oxlint と ESLint の `no-restricted-imports` でも禁止し、`#lib` への書き換えを案内している。lint が検出するのは import・export 宣言(`import type`・`export … from` を含む)と `.ts` の動的 `import()` で、`vi.mock('$lib/...')` と `.svelte` 内の動的 `import()` は検出しない。 +import の specifier をまとめて変えたときは、`rg -n '\$lib|\$features|@/' src e2e prisma` で残りが無いことも確かめる。 + `$types` は SvelteKit が生成する route 型専用で、domain type は `src/**/types/` に置く。 ## 将来案・未実装 diff --git a/e2e/workbook_edit.spec.ts b/e2e/workbook_edit.spec.ts index 8ffbf7884..542a2e2d5 100644 --- a/e2e/workbook_edit.spec.ts +++ b/e2e/workbook_edit.spec.ts @@ -58,7 +58,7 @@ test.describe('workbook edit page access control (load)', () => { }); test.describe('workbook edit form', () => { - test('keeps nested workbook tasks after a failed submission', async ({ page }) => { + test('keeps edited nested workbook tasks after a failed submission', async ({ page }) => { await loginAsAdmin(page); await page.goto(WORKBOOK_EDIT_URL); @@ -66,6 +66,16 @@ test.describe('workbook edit form', () => { await expect(taskRows.first()).toBeVisible({ timeout: TIMEOUT }); const taskRowTexts = await taskRows.allTextContents(); + // Removing the last row leaves the other row numbers unchanged + const lastRowNumber = String(taskRowTexts.length); + const editedRowTexts = taskRowTexts.slice(0, -1); + page.once('dialog', (dialog) => dialog.accept()); + await taskRows + .filter({ has: page.getByRole('cell', { name: lastRowNumber, exact: true }) }) + .getByRole('button', { name: '削除' }) + .click(); + await expect(taskRows).toHaveText(editedRowTexts); + // A two-character title fails server validation, so nothing is saved await page.locator('input[name="title"]').fill('ab'); await page.getByRole('button', { name: '更新', exact: true }).click(); @@ -73,6 +83,7 @@ test.describe('workbook edit form', () => { await expect(page.getByText('3文字以上入力してください')).toBeVisible({ timeout: TIMEOUT }); await expect(page).toHaveURL(WORKBOOK_EDIT_URL); await expect(page.locator('input[name="title"]')).toHaveValue('ab'); - await expect(taskRows).toHaveText(taskRowTexts); + // The table must show the submitted tasks, not the workbook's original tasks + await expect(taskRows).toHaveText(editedRowTexts); }); }); diff --git a/e2e/workbook_order.spec.ts b/e2e/workbook_order.spec.ts index b4ba4c1ef..cf2891252 100644 --- a/e2e/workbook_order.spec.ts +++ b/e2e/workbook_order.spec.ts @@ -216,7 +216,7 @@ test.describe('workbook order page', () => { expect(categories.split(',')).toContain(SolutionCategory.GRAPH); }); - test('keeps kanban state while replacing filter URL', async ({ page }) => { + test('replaces filter URL without reloading data or adding history', async ({ page }) => { await page.goto( `${ORDER_URL}?tab=solution&categories=${SolutionCategory.PENDING},${SolutionCategory.GRAPH},${SolutionCategory.DATA_STRUCTURE}`, ); From 444e4daf907bc3e1016121aafed8f2f5bf185f89 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Mon, 19 Oct 2026 12:40:09 +0000 Subject: [PATCH 06/11] docs: describe how SvelteKit 3 rejects removed import aliases The subpath imports section still assumed Kit 2, where lint was the only guard against $lib. On Kit 3, $lib fails in every form (vi.mock included, via module_removed_lib), while vi.mock('$features/...') is silently ignored, which is why the rg sweep remains. Co-Authored-By: Claude Opus 5.5 --- docs/guides/architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md index 7a1c38a64..8eec1baa1 100644 --- a/docs/guides/architecture.md +++ b/docs/guides/architecture.md @@ -155,8 +155,8 @@ import type { PageServerLoad } from './$types'; import { formatDate } from '$lib/utils/format'; ``` -`$lib` は SvelteKit 2 の習慣で書かれやすいため、oxlint と ESLint の `no-restricted-imports` でも禁止し、`#lib` への書き換えを案内している。lint が検出するのは import・export 宣言(`import type`・`export … from` を含む)と `.ts` の動的 `import()` で、`vi.mock('$lib/...')` と `.svelte` 内の動的 `import()` は検出しない。 -import の specifier をまとめて変えたときは、`rg -n '\$lib|\$features|@/' src e2e prisma` で残りが無いことも確かめる。 +`$lib` は、静的・動的 import、`.ts`・`.svelte`、`vi.mock` のどれで書いても型チェックか Vite(dev・build・Vitest)で失敗し、SvelteKit が `module_removed_lib` で `#lib` への書き換えを案内する。oxlint と ESLint の `no-restricted-imports` でも禁止しているので、commit 前の lint でも止まる。 +`$features`・`@/` は import なら型チェックで失敗するが、`vi.mock('$features/...')` は解決されないまま無視され、mock が効かずに本物のモジュールが使われる。specifier をまとめて変えたときは、`rg -n '\$lib|\$features|@/' src e2e prisma` で残りが無いことを確かめる。 `$types` は SvelteKit が生成する route 型専用で、domain type は `src/**/types/` に置く。 From 810d7260f12be3961ab2db9f1ea81cf7f4318cbe Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Wed, 21 Oct 2026 04:27:44 +0000 Subject: [PATCH 07/11] docs: clarify that SvelteKit 3 dropped svelte.config.js The previous note read as if the file was removed by choice. Link the official migration guide, which states svelte.config.js is no longer supported. Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 893e015d1..64d742cc0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ - [Node.js](https://nodejs.org): v24.x - 汎用フレームワーク - [Svelte](https://svelte.dev/): v5.x。Runes mode で運用中 - - [SvelteKit](https://svelte.dev/docs/kit): v3.x。設定は `vite.config.ts` の `sveltekit()` に書く(`svelte.config.js` は無い) + - [SvelteKit](https://svelte.dev/docs/kit): v3.x。設定は `vite.config.ts` の `sveltekit()` に書く(`svelte.config.js` は Kit 3 で[廃止](https://svelte.dev/docs/kit/migrating-to-sveltekit-3)) - UIライブラリ - [Flowbite Svelte](https://flowbite-svelte.com/) - コンポーネントライブラリ - [Flowbite](https://flowbite.com/)も利用可能 From cfa168892f0a9d03d407650857434ee1c5a6d9f8 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Wed, 21 Oct 2026 04:30:31 +0000 Subject: [PATCH 08/11] docs: describe the $lib lint rule from the SvelteKit 3 side Kit 3 already explains $lib imports through module_removed_lib, so the rule's remaining value is catching them at pre-commit, where lefthook runs ESLint and oxlint but not svelte-check or Vite. Co-Authored-By: Claude Opus 5.5 --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index bb38ab825..0a6656ee4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -80,7 +80,7 @@ export default [ 'svelte/no-useless-mustaches': 'warn', 'svelte/prefer-writable-derived': 'warn', // New in 3.6.0 - prefer $derived over $state+$effect 'svelte/valid-prop-names-in-kit-pages': 'warn', // Allow props other than data/errors in pages - // Points Kit 2 habits to #lib with a clearer message than the type error; oxlint covers JS/TS files + // Stops $lib at pre-commit, before svelte-check or Vite reports it; oxlint covers JS/TS files 'no-restricted-imports': [ 'error', { From 5744db3244e307cdcc4f9b24775e8cc4f7a459bf Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Wed, 21 Oct 2026 06:02:24 +0000 Subject: [PATCH 09/11] test(e2e): cover submission status updates and saved /problems tabs Two manual checks for the SvelteKit 3 upgrade had no E2E coverage: - submission_status: change the status of the only seeded ABS task as guest, reload, and read the checked item in the dropdown; the original status is restored in finally. The dropdown updates the page without checking the response, so only a reload proves the server saved it. - problems_tabs / problems_contest_table: the selected problem list tab and the ABC 319 onwards group survive a reload (localStorage). Each test fails at its post-reload assertion when the server save or the localStorage write is disabled (mutation-checked, then reverted). Co-Authored-By: Claude Opus 5.5 --- e2e/problems_contest_table.spec.ts | 17 +++++++ e2e/problems_tabs.spec.ts | 28 +++++++++++ e2e/submission_status.spec.ts | 74 ++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 e2e/problems_tabs.spec.ts create mode 100644 e2e/submission_status.spec.ts diff --git a/e2e/problems_contest_table.spec.ts b/e2e/problems_contest_table.spec.ts index e80757b1e..cb3ced323 100644 --- a/e2e/problems_contest_table.spec.ts +++ b/e2e/problems_contest_table.spec.ts @@ -9,6 +9,8 @@ const XDPC_GROUP_TABLE_TITLES = [ '組合せゲーム 24 題', "AAL Contest 001: Let's use segtree!", ]; +const ABC319_ONWARDS_GROUP_ARIA_LABEL = 'Filter contests from ABC 319 onwards'; +const ABC319_ONWARDS_TABLE_TITLES = ['AtCoder Beginner Contest 319 〜(7 問制)']; test.describe('xDPC group in contest table', () => { test('shows Game 24 and AAL Contest 001 tables after FPS 24 in the xDPC group', async ({ @@ -22,3 +24,18 @@ test.describe('xDPC group in contest table', () => { await expect(page.getByRole('heading', { level: 2 })).toHaveText(XDPC_GROUP_TABLE_TITLES); }); }); + +test.describe('contest type selection', () => { + // ABC 319 onwards is the group most users open + test('keeps the selected contest type after reload', async ({ page }) => { + await page.goto('/problems'); + + await page.getByRole('button', { name: ABC319_ONWARDS_GROUP_ARIA_LABEL }).click(); + await expect(page.getByRole('heading', { level: 2 })).toHaveText(ABC319_ONWARDS_TABLE_TITLES); + + await page.reload(); + + // The default group is ABS, so these headings only appear when the choice was saved + await expect(page.getByRole('heading', { level: 2 })).toHaveText(ABC319_ONWARDS_TABLE_TITLES); + }); +}); diff --git a/e2e/problems_tabs.spec.ts b/e2e/problems_tabs.spec.ts new file mode 100644 index 000000000..3a87ea1d3 --- /dev/null +++ b/e2e/problems_tabs.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; + +const PROBLEMS_URL = '/problems'; +const GRADE_TAB_TITLE = 'グレード別'; +const CONTEST_TABLE_TAB_TITLE = 'コンテスト別(アルファ版)'; + +test.describe('problem list tabs', () => { + test('keeps the selected problem list tab after reload', async ({ page }) => { + await page.goto(PROBLEMS_URL); + + await page.getByRole('tab', { name: GRADE_TAB_TITLE }).click(); + await expect(page.getByRole('tab', { name: GRADE_TAB_TITLE })).toHaveAttribute( + 'aria-selected', + 'true', + ); + + await page.reload(); + + await expect(page.getByRole('tab', { name: GRADE_TAB_TITLE })).toHaveAttribute( + 'aria-selected', + 'true', + ); + await expect(page.getByRole('tab', { name: CONTEST_TABLE_TAB_TITLE })).toHaveAttribute( + 'aria-selected', + 'false', + ); + }); +}); diff --git a/e2e/submission_status.spec.ts b/e2e/submission_status.spec.ts new file mode 100644 index 000000000..a71be1bce --- /dev/null +++ b/e2e/submission_status.spec.ts @@ -0,0 +1,74 @@ +import { test, expect, type Locator, type Page } from '@playwright/test'; + +import { loginAsUser } from './helpers/auth'; + +const PROBLEMS_URL = '/problems'; +// The only ABS task in the seed, so it is the only cell in the default contest table group +const TASK_TITLE = 'Welcome to AtCoder'; +const STATUS_LABELS = ['AC', '解説AC', '挑戦中', '未挑戦'] as const; + +type StatusLabel = (typeof STATUS_LABELS)[number]; + +function statusItem(page: Page, label: StatusLabel): Locator { + return page.getByRole('button', { name: label, exact: true }); +} + +async function openStatusDropdown(page: Page): Promise { + const taskCell = page + .getByRole('cell') + .filter({ has: page.getByRole('link', { name: TASK_TITLE }) }); + + await taskCell.getByRole('button', { name: 'Update submission status' }).click(); + await expect(statusItem(page, 'AC')).toBeVisible(); +} + +// The cell shows the status only as a background color, so read the check icon in the dropdown +async function readCheckedStatus(page: Page): Promise { + await openStatusDropdown(page); + + for (const label of STATUS_LABELS) { + if ((await statusItem(page, label).locator('svg').count()) > 0) { + return label; + } + } + + throw new Error('No submission status is checked'); +} + +async function selectStatus(page: Page, label: StatusLabel): Promise { + const saved = page.waitForResponse( + (response) => response.url().includes('?/update') && response.request().method() === 'POST', + ); + + await statusItem(page, label).click(); + expect((await saved).ok()).toBe(true); +} + +// Reloads first so the result reflects the server, not the optimistic update in the page +async function setStatusAfterReload(page: Page, label: StatusLabel): Promise { + await page.reload(); + + if ((await readCheckedStatus(page)) !== label) { + await selectStatus(page, label); + } +} + +test.describe('submission status in the contest table', () => { + test('keeps the updated status after reload', async ({ page }) => { + await loginAsUser(page); + await page.goto(PROBLEMS_URL); + + const originalStatus = await readCheckedStatus(page); + const updatedStatus: StatusLabel = originalStatus === 'AC' ? '挑戦中' : 'AC'; + + try { + await selectStatus(page, updatedStatus); + await page.reload(); + + // The dropdown updates the page without checking the response, so only a reload proves it was saved + expect(await readCheckedStatus(page)).toBe(updatedStatus); + } finally { + await setStatusAfterReload(page, originalStatus); + } + }); +}); From 79ebaf999c44db0791c57716cdce85b6ac6ad1d8 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Wed, 21 Oct 2026 06:14:38 +0000 Subject: [PATCH 10/11] docs: add SvelteKit 3 and adapter-vercel 7 upgrade dev notes Co-Authored-By: Claude Sonnet 5.5 --- .../codemod_subpath_imports.py | 92 +++++ .../plan.md | 338 ++++++++++++++++++ .../summary.md | 85 +++++ .../survey.md | 327 +++++++++++++++++ 4 files changed, 842 insertions(+) create mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py create mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md create mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md create mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py new file mode 100644 index 000000000..63dd5333a --- /dev/null +++ b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py @@ -0,0 +1,92 @@ +"""Rewrite alias specifiers to package.json subpath imports with an explicit extension. + +Usage: python3 -I codemod_subpath_imports.py + +- lib: `$lib/...` -> `#lib/...` +- features: `$features/...` and `@/features/...` -> `#features/...` + +The rewrite is idempotent, so it can be re-run on a branch after rebasing onto the change. +""" +import os +import re +import sys + +root = sys.argv[1] +target = sys.argv[2] + +# (specifier prefix, source directory relative to the root, replacement prefix) +RULES = { + 'lib': [('$lib/', 'src/lib', '#lib/')], + 'features': [ + ('$features/', 'src/features', '#features/'), + ('@/features/', 'src/features', '#features/'), + ], +} + +if target not in RULES: + sys.exit(f'unknown target: {target} (expected one of {", ".join(RULES)})') + +rules = [ + (re.compile(r"""(['"])""" + re.escape(prefix) + r"""([^'"]+)\1"""), os.path.join(root, source), replacement) + for prefix, source, replacement in RULES[target] +] + +unresolved = [] +changed_files = 0 +changed_specs = 0 +file_changes = 0 +current_file = '' + + +def resolve(base_dir, path): + # a `.js` specifier is kept as is because TypeScript and Vite map it to the `.ts` source; + # other existing files keep their extension (e.g. `.svelte` components) + if path.endswith('.js') or os.path.isfile(os.path.join(base_dir, path)): + return path + # `x.svelte` that only exists as `x.svelte.ts` is a runes module and becomes `x.svelte.js` + candidates = [ + (path + '.ts', path + '.js'), + (path + '.svelte.ts', path + '.svelte.js'), + (path + '/index.ts', path + '/index.js'), + (path + '.js', path + '.js'), + ] + for source, specifier in candidates: + if os.path.isfile(os.path.join(base_dir, source)): + return specifier + return None + + +def make_replacer(base_dir, replacement): + def replace_specifier(match): + global changed_specs, file_changes + resolved = resolve(base_dir, match.group(2)) + if resolved is None: + unresolved.append((current_file, match.group(0))) + return match.group(0) + file_changes += 1 + changed_specs += 1 + return f'{match.group(1)}{replacement}{resolved}{match.group(1)}' + + return replace_specifier + + +for base in ('src', 'e2e', 'prisma'): + for dirpath, dirnames, filenames in os.walk(os.path.join(root, base)): + dirnames[:] = [name for name in dirnames if name not in ('node_modules', '.svelte-kit')] + for filename in filenames: + if not filename.endswith(('.ts', '.svelte', '.js')): + continue + full = os.path.join(dirpath, filename) + current_file = os.path.relpath(full, root) + text = open(full, encoding='utf-8').read() + file_changes = 0 + new_text = text + for pattern, base_dir, replacement in rules: + new_text = pattern.sub(make_replacer(base_dir, replacement), new_text) + if file_changes: + open(full, 'w', encoding='utf-8').write(new_text) + changed_files += 1 + +print(f'files changed: {changed_files}, specifiers changed: {changed_specs}, unresolved: {len(unresolved)}') +for item in unresolved: + print('UNRESOLVED', item) diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md new file mode 100644 index 000000000..8547cc815 --- /dev/null +++ b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md @@ -0,0 +1,338 @@ +# SvelteKit 3 / adapter-vercel 7 移行計画 + +## 概要 + +SvelteKit 2.70.3 → 3.1.0、adapter-vercel 6.3.4 → 7.0.1 に移行する。Kit 3 対応に必要な Superforms 2.31.0 → 3.0.0 の更新と、alias(`$lib`・`$features`・`@`)から `package.json#imports`(`#lib`・`#features`)への置換も含める。 +外部の依存ブロッカーはなく、移行は可能と判断した。根拠・影響範囲・検証結果は [survey.md](./survey.md) にまとめた。 +2026-10-21 時点で、#4115・PR A(#4190)・PR A2(#4191)はマージ済み。PR B は branch `#4192` で実装・手元の確認・preview での手動確認まで終わり、push と production の確認が残る。要点は [summary.md](./summary.md) にまとめた。計画を立てた時の基準は 2026-10-18 JST の `origin/staging`(`f02b7238`)。 + +## PR の構成と順序 + +| 順 | PR | 内容 | 土台 | マージの条件 | +| --- | ----- | ----------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------ | +| 1 | #4115 | Superforms 2.31.0 → 3.0.0(依存と lockfile のみ) | Kit 2.70.3 | 2026-10-18 にマージ済み(`dc9f2471`)。マージ前に5画面と既存 E2E を確認した(Phase 0)。 | +| 2 | PR A | `$lib` → `#lib`(282ファイル・676 specifier の機械置換) | Kit 2.70.3 | #4115 の確認後。共同開発者の新機能追加より前、遅くとも1か月以内(Phase A)。 | +| 3 | PR A2 | `$features`・`@` → `#features`(144ファイル・286 specifier の機械置換) | Kit 2.70.3 | PR A のマージ後、PR B より前(Phase A2)。 | +| 4 | PR B | Kit 3.1.0・adapter-vercel 7.0.1 と、設定・型・state・フォームの移行 | Kit 3.1.0 | Phase 1 の spike と Phase 3 の E2E が通ってから preview・production へ進む(Phase 1〜4)。 | + +1本ずつマージするのは、不具合が出た時に原因の PR を切り分けるため。CI は E2E を実行しないので、各段階の実ブラウザー確認と E2E は手元で行う。 + +## 実装フェーズ + +### Phase 0: #4115(Superforms 3.0.0) + +- [x] PR 本文の rebase のチェックボックスで Renovate に rebase させ、今の staging の上で CI を再実行する(workflow の re-run は当時の合成コミットを使うので、rebase の代わりにならない)。2026-10-18 に rebase し、head `5833c1d0` は staging `f02b7238` の直上。CI は build (24)・preview 成功、production は skipped。 +- [x] lockfile の差分が Superforms 本体と optional validator に限られることを確認する。pnpm 12.14.0 の再解決で書き換えは出ず、supply-chain policy の検査も通った(survey の「Superforms 3.0.0(#4115)」)。 +- [x] マージ前に、v3 で内部のページ状態の同期が変わるため、`superForm` を使う5画面を実ブラウザーで確認する。CI は E2E を実行しないので、PR の branch を手元で checkout して既存 E2E(`signin / redirect_after_login / votes / workbook_edit / workbook_order`)も流す。 + - 画面の確認は、#4115 の CI が作った preview deploy でも行える。preview は本番とは別の DB(`PREVIEW_DATABASE_URL`)を使う。 + - staging ドメインは他の PR の CI でも付け替わる(`ci.yml` の alias の step の条件が `github.ref != 'refs/heads/main'` のため)。確認には #4115 のデプロイ URL を使う。 + - 結果(2026-10-18): 5画面は #4115 の preview deploy で確認した。E2E は head `5833c1d0` を手元で checkout し(Superforms 3.0.0・Playwright 1.64.1、ローカルの `test_db`)、5本の spec の56件がすべて成功した。 + +| 画面 | 確認すること | +| -------------------- | --------------------------------------------------------------------------- | +| ログイン・新規登録 | 入力エラー時に入力値が残る、成功後に redirect される、ゲストログイン。 | +| 問題集の作成・編集 | 問題リスト(JSON の入れ子データ)が送信・再表示される、失敗時に入力が残る。 | +| 管理: アカウント移行 | 成功・失敗のメッセージ、一般ユーザーが実行できない。 | + +- [x] 問題がなければ staging にマージする。production は `main` への push でしか deploy されないので、staging へのマージは本番に影響しない。2026-10-18 にマージした(merge commit `dc9f2471`、tree は検証した head `5833c1d0` と同一)。staging への push の [CI](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/actions/runs/39128896093) も成功した。 +- [x] 挙動の変更が必要になったら、Phase 3 の確認表にあるテストを先に書き、対応する層だけを修正する。依存だけの更新では新規 unit test を追加しない。 + - 結果: 不要だった。使用中の API(`superValidate`・`superForm`・`zod4`)は不変で、preview での5画面の確認・手元の E2E 56件・staging の CI がすべて通った。マージ後の `svelte-check` も 0 errors・30 warnings で、2.31.0 のときと件数が変わらない。 + +### Phase A: PR A(`$lib` → `#lib`、Kit 2 上) + +- [x] `package.json` に `"imports": { "#lib/*": "./src/lib/*" }` を追加する。 +- [x] [codemod_subpath_imports.py](./codemod_subpath_imports.py) の `lib` モード(`python3 -I codemod_subpath_imports.py lib`)で置換し、`pnpm format` を実行する。拡張子の規約は survey の「拡張子の規約」に従う。 +- [x] `$lib` の import を禁止する `no-restricted-imports` を、`.ts` 向けに `.oxlintrc.json`、`.svelte` 向けに `eslint.config.mjs` へ追加する。Kit 2 は `$lib` を組み込みで解決するので、規則が無いと新しいコードの `$lib` に PR B まで気付けない。 +- [x] `docs/guides/architecture.md`・`agent-rules/testing.md`・`testing-e2e.md`・`server-cache.md`・`prisma-db.md` の `$lib` 記述を、`#lib` と拡張子の規約に更新する。 +- [x] `pnpm check / lint / test:unit --run / build` と既存 E2E(`signin / votes / workbook_edit / workbook_order`)で確認する。置換・設定・docs の変更なのでテスト先行は省略する。 + - 結果(2026-10-18、branch `refactor/lib-subpath-imports`): lint 成功(oxlint の既存の警告1件のみ)、check は 0 errors・30 warnings(置換前と同数)、unit test は97ファイル・2726件が成功、build 成功。E2E は `redirect_after_login` も含めた5本の spec の56件が成功。seed の import 経路も tsx で解決できる。変更は290ファイル(置換282・`package.json`・`.oxlintrc.json`・`eslint.config.mjs`・docs 5ファイル)。 +- [ ] 共同開発者の新機能の着手前、遅くとも1か月以内にマージし、連絡する。作業中の branch は rebase 後に codemod を再実行してもらう。codemod は repo に入れず、PR A の本文に全文を貼って共有する(レビュー手順・rebase 手順と一緒に記載)。 +- [x] cross-review は、手編集ではなくスクリプト出力なので AGENTS.md の除外規定(生成物・機械的変更)に照らして要否を判断する。レビュー者は `staging` で codemod と `pnpm format` を再実行し、diff の一致を確かめられる。 + - 結果(2026-10-18、commit `f629666a`): import の規約という共有 architecture の変更に当たるため、Codex の cross-review を受けた。staging(`dc9f2471`)で codemod と prettier を再実行すると `src/`・`e2e/`・`prisma/` が commit と一致することも確認した。Critical・Severe の指摘はなく、誤った置換も見つからなかった。 + - Minor(解消済み): lint 規則は `vi.mock('$lib/...')` と `.svelte` 内の動的 `import('$lib/...')` を検出しない(`.ts` の `import type`・`export … from`・`export * from`・動的 `import()` は oxlint が検出することを実測した)。`architecture.md` の「禁止している」は、この検出範囲を補足すると正確になる。 + - PR A では `architecture.md` に検出範囲を補足した。Kit 3 では、`vi.mock('$lib/...')` は Vitest が `module_removed_lib` で失敗し、`.svelte` の動的 `import('$lib/...')` は check が失敗するので、検出漏れ自体が無くなった。PR B(`444e4daf`)で `architecture.md` をこの挙動に合わせた(Phase 4 の cross-review の項目を参照)。 + - Nit: `git diff --check` の結果が review の資料に無かった。commit の範囲で実行して問題なし。 + +### Phase A2: PR A2(`$features`・`@` → `#features`、Kit 2 上) + +- [x] PR A のマージ後に着手し、`package.json#imports` に `"#features/*": "./src/features/*"` を追加する。PR A は #4190(`485ce861`)でマージ済み。branch は `refactor/features-subpath-imports`。 +- [x] codemod の `features` モードで `$features/...` と `@/features/...` を置換し、`pnpm format` を実行する。 +- [x] `rg -n '\$lib|\$features|@/features' src e2e prisma` で残りが 0件であることを確かめる。lint 規則と型チェックは `vi.mock` の specifier と `.svelte` 内の動的 `import()` を検出しないため。 +- [x] `svelte.config.js` の `alias` を削除する(`@`・`$features`・`$features/*` と、PR A で参照が 0 になった `$lib` の2行)。残った `$features` は型チェックで失敗するので、リント規則は要らない。 +- [x] `docs/guides/architecture.md`・`agent-rules/testing-e2e.md` の `$features` 記述を更新する。あわせて `svelte.config.js` と `tsconfig.json` の alias に関するコメントも更新した。 +- [ ] 確認・連絡・cross-review は Phase A と同じ。PR B の前にマージする。 + - 確認の結果(2026-10-18、未 commit の作業ツリー): lint 成功(oxlint の既存の警告1件のみ)、check は 0 errors・30 warnings(変更前と同数)、unit test は97ファイル・2726件が成功、build 成功、E2E は5本の spec の56件が成功、seed の import 経路も tsx で解決できる。staging(`485ce861`)で codemod と prettier を再実行すると、git 管理下の `src/`・`e2e/`・`prisma/` が作業ツリーと一致する。変更は149ファイル(置換144・`package.json`・`svelte.config.js`・`tsconfig.json`・docs 2ファイル)。 + - alias を外した後の確認: `$features` を import すると型チェックで `Cannot find module` になる。Kit 2 の生成 tsconfig には `$lib` の paths だけが残る(組み込み。再流入は PR A の lint 規則で防ぐ)。 + - Codex の cross-review(未 commit の diff): Critical・Severe の指摘なし、置換の誤りなし。Minor は、`architecture.md:147` の grep の例が `@/` を含まない点と、リポジトリ全体の残存確認が無い点。全体を検索すると `.coderabbit.yaml:28` に `$lib/server/database` が残っていた(PR A での取りこぼし)。`git diff --check` は問題なし。grep の例に `@/` を加え、`.coderabbit.yaml` を `#lib/server/database.js` に直して commit した(`bbb40c1e`、150ファイル)。 + +本 PR のスコープ外: + +- `TaskTableBodyCell.svelte` の feature 間 import(survey の「気付いた既存の問題」)。置換しても違反は残るが、直すのは別タスクとする。 + +### Phase 1: PR B・設定と module 解決 + +- [x] 独立した作業環境で `npx sv migrate sveltekit-3` の spike を行い、`pnpm build / check / lint / test:unit --run` が通ることを確認する。通らない場合は原因を切り分けるまで本線に取り込まず、Superforms 側の fork や shim で埋めない。特に次の3点を見る。 + - Vitest が Kit 3 で `$app/server` を解決できるか(`src/features/workbooks/zod/schema.test.ts` が Superforms の `/server` を実体で import する)。解決できなければ、同テストの import を root の `sveltekit-superforms` に切り替える。→ 解決できた(unit test 2726件が成功)。 + - 移設後の `dynamicCompileOptions` が node_modules の `SuperDebug.svelte` に効き、build が通るか。→ `sveltekit({...})` の直下に置けば効き、build が通る。`sv migrate` が出力した `vitePlugin: { dynamicCompileOptions }` は、Kit 3 では `config.vitePlugin` が削除済みで `svelte-kit sync` が失敗する。 + - `#lib`・`#features` が Kit 3 でも TypeScript / Vite / Vitest で解決され、新しい警告が出ないか。`sv migrate` が `$lib` に触れる場合は、PR A の結果と食い違わないか確認する。spike の前に `rg -n '\$lib|\$features|@/' src e2e prisma` が 0件であることも確かめる(PR A・PR A2 の後に入った `vi.mock` などを拾うため)。→ 0件。`#lib`・`#features` は Kit 3 でも解決でき、新しい警告も無い(build の警告は chunk サイズの1件のみ)。 +- [x] Kit 3.1.0・adapter-vercel 7.0.1 に更新し、`svelte.config.js` を `vite.config.ts` に集約する(→「設計方針」の設定例)。tsconfig の extends を `$app/tsconfig` に変える。 +- [x] 設定・機械的な import 変更はテスト先行を省略し、`pnpm build / check / lint` と既存 unit test で確認する。 + - `#4192` への反映後も spike と同じ結果(check の 20 errors も同じ箇所)。check の失敗は Phase 2 で直す方針(2026-10-19 に決定)。commit `76a6d0c3`(5ファイル、未 push)。この commit 単体では CI の check が落ちるので、Phase 2 の後に push する。 + - spike の結果(2026-10-19、`sv` 1.1.1、Kit 3.1.0・adapter-vercel 7.0.1): build・unit test・lint は成功、check は 20 errors(すべて Phase 2 の項目。下記)。build の `.vc-config.json` は `nodejs24.x`・`hnd1`・memory 3008・maxDuration 30。 + - `sv migrate` の出力のうち採用しなかったもの: `vitePlugin` の入れ子(上記)、Kit と adapter の `^` 範囲指定(`3.1.0`・`7.0.1` に固定)、実在しない `src/lib/index.js` への bare `#lib`、Markdown の `$lib` の置換(`architecture.md` の文意が壊れ、過去の dev-notes も書き換えた)、既存コメントの移動と `/* */` への書き換え。`tsconfig.json` は codemod の形(`$app/tsconfig` が削った設定をすべて提供する)に、既存の説明コメントを残した。 + - `tsconfig.json` から消えた9個(`allowJs`・`checkJs`・`esModuleInterop`・`forceConsistentCasingInFileNames`・`isolatedModules`・`moduleResolution`・`resolveJsonModule`・`skipLibCheck`・`verbatimModuleSyntax`)は、`$app/tsconfig` が同じ値を設定するので、重複として消した。デフォルトが厳しくなったからではない。`tsc --showConfig` で Kit 2(`staging`)と Kit 3 の実際の設定を比べ、9個とも同じ値であることを確かめた(2026-10-21)。 + - 実際の設定で違うのは、Kit 3 の基本設定から来る4つだけ: `paths` から `$lib` が消えた(意図どおり)、`types` に `$app/types` が増えた(`types` は継承されず上書きされるため)、`moduleDetection: "force"`(すべてのファイルをモジュールとして扱う)、`allowImportingTsExtensions: true`(`.ts` の拡張子の import を許す。`.js` と書く規約は変えない)。 + - 残したのは、基本設定に無い `strict: true` と、Kit 2 から引き継いだ `incremental`・`sourceMap`・`outDir`。`outDir` は svelte-check の `--tsgo` を壊すので、[CI の高速化](../../2026-10-20/ci-and-typecheck-speedup/plan.md)の Phase 2 で消す。`sourceMap` は `noEmit` では効かないので、同じ時に消してよい。 + +### Phase 2: PR B・ルート型と state + +- [x] `Handle` を `@sveltejs/kit/hooks`、`SubmitFunction` を `$app/forms` に移す。`$app/environment` を `$app/env` に揃える(テストの mock も含む)。 + - 結果: ソース4ファイルとテストの mock 4ファイルを `$app/env` に揃え、コメントの docs リンクも `$app-env` に直した。Kit 3 の実行時の `$app/environment` は `../env/index.js` を再 export するだけなので、mock を先に `$app/env` へ変えてもテストは通り続けた(mock は両方の名前に効く)。 +- [x] 修正の前に、E2E `keeps kanban edits while replacing filter URL`・`restores saved workbook filters without adding a history entry` を追加する。 + - Kanban のテストは `keeps kanban state while replacing filter URL` という名前で `workbook_order.spec.ts` に追加した。ドラッグは Playwright では不安定なので、並べ替えの状態が消えない仕組み(絞り込みの変更で履歴が増えず、`__data.json` の再取得も起きない)を検証する。修正前は `$app/stores` の例外でページが描画されず失敗した。 + - 復元のテストは `workbooks_list.spec.ts` に追加した。Kit 3.1.0 の `goto` は非推奨の `replaceState` も `replace` として受け付ける(`opts.replace ?? opts.replaceState`)ため、修正前から通る回帰の監視用のテストになる。 + - どちらも、壊れた実装に一時的に差し替えて失敗することを確かめた(Kanban は `shallow`・`replace` をそれぞれ外す2通り、復元は `replace` を外す1通り)。 +- [x] `KanbanBoard.svelte` の `$app/stores` を `$app/state` と Runes に移し、`replaceState` を `goto(..., { shallow: true, replace: true, state: page.state })` へ移す。`workbooks/+page.svelte` の `{ replaceState: true }` を `{ replace: true }` にする。 + - Kit 3 の `page.url` は型の上で読み取り専用(`searchParams` は `ReadonlyURLSearchParams`)なので、`kanban.ts` の `parseInitialCategories`・`parseInitialGrades` の引数を `Pick`、`buildUpdatedUrl` の引数を `Pick` に広げた(型だけの変更で、既存の unit test 36件が成功)。 +- [x] `resolve()` の呼び出しを、route ID と pathname(leading slash なし)の区別に合わせる。純粋な URL 変換を足す場合は既存の `workbook_url_params.ts / kanban.ts` を先に調べ、隣接 unit test を先行する。 + - Kit 3 の `resolve()` は、`/` で始まる引数を route ID(`(group)` を除き `[param]` を埋める。`?`・`#` 以降は残す)、それ以外を pathname として扱う。型は既知の route ID・pathname と、それに `?`・`#` を付けたテンプレート文字列だけを受け付ける。 + - 定数を渡す6か所(`HOME_PAGE`・`EDIT_PROFILE_PAGE` など)は値がそのまま route ID なので、Kit 2 の回避用の `@ts-expect-error` を外した。`resolve('/votes', {})` の `{}` も外した。 + - `buildWorkbooksUrl` の戻り値の型を `` `/workbooks?${string}` `` にして、`workbooks/+page.svelte` の3か所の `@ts-expect-error` を外した。復元は `` `/workbooks?${savedUrl.searchParams}` `` で組み立てる(保存される値は常に `buildWorkbooksUrl` の出力)。 + - `KanbanBoard` は route ID `` `/(admin)/workbooks/order?${...}` `` で組み立てる。 + - `AuthForm` の `alternativePageLink`(`buildSignupPath()` が実行時に作る `/signup?redirectTo=...` など)は型で表せないので、`@ts-expect-error` を残し、理由を Kit 3 に合わせて書き直した。新しい URL ユーティリティは追加していないので、追加の unit test は無い。 +- [x] `page.url` を直接変更していないことを確認する。Kit 3 の `page.url` は型の上で読み取り専用なので、check が 0 errors で通ることで確かめた。 +- [x] Phase 1 の check で見つかった Kit 3 の型の変更を直す。`(admin)/tasks/[task_id]/+page.server.ts` は `Actions` を `@sveltejs/kit` の汎用型から import しているため `params.task_id` が `string | undefined` になる。`./$types` の `Actions` に替える(同じ書き方の他の7ファイルは `params` を使わないので任意)。 + +Phase 1 の check の 20 errors(Phase 2 で直す): + +| 分類 | 件数 | 箇所 | +| ------------------------------------------------------------------------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `$app/environment` が見つからない | 4 | `features/auth/server/auth.ts`・`lib/stores/local_storage_helper.svelte.ts`・`features/workbooks/stores/replenishment_workbook.svelte.ts`・`lib/actions/handle_dropdown.ts` | +| `Handle` が無い(+ 暗黙の any 2件) | 3 | `hooks.server.ts` | +| `SubmitFunction` が無い(+ 暗黙の any 2件) | 4 | `(admin)/tasks/+page.svelte`・`(admin)/tasks/_components/TaskTableForImport.svelte` | +| `$app/stores` が見つからない | 1 | `(admin)/workbooks/order/_components/KanbanBoard.svelte` | +| 不要になった `@ts-expect-error`(Kit 2 の `RouteId` の型の不具合の回避) | 6 | `VotableGrade.svelte`・`AuthForm.svelte`(2件)・`TagForm.svelte`・`users/[username]/+page.svelte`・`votes/[slug]/+page.svelte` | +| `resolve()` の引数の数 | 1 | `votes/[slug]/+page.svelte:45` の `resolve('/votes', {})`(`{}` は Kit 2 の回避策) | +| `params` の型 | 1 | `(admin)/tasks/[task_id]/+page.server.ts:71`(上記) | + +`sv migrate` の Phase 2 向けタスク(`environment`・`imports`・`app-state`・`shallow-routing`・`paths`)は、Phase 1 と同じく本線に直接かけず、コピーで出力を確かめてから採用する。 + +- 結果(2026-10-19): `environment`・`imports`・`app-state`・`shallow-routing` の出力は計画どおりで、同じ変更を手で入れた。`paths` は何も変更しなかった。整形の副作用(`x: x` → `x`、空要素の自己閉じ化、`tsconfig.json` のコメント削除)は採用しなかった。`hooks.server.ts` は import が既存コメントの上に移ったので、元の位置で import 元だけを変えた。 +- Phase 2 の確認(2026-10-19、commit `a046e268`、23ファイル、未 push): check は 0 errors・30 warnings(変更前と同数)、lint 成功(oxlint の既存の警告1件のみ)、unit test は97ファイル・2726件が成功、build 成功(deprecated の警告なし)。E2E は `signin`・`redirect_after_login`・`votes`・`workbook_edit`・`workbook_order`・`workbooks_list` の82件が成功、1件は既存の spec がデータ不足で自ら skip したもの。 +- Phase 4 への申し送り: `agent-rules/sveltekit.md` の「Static: `resolve('/')` + `@ts-expect-error`(AppTypes merging quirk)」は Kit 3 では不要になった。上記の `resolve()` の扱いに合わせて書き換える。 + +### Phase 3: PR B・フォーム・認証・error + +- [x] 先に E2E `keeps form values after a failed enhanced submission`・`returns the fail status for an enhanced submission`・`redirects to home when logging out without a session` を書く。 + - 3本とも `signin.spec.ts` に追加した。Kit 3 の branch では3本とも成功する。staging(`e3d1145c`、Kit 2)のコピーでも流し、`returns the fail status ...`(Kit 2 は 200)と `redirects to home ...`(Kit 2 は `/workbooks` に留まる)が失敗することを確かめた。どちらも Kit 3 の挙動の変化を検出できる。`keeps form values ...` は Kit 2 でも成功する(挙動は同じ)。 +- [x] 次の表の確認を行う。一般ユーザーが管理 action を実行できないことも確認する。 + - 結果は表の下に記載。 + +| 層・責務 | 対象 | 確認・テスト名 | +| -------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 共通 util / 型 | src/lib/utils/auth_forms.ts、src/lib/types/auth_forms.ts、src/test/lib/utils/auth_forms.test.ts | 既存 util を再利用し、schemaShape・errors・posted・valid の契約を保持する。unit test は Superforms を mock するので、統合対応の証拠にはしない。 | +| 認証ルート・UI(層1) | login / signup、src/lib/components/AuthForm.svelte | 実際の superForm / enhance を通して `keeps form values after a failed enhanced submission` と既存の redirect_after_login spec。入力エラー・認証失敗・成功時の session と redirectTo を確認する。ゲストログインは `fetch` 直送なので別経路として確認する。 | +| 管理ルート・UI(層1) | (admin)/account_transfer の server / page | `rejects account transfer by a non-admin user`。権限拒否・validation failure・成功時の message と入力状態を確認する。action は `fail()` ではなく通常の返り値で失敗を表すため、status の挙動変更は受けない。 | +| 問題集ルート・UI(層1) | workbooks/create、workbooks/edit/[slug]、WorkBookForm.svelte | JSON の nested data と workBookTasks が送信・再表示されること、失敗時の入力保持、成功後の再初期化を、既存の workbook_edit / workbook_order と `keeps nested workbook tasks after a failed submission` で確認する。 | +| 検証フォーム(層1) | problems、votes/[slug]、workbooks/[slug] の server load/action | zod4 の errors・constraints と投票結果の表示を既存の votes spec で確認する。superValidate だけを使う経路と、独自 enhance の経路を区別する。 | +| Kit 3 の応答・state(層1) | 上記フォーム全体、Header のログアウト | `returns the fail status for an enhanced submission` で fail(400/403/500) を確認し、戻る/進む、再 load、redirect、onError の表示も見る。ログアウトは成功を既存の `signin.spec.ts`、未ログインでの失敗を `redirects to home when logging out without a session` で固定する。 | + +確認表の結果(2026-10-19、Kit 3.1.0 の branch `#4192`、commit `2c420dac`、未 push): + +- 共通 util / 型: 変更は不要だった。`auth_forms` の unit test は Superforms を mock するので、統合の確認は下の E2E で行った。 +- 認証: `keeps form values after a failed enhanced submission`(失敗のメッセージが出て、ユーザー名が残る)と既存の `redirect_after_login` が成功。ゲストログインのボタンは本番用のパスワードを送るため、ローカルの seed では成功の経路を流せない(`AuthForm` の HACK)。成功時の応答は HTTP 200 の redirect で Kit 2 と同じ。失敗時は Kit 3 では HTTP 400 になり `response.ok` が false なのでログイン画面に留まる(Kit 2 は 200 が返り、失敗してもホームへ遷移していた)。 +- 管理: 新しい `account_transfer.spec.ts` に `rejects account transfer by a non-admin user`(ページを開くと別ページへ redirect され、直接 POST すると `/login` への redirect が返る)と、管理者で新旧に同じ名前を入れると検証のメッセージが出て入力が残るテストを追加した。万一ガードが壊れていても実データをコピーしないよう、存在しないユーザー名を使う。成功の経路はデータを書き換えるので流していない。 +- 問題集: `keeps nested workbook tasks after a failed submission` を `workbook_edit.spec.ts` に追加した(管理者が編集ページでタイトルを2文字にして送信すると、検証のメッセージが出て、タイトルと問題リストが残り、保存されない)。編集の action は検証の失敗を `fail()` ではなく通常の返り値で返すので、status の変更は受けない。作成ページは問題の検索の操作が要るので UI では流していない。成功後の再初期化もデータを書き換えるので流していない。 +- 検証フォーム: 既存の `votes` spec が成功。 +- Kit 3 の応答・state: `returns the fail status for an enhanced submission`(`fail(400)` が HTTP 400 になる)と `redirects to home when logging out without a session` が成功。戻るは Phase 2 の復元のテスト、redirect は既存の `redirect_after_login` が守る。エラーページは上記の 404・403・500 で確かめた。enhanced form の action が `error()` を投げたときの UI の表示(Superforms の `onError`)は、既存の直接 POST のテスト(`workbook_edit` の 403)で status だけを確かめている。 +- 一般ユーザーが管理 action を実行できないこと: アカウント移行(上記)と、既存の `workbook_edit` の直接 POST(403)で確かめた。 +- E2E 全体(18本の spec): 116件が成功、1件は既存の spec がデータ不足で自ら skip したもの。lint も成功。 + +- [x] 404 / 403 / 500 と `+error.svelte` の表示、`handleError` に expected error も届くことを確認する。cookie は secure / httpOnly / sameSite を維持し、session が続くことを実フォームで確認する。 + - 404・403 は新しい `error_pages.spec.ts` で固定した(未知のルートで 404、一般ユーザーが `/workbooks/create` を開くと `load` の `error(403)`)。 + - 500 は、必ず例外を投げる一時的なルートと確認用の spec を置いて確かめ、確認後に両方とも削除した(commit していない)。status は 500 で、`+error.svelte` に「エラーが発生しました」・`500`・`Internal Error`・「ホームに戻る」が出た。 + - `handleError` は本アプリでは定義していないので、Kit 3 の既定の実装が使われる。500 のときはサーバーに例外のスタックが出て、404・403 のときは出なかった(既定の実装は想定済みのエラーを記録しない)。 + - cookie は `keeps the session cookie attributes after login` で固定した(`auth_session` が `path: /`・`httpOnly`・`secure`・`sameSite: Lax`。ログイン後に session が必要な `/users/edit` を開けることも確かめる)。 + +### Phase 4: PR B・配備と規約 + +- [x] robots / sitemap / meta tags と、anonymous・logged-in・admin の SSR を確認する。 + - robots・sitemap は Phase 3 の E2E 全件(`robots.spec.ts`・`sitemap.spec.ts` を含む)で成功。 + - `pnpm build` + `pnpm preview` に curl で10パスを取得した(2026-10-19)。anonymous は公開ページが 200、保護ページが `/login?redirectTo=...` への 307。guest は利用者ページが 200、admin ページが 307。admin はすべて 200。 + - 200 のページはすべて title・description・twitter・og:* を持つ。`pageMetaTags` を設定するルートは無いので、全ページが共通の tag になるのは仕様どおり。header は anonymous にログイン・アカウント作成のリンク、guest・admin に利用者名を SSR で出す。 +- [ ] Vercel preview の build / deploy と、生成された `.vc-config.json` の runtime・region・memory・maxDuration を確認する。production は preview と別に確認する。 + - ローカルの build で生成された `.vc-config.json` は `nodejs24.x`・`hnd1`・memory 3008・maxDuration 30、framework は sveltekit 3.1.0。 + - preview と production は push 後に人が確認する(agent は push しない)。 +- [x] `docs/guides/architecture.md`(feature server の保護・alias の説明)、`agent-rules/sveltekit.md`(pathname 規則)、`auth.md`(redirect)を移行後の仕様に更新する。 + - architecture.md: Kit 3 は `src/routes`・`static` 外のすべての `server/` と `server.*`・`*.server.*` を server-only として扱う(`exports/vite/utils.js` の `is_server_only_path` で確認)。alias は `sveltekit()` に置かない。`$lib` の lint 禁止は Kit 2 の習慣への案内として残す。 + - sveltekit.md: `resolve()` は route ID(`/` 始まり、group を含めて書く)と pathname(leading slash なし)を区別し、`string` は渡せない。Kit 2 の回避用の `@ts-expect-error` は不要で、実行時の pathname を渡す `AuthForm.svelte` だけ理由付きで残る。`redirect()` は外部 location で throw する(`exports/url.js` の `validate_redirect_location`)ので、利用者入力は従来どおり `isSameOriginRedirect()` で検証する。 + - auth.md: 同じ `redirect()` の変更を Security に記載。 + - Kit 2 の記述の全体の検索(AGENTS.md・CONTRIBUTING.md・agent-rules・設定・source): AGENTS.md の stack 表記、CONTRIBUTING.md の version と設定場所、testing.md の `$app/environment` の mock、testing-e2e.md の form action の応答(`fail(N)` の status、別ページの action)、`eslint.config.mjs` のコメント、`src/app.d.ts` の旧 URL を更新した。残る言及は意図した履歴の注記だけ。 + - `e2e/signin.spec.ts` の生の `JSON.parse` を testing-e2e.md の型付き `deserializeActionResult` に置き換えた。 + - commit `b5e822aa`(未 push)。check 0 errors / 30 warnings、lint 成功、unit 2726件成功、signin E2E 6件成功、`git diff --check` 問題なし。 +- [x] PR B は共有 architecture の変更に当たりうるため、AGENTS.md に従って cross-review の要否を判断する(実装した agent とは別の agent、不可なら CodeRabbit)。 + - 共有の設定・型・docs を変えるので必要と判断し、Codex で `staging...b5e822aa` を review した(2026-10-19、`codex exec -s read-only`、資料は prompt に埋め込み)。Critical・Severe は無し。 + - Minor(確認済み): `architecture.md:78` は、`src/routes`・`static` の除外が `server.*`・`*.server.*` にもかかるように読める。実装では除外は `server/` ディレクトリの判定だけにかかる。 + - Minor(確認済み): `workbook_order.spec.ts` の `keeps kanban state while replacing filter URL` は未保存の drag を作らないので、`__data.json` の再取得と履歴の追加は検出できるが、ローカル状態を初期データに戻す回帰は検出できない。テスト名が検証範囲より広い。 + - Minor(可能性あり): `workbook_edit.spec.ts` の `keeps nested workbook tasks after a failed submission` は問題リストを変えずに失敗させるので、nested data の送信が壊れてサーバーの元データに戻る回帰を通しうる。 + - 確認事項は解消: prettier・lefthook・`ci.yml` を含む repo 全体に `svelte.config.js`・`.svelte-kit/tsconfig` への参照は無い。Vercel の配備確認は上の項目で扱う。 + - ユーザーの選択で3件とも対応した。`architecture.md:78` は、ファイル名による判定(場所を問わない)と `server/` ディレクトリによる判定(`src/routes`・`static` を除く)を分けて書いた。Kanban のテスト名は `replaces filter URL without reloading data or adding history` に絞った(drag の E2E は不安定になりやすいので足さない)。workbook_edit のテストは、最後の行を削除してから失敗させ、失敗後も削除後の4行が残ることを確認する `keeps edited nested workbook tasks after a failed submission` にした。 + - 変異の確認: 編集ページの `superForm` に、更新のたびに元の問題リストへ戻す `onUpdate` を一時的に入れると、新しいテストは最後の assertion(5行が表示)で失敗した。元のテストは元のリストと比べるので、この変異を通す。変異は戻した。 + - 同じ時に、`architecture.md` の 6節(subpath imports)が Kit 3 で使うもの・使わないものを区別しにくいという指摘を受け、「使う」「使わない」「拡張子」の箇条書きと OK・NG の例に再編した。 + - format・lint・check(0 errors / 30 warnings)・`git diff --check` は成功。E2E は workbook_edit・workbook_order の 21件が成功。 + - 6節の lint の段落も Kit 2 前提(lint だけが `$lib` を止める)だったので、一時ファイルで Kit 3 の挙動を確かめて書き直した(2026-10-19)。`$lib` は `vi.mock('$lib/...')` でも Vitest が `module_removed_lib` で失敗し(Kit の Vite plugin の `resolveId` が `$lib` を検出する)、`.svelte` の動的 `import('$lib/...')` は check が `Cannot find module` で失敗する。`vi.mock('$features/...')` は解決されないまま無視され、本物のモジュールが使われる。残る穴はこれだけなので、`rg` の確認はその理由で残した。一時ファイルは削除した。 + +### 手動確認: PR B の preview・配備 + +unit test と E2E が通らない操作のうち、Kit 3 の変更が当たるものを手で確認する。1〜7 は preview で、成功条件の smoke test(ログイン・投票・編集)と一緒に行う。 +依存パッケージで Kit 3 が削除した `$app/stores`・`$app/environment` を参照するのは eslint-plugin-svelte の lint 規則だけで、実行時に壊れるものは無い(2026-10-19 に `node_modules` を検索)。 + +優先度・高(E2E が無く、Kit 3 の変更が直接当たる): + +- [x] 1. 提出状況の更新: /problems・問題集の詳細・利用者ページで、ドロップダウンとモーダルから状態を AC などに変え、表示が変わり、リロード後も残る。最もよく使う操作だが E2E が無く、`use:enhance` の form action は Kit 3 で失敗時の応答(`fail(N)` の status)が変わった。 + - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 +- [x] 2. admin の問題インポート(/tasks): コンテストの取得(`?/fetch`)から問題の作成(`?/create`)まで通す。Phase 2 で型の import を直した独自の enhance callback と `goto(result.location)` を通る。外部 API を使うので E2E が無い。 + - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 +- [x] 3. 新規登録とアカウント設定: signup の入力エラーの表示と登録の成功、AtCoder ID の検証コードの発行・確認・リセット。どれも `fail(N)` の status が変わったフォーム。 + - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 + - 「確認」は devcontainer では必ず失敗し、Kit 3 では HTTP 500 として見える(2026-10-21 に確認)。#4061(`6696b604`)で、秘密の値をコンテナに置かないため `CONFIRM_API_URL` を設定しない方針にし、`compose.yaml` で空の `.devcontainer/empty.env` を `.env` に重ねている。そのため `process.env.CONFIRM_API_URL` が無く、action が `fail(500)` を返す。seed の `admin`・`guest` は連携済みなので、開発には支障が無い。 + - 同じリクエストを Kit 2 に送ると、応答の中身は同じ `{"type":"failure","status":500,…}` で、HTTP status だけが 200 だった。Kit 3 で壊れたのではなく、`fail(N)` の status がそのまま返るようになって見えただけ。「発行」と「リセット」は手元の Kit 2・Kit 3 とも 200 で成功した。 + - 「確認」の成功は、`CONFIRM_API_URL` が設定された Vercel の preview で確かめる。→ Kit 3 を仮に deploy した preview で、本人確認が成功した(2026-10-21、ユーザーが確認)。 + - PR B のスコープ外(Kit 2 からある問題で、別 PR で直す。調査は [atcoder-verification-failures/survey.md](../../2026-10-21/atcoder-verification-failures/survey.md)): + - 本番の DB で、本人確認が完了していないケースがときどきある。検証コードに有効期限は無く、失敗の理由がログにも画面にも残らないため、原因はまだ決められない。 + - `users/edit/+page.svelte` は `load` の `data.message`(常に空)だけを表示し、action が返す `form.message` を表示しないので、成功・失敗のメッセージが画面に出ない。 + +優先度・中: + +- [x] 4. 選択状態の保存(`browser` の import 元を `$app/env` に変えた store): /problems のコンテスト種別のタブと問題一覧のタブを切り替えてリロードし、選択が残る。/workbooks で条件を選び、別のページから戻ると条件が復元される。unit test は mock なので、実際のブラウザーでの保存は未確認。 + - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 +- [x] 5. admin の残りの画面(admin でログインして操作する): + - 結果: Kit 3 を仮に deploy した preview で、下の3画面の操作をすべて確認した(2026-10-21、ユーザーが確認)。 + - グレードの設定(/tasks/grade): 問題のグレードを変えると保存され、リロード後も残る。`use:enhance` の form action(`?/setTaskGrade`)で、失敗時の status が変わったフォーム。 + - 問題の編集(/tasks/[task_id]、例: `/tasks/abc001_a`): グレードを変えて保存(`?/update`)すると保存され、別の問題の値は変わらない。Phase 2 で `Actions` を `@sveltejs/kit` の汎用の型から `./$types` に替えた画面(`params.task_id` が `string | undefined` になるのを直した、型だけの変更)。 + - タグの編集(/tags/[tag_id]): タグ名を変えて保存すると保存される。「タグ一覧へもどる」と、問題へのリンク(Phase 2 で `resolve()` を直した `TagForm.svelte`)が正しいページを開く。 + - `/tasks/[task_id]` は devcontainer では存在する問題でも 500 になるので、preview で確認する(2026-10-21 に確認)。`load` が表示のたびに `taskTagsApiService.getTaskTags()` で Google Apps Script(`script.google.com`)からタグを取得し、失敗するとそのまま投げる。devcontainer の外向き通信の firewall(`30b5f207`、`.devcontainer/init-firewall.sh`)は `script.google.com` を許可していないので、`fetch failed`(`EHOSTUNREACH`)で失敗する。Kit 2(`staging` のコピー)でも同じく 500 で、Kit 3 の regression ではない。→ Kit 3 を仮に deploy した preview で、`/tasks/[task_id]` の閲覧とグレードの保存を確認した(2026-10-21、ユーザーが確認)。 + - PR B のスコープ外(Kit 2 からある問題で、別 PR で直す): + - タグの取得に失敗すると、問題の編集画面全体が 500 になる。本番でも Google Apps Script が遅い・落ちていると起きうる。取得に失敗したら DB のタグだけで表示する案がある。 + - Google Apps Script の URL が `src/lib/constants/urls.ts` ではなく `tasktagsApiService.ts` に直接書かれている。firewall の一覧は「アプリが呼ぶ外部 API(`urls.ts`)」を基準にしているため、漏れた。`urls.ts` に移すか、firewall で許可するか(Apps Script の応答は `script.googleusercontent.com` へ転送されるはずで、エージェントが出られる範囲が広がる)を決める。 +- [x] 6. 問題集の作成(/workbooks/create): 入力エラーを出し、直して保存すると問題リストも保存される。編集は E2E があるが、作成には無い。 + - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 +- [x] 7. 画面遷移: header のドロップダウン(利用者メニュー・外部リンク)が開閉する。別のページへの遷移では spinner が出て、同じページでの条件変更では出ない。ブラウザーの戻る・進むが正しく動く。 + - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 + +ローカル: + +- [x] 8. `pnpm dev`: 起動・HMR と、terminal とブラウザーの console に新しい警告が出ないか。build の警告は確認済みだが、dev server は未確認(「検証手順と完了条件」の「build と dev に出る新しい警告・エラーはすべて調査する」)。 + - 結果(2026-10-21): `#4192`(`cfa16889`)を `git archive` したコピーで `vite dev` を起動し、Playwright で anonymous・guest・admin の16ページを開いた(初回と2回目)。起動と HMR は正常で、新しい警告は無い。 + - ブラウザーの console は、Kit 2(`staging` のコピー)と同じ3種類だけ: firewall が `fonts.googleapis.com`・`www.googletagmanager.com` を許可しないための `net::ERR_ADDRESS_UNREACHABLE`、flowbite-svelte の `Tabs` の deprecated の警告2種(`ulClass`・`contentClass`)。 + - HMR: `/about` のコンポーネントを書き換えると、ページ全体を再読み込みせずに反映され、元に戻した変更も同じく反映された。console のエラーは無い。 + - terminal: 依存の事前スキャンが `src/routes/about/SectionSnippets.svelte` の snippet の export で `PARSE_ERROR` になり、依存の事前バンドルを飛ばす(Kit 2 も同じ9件)。そのため初回の表示では、依存を見つけるたびに `optimized dependencies changed. reloading` が起きる。PR B のスコープ外(Kit 2 から)。 + - 注意: 同じディレクトリで dev server を2つ動かすと、Vite の依存のキャッシュ(`node_modules/.vite`)を共有して壊れる(`Failed to hydrate`・`lifecycle_outside_component`・`Cannot read properties of undefined (reading 'call')`)。今回、ユーザーの `pnpm dev`(`127.0.0.1:5173`)と確認用の dev server(`[::1]:5173`)が重なって起き、別のコピーで動かすと消えた。Kit 3 の問題ではない。 + +配備時: + +- [ ] 9. デプロイをまたぐタブ: production のデプロイ前にタブを開いておき、デプロイ後にそのタブでページ遷移とフォーム送信をする。メジャー更新なので、古い client と新しい server が食い違うのはこの時。 +- [ ] 10. 配備後の Vercel のログ: 最初の数日、500 や見慣れないエラーが出ていないか。Kit 3 の既定の `handleError` は想定外のエラーだけを記録する。 + +### 手動確認の自動化: PR B(手動確認の1・4) + +手動確認の1(提出状況の更新)と4(/problems のタブの保存)を E2E にする(2026-10-21 にユーザーが決定)。どちらも今は E2E が無く、DB の後片付けが要らないか、元に戻せる。本番のコードは変えない(ユーザーの判断)。 + +- [x] `e2e/submission_status.spec.ts` の `keeps the updated status after reload`: guest で /problems の既定の ABS の表を開き、seed で唯一の ABS の問題 `practice_1`(Welcome to AtCoder)のセルのドロップダウンで、今と違う状態を選ぶ。`/problems?/update` の POST を待ってリロードし、ドロップダウンを開き直して、選んだ項目にチェックのアイコンがあることを確かめる。最後に元の状態へ戻す。 + - セルの状態は背景色でしか表されないので、リロード後のドロップダウンのチェックのアイコンで確かめる。DOM の構造に依存するが、本番のコードを変えないためにこの方法をとる。 + - ドロップダウンは自前の `fetch` で送信し、`response.ok` も結果の種類も見ずに画面を更新する(Kit 2 から)。画面だけでは保存の失敗を検出できないので、必ずリロードしてから確かめる。 +- [x] `e2e/problems_tabs.spec.ts` の `keeps the selected problem list tab after reload`: 「グレード別」のタブを選んでリロードし、`aria-selected="true"` のままであることを確かめる(flowbite の `TabItem` は `role="tab"`・`aria-selected` を出す)。 +- [x] `e2e/problems_contest_table.spec.ts` の `keeps the selected contest type after reload`: 「ABC 319 〜」のグループ(利用者が最も多い)を選んでリロードし、その表の見出し(AtCoder Beginner Contest 319 〜(7 問制))が出ることを確かめる。最初は xDPC で書いたが、ユーザーの指摘で ABC 319 〜 に替え、変異の確認もやり直した。既定は `abs` なので、保存されていなければ失敗する。ボタンの選択状態は CSS の class でしか表されないので、表示される内容で確かめる。 +- [x] 変異で検出力を確かめる: `updateTaskResult` が保存しないようにすると提出状況のテストが、`LocalStorageWrapper` が保存しないようにするとタブの2つのテストが失敗すること。変異は戻す。 +- 結果(2026-10-21): 3つとも `--repeat-each=3` で9回すべて成功。変異(`updateTaskResult` が保存しない・`LocalStorageWrapper` が保存しない)を入れると、3つともリロード後の assert で失敗し、既存の xDPC のテストは通った(ABC 319 〜 に替えた後も、`LocalStorageWrapper` の変異で失敗することを確認)。変異は戻した。format・lint(既存の警告1件のみ)・check(0 errors / 30 warnings)・`git diff --check` は成功。E2E の全件は 119 passed / 1 skipped(前回の 116 に3つ増えた)。 + +- 既存の動作を確かめるテストなので、テスト先行(RED)は無い。変異で代わりに検出力を確かめる。 + +残り・保留(2026-10-21 にユーザーが決定): + +- 残りタスク(別 PR): 3(signup の入力エラーの表示と、Alice での発行 → リセット。「確認」は外部 API が要るので service の unit test のまま)、5 のうち /tasks/grade とタグの編集(値を戻して E2E にできる)、6(問題集の作成。作った問題集を消す後片付けが要る)。 +- 保留: 2(問題のインポートはサーバーから外部 API を呼ぶので Playwright で差し替えられず、DB に問題を作る)、5 のうち `/tasks/[task_id]`(devcontainer では firewall で 500 になる。タグの取得を直す別 PR の後)、7(spinner は一瞬しか出ず assert が不安定。利用者メニューの開閉はログアウトの E2E で通っている)。 +- E2E は CI では流していない(`ci.yml` の TODO)。追加したテストが回帰を防ぐのは、手元で流したときだけ。 + +### Phase 5: 任意機能(別タスク) + +- [ ] 必須移行の配備後に、更新通知(`updated.current`)や tracing を選ぶ。候補と推薦度は survey の「便利な機能・採用推薦」。移行の成功条件には混ぜない。 +- [ ] 更新通知を採用する場合は、先に Vercel の skew protection が有効かを確認する。有効だと古い版のユーザーの通信は古いデプロイに送られ、ページ遷移などの通常の通信では新しいデプロイを検知できないことがある。確認先は Vercel のプロジェクト設定か、CI の preview ジョブで `vercel build` が出力する `config.json` の `__vdpl`(repo からは分からない)。 + +## 設計方針 + +- **Superforms を最初に入れる:** 3.0.0 は Kit 2 と 3 の両方に対応するので、Kit 2 上で先に入れてフォームの基準を確認できる。後で Kit 3 のフォームに不具合が出た時、原因を Kit 3 側に絞れる。 +- **alias の置換を Kit 3 本体から切り離す:** `$lib` は Kit 3 でも alias で維持できるが、`kit.alias` 自体が deprecated。共同開発者が1か月以内に新機能を追加するため、新しい `$lib` の import が増える前に置換する。`#lib` が Kit 2 でも解決されることはコピー上で確認済みなので、Kit 3 を待たずに先行できる。 +- **`$features`・`@` は PR A と分ける:** PR A が既に282ファイルあるため。PR A2 で alias の参照を 0 にすれば、PR B は `alias` を使わずに済み、`kit.alias` の deprecated 警告も出ない。 +- **移行先は Kit 3.1.0 / adapter 7.0.1:** 3.1.0 に 3.0.x の server-only 判定の修正が入っている。 +- **Kit と adapter は同じ PR で更新する:** adapter 7 は Kit 3 を peer に要求し、`builder.generateServerInstance` を呼ぶ。片方だけの更新は Build が失敗する。 + +設定移行は既存の値を保つ。`vite.config.ts` の `plugins` を次の形にし、現行の `optimizeDeps / test` の設定は残す。 +`alias` は書かない。`$lib` の alias を残すと Kit 3 で `$lib` が黙って復活し、新しいコードの逆戻りを build で検知できなくなる。`dynamicCompileOptions` は、Superforms の root entry が巻き込む旧構文の `SuperDebug.svelte` のために残す。adapter の runtime は実行環境の Node 24 に合わせて生成結果を確認する。 + +```typescript +import adapter from '@sveltejs/adapter-vercel'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tailwindcss(), + sveltekit({ + preprocess: vitePreprocess(), + adapter: adapter({ + regions: ['hnd1'], + memory: 3008, + maxDuration: 30, + }), + compilerOptions: { runes: true }, + dynamicCompileOptions({ filename }) { + if (filename.includes('node_modules')) { + return { runes: undefined }; + } + }, + }), + ], + // Keep the existing optimizeDeps and test options here. +}); +``` + +公式の `npx sv migrate sveltekit-3` は PR B の作業時に独立した作業環境で使い、自動変換しきれない設定・`dynamicCompileOptions`・query/hash・依存側の旧 API は手で確認する。alias の置換は PR A・PR A2 の自前の codemod で済ませる。 + +### 却下した代替案 + +- Kit と adapter を別々にマージ: peer と Builder API が不整合になり、実際に #4104・#4105 の Build が失敗した。 +- #4105・#4104 の 3.0.0 / 7.0.0 をそのまま採用: 3.1.0 / 7.0.1 に server-only 判定などの修正が出ている。 +- Superforms 2.31.0 のまま Kit 3 へ: Kit 3 の `$app/stores` は購読で例外を投げるスタブで、install と build は通っても `superForm()` を使う画面が実行時に失敗する。 +- `$app/stores` の互換 shim や依存の独自 fork を恒久運用: フォームと認証の依存に保守負担を持ち込む。公式 v3 が対応済みのため不要。 +- `$lib` を alias で維持して `#lib` を先送り: Kit 3 では動くが、共同開発者の新機能が `$lib` の import を増やすほど置換の衝突と手戻りが増える。 +- `$lib` → `#lib` を Kit 3 本体の PR に混ぜる: 282ファイルの機械置換が数ファイルの実変更を埋もれさせ、Kit 3 の回帰と置換の不具合を切り分けられなくなる。 +- `$features`・`@` を PR A に含める: PR A がさらに大きくなり、レビューと共同開発者の rebase が重くなる。 +- `$features` を alias のまま Kit 3 へ持ち込む: 動くが deprecated 警告が残り、alias が削除される時に同じ置換を Kit 3 の後でやり直すことになる。 +- alias の置換に公式の `sv migrate` を使う: 自動置換の対象が明記されておらず、Kit 2 上の先行 PR では使えない。実在ファイルで拡張子を判定する自前の codemod なら、結果を再現・検証できる。 +- 移行と同時に全フォームを remote functions へ置換: 実験機能で変更範囲が大きく、現行 Superforms + Zod の設計も変えるため別の検討にする。 +- 移行と同時に URL utility や UI を再設計: removed API への対応に絞り、既存フォームの業務仕様は維持する。 +- `posted` や `flashMessage` など Superforms の非推奨 API を移行と同時に整理: 3.0.0 で動作するため、削除された時に対応する。 +- super-sitemap を移行と同時に 2.x へ更新: export が変わる major 更新で、Kit 3 移行の必須条件ではない。1.0.12 は Kit の API に依存しない。 + +## 検証手順と完了条件 + +各 PR で次を実行する。 + +```bash +pnpm install +pnpm build +pnpm lint +pnpm check +pnpm test:unit --run +pnpm test:e2e -- e2e/signin.spec.ts e2e/redirect_after_login.spec.ts e2e/votes.spec.ts e2e/workbook_edit.spec.ts e2e/workbook_order.spec.ts e2e/robots.spec.ts e2e/sitemap.spec.ts +git diff --check +``` + +- E2E は既存の test DB・認証 fixture を用意する。`playwright.config.ts` は build + preview を起動するため、CI の Node 24 build / unit test だけで完了扱いにしない。 +- PR B では `kit.alias` を使わないため、build と dev に出る新しい警告・エラーはすべて調査する。 +- PR B は preview の `pull / build / deploy --prebuilt / alias set` に加え、production の `build --prod / deploy --prod --prebuilt` と、ログイン・投票・編集の smoke test も成功条件に含める。 +- 失敗時は該当 PR を通常の revert で戻し、Kit 2 / adapter 6 の成果物に戻せる状態を保つ。DB schema / migration はこの計画の対象外。 +- codemod は repo に入れず、PR A の本文に全文を貼って共有する。PR A2 の `features` モードも同じスクリプトを使う。 diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md new file mode 100644 index 000000000..d773007b7 --- /dev/null +++ b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md @@ -0,0 +1,85 @@ +# SvelteKit 3 移行のまとめ + +SvelteKit 2.70.3 → 3.1.0、adapter-vercel 6.3.4 → 7.0.1 への移行の要点(2026-10-21 時点)。 +作業の記録は [plan.md](./plan.md)、調査の詳細は [survey.md](./survey.md) にある。 + +## いまの状況 + +| PR | 内容 | 状態 | +| ---------------------- | ------------------------------ | ---------------------------------- | +| #4115 | Superforms 2.31.0 → 3.0.0 | マージ済み | +| #4190(PR A) | `$lib` → `#lib` | マージ済み | +| #4191(PR A2) | `$features`・`@` → `#features` | マージ済み | +| PR B(branch `#4192`) | Kit 3 本体 | 手元と preview で確認済み、未 push | + +- 確認済み: check・lint・unit test・build・E2E(119件)、preview での手動確認、Codex のレビュー(重大な指摘なし)。 +- 残り: push と CI、production への配備と確認(→「残りタスク」)。 + +## なぜ移行したか + +- adapter-vercel 7 以降は Kit 3 が前提で、Kit 2 のままでは Vercel 向けの更新を取り込めない。 +- 速度やサイズの恩恵は無い(build は約3秒遅く、初回表示の JS は約4 KB 増)。得られるのは、開発時の検査の強化。 + +## 主な決定 + +- PR を4本に分け、1本ずつマージした。不具合が出た時に原因を切り分けるため。 + - Superforms は Kit 2・3 の両方に対応するので、最初に Kit 2 上で入れた。 + - alias の置換は、共同開発者が新機能を足す前に、Kit 2 上で先に済ませた。 +- Kit と adapter は同時に更新する。片方だけだと build が失敗する。 +- 版は 3.1.0 / 7.0.1 に固定した。3.1.0 に server-only の判定の修正が入っている。 +- alias は使わず、`package.json` の `imports`(`#lib`・`#features`)にした。Kit 3 で alias は非推奨。 +- 公式の `sv migrate` はコピーで試して一部だけ採用し、alias の置換は自前の codemod([codemod_subpath_imports.py](./codemod_subpath_imports.py))で行った。 +- 却下した案は plan.md の「却下した代替案」にある。 + +## 知っておきたい Kit 3 の挙動 + +- `fail(400)` は HTTP 400 で返る(Kit 2 は 200)。 +- 別ページの action を持つフォーム(ログアウト)は、失敗しても action のページへ移る。 +- `$app/stores` は使うと実行時に例外になるので、`$app/state` を使う。`$app/environment` は `$app/env` に変わった。 +- `resolve()` に渡せるのは実在する route だけ。`/` 始まりは route ID として扱われ、group も含めて書く。 +- `redirect()` は外部の URL へは throw する。 +- `server/` ディレクトリと `*.server.*` のファイルは、client から import できない(`features/*/server/` も対象)。 +- `svelte.config.js` は使えず、置くとエラーになる。設定は `vite.config.ts` の `sveltekit()` に書く。 +- `$lib` はどこに書いてもエラーになる。ただし `vi.mock('$features/...')` だけはエラーにならず、黙って無視される。 +- import の拡張子は、TS が `.js`、runes モジュールが `.svelte.js`。E2E では `#lib` が解決できないので相対 import にする。 + +根拠になったコードと、Kit 2 との比較の結果は survey.md と plan.md(Phase 2〜4)にある。 + +## 開発環境での注意 + +- E2E は CI で流していない。手元で流す。 +- devcontainer では次のことが起きるが、Kit 3 の不具合ではない。 + - AtCoder ID の「確認」が必ず失敗する。秘密の値を置かない方針で、`CONFIRM_API_URL` が無いため(#4061)。 + - `/tasks/[task_id]` が 500 になる。firewall が Google Apps Script への通信を通さないため。 + - 同じディレクトリで dev server を2つ動かすと、Vite のキャッシュを共有して壊れる。 + +## 残りタスク + +PR B: + +- [ ] push し、CI が通ることを確かめる(push は人が行う)。 +- [ ] preview と production の build・deploy と smoke test(preview は仮の deploy で確認済み)。 +- [ ] 配備時の確認: デプロイ前から開いていたタブの動作と、配備後の Vercel のログ。 +- [ ] 共同開発者への連絡(PR A・A2 のマージと、作業中の branch での codemod の再実行)。 + +別 PR(計画あり): + +- [CI と型チェックの高速化、actions の SHA 固定](../../2026-10-20/ci-and-typecheck-speedup/plan.md) +- [Prettier から oxfmt への移行(#2226)](../../2026-10-21/oxfmt-migration/plan.md) +- [AtCoder ID の本人確認がときどき失敗する件](../../2026-10-21/atcoder-verification-failures/survey.md) + +別 PR(計画はまだ無い): + +- `/tasks/[task_id]` が、タグの取得に失敗すると 500 になる。 +- E2E の追加(signup、`/tasks/grade` とタグの編集、問題集の作成)。 +- `TaskTableBodyCell.svelte` が別の feature(votes)を import している。 +- 提出状況のドロップダウンが、サーバーの応答を確かめずに画面を更新する。セルの状態が色だけで表される。 +- 任意機能の検討(更新通知、tracing)。 +- 細かいもの: dev server の依存の事前スキャンのエラー、flowbite-svelte の `Tabs` の非推奨の props、Superforms の `posted`。 + +## 出典 + +- [SvelteKit 3 の公式移行ガイド](https://svelte.dev/docs/kit/migrating-to-sveltekit-3) +- リリース: [Kit 3.0.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.0.0)・[3.1.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.1.0)、[adapter-vercel 7.0.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fadapter-vercel%407.0.0)、[Superforms v3.0.0](https://github.com/ciscoheat/sveltekit-superforms/releases/tag/v3.0.0) +- `$lib` が `#lib` になった経緯: issue [#16182](https://github.com/sveltejs/kit/issues/16182)、PR [#16360](https://github.com/sveltejs/kit/pull/16360) ほか(survey.md の「Kit 3 が `$lib` を `#lib` に変えた理由」) +- 挙動の根拠にした Kit のコードと実測は、survey.md と plan.md に記載。 diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md new file mode 100644 index 000000000..1f8f857d2 --- /dev/null +++ b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md @@ -0,0 +1,327 @@ +# SvelteKit 3 / adapter-vercel 7 移行調査 + +## 概要 + +[移行計画](./plan.md) の根拠となる調査。Renovate の [#4105](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/4105)(Kit 2.70.3 → 3.0.0)と [#4104](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/4104)(adapter-vercel 6.3.4 → 7.0.0)は、それぞれ片方だけを更新していたため 2026-10-03 時点で Node 24 CI の Build が失敗し、現在は closed。2026-10-18 時点で open の依存 PR は [#4115](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/4115)(Superforms 3.0.0)のみ。 +調査は 2026-10-03 に始め、10-06・10-18 に追加した。基準は 2026-10-18 JST の `origin/staging`(`f02b7238`)。方法は manifest・配布コード・ソースの静的調査と、alias の置換をリポジトリのコピー上で試した検証。2026-10-19 には Kit 3 の spike で build・unit test・lint・check を実行した(→「Kit 3 の spike」)。その後、branch `#4192` で E2E の全件を流し、Kit 3 を仮に deploy した preview で手動確認も行った(2026-10-21、plan の「手動確認」)。要点は [summary.md](./summary.md) にまとめた。 +前回までの誤認と古くなった記述は、末尾の「前回調査からの訂正」にまとめた。 + +## 移行可否の判断 + +**外部の依存ブロッカーはなく、移行は可能。** + +| 判断軸 | 状況 | 判定 | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | +| Superforms の Kit 3 対応 | 3.0.0(2026-10-04)が Kit `^2.12.0 \|\| ^3.0.0` に対応。#4115 は 2026-10-18 に staging へマージ済み(`dc9f2471`)。マージ前の head `5833c1d0` の CI は build (24)・preview 成功で、手元の E2E も成功した。公開 API は不変。 | 解消 | +| 最低環境 | Kit 3.1.0 の要件(Node >=22.17・Vite ^8.0.12・Svelte ^5.57.1・TypeScript ^6.0.0・vite-plugin-svelte ^7.0.0)を、現行の Node 24.x・Vite 8.3.4・Svelte 5.57.2・TypeScript 6.0.3・plugin 7.3.1 が満たす。adapter 7.0.1 の peer は Kit `^3.0.0-next.0`。 | 解消 | +| 移行先の成熟度 | Kit は 3.0.0 の後に 3.0.1・3.1.0、adapter は 7.0.1 が出た。いずれも form・cookie・navigation の挙動には触れない(→「対象バージョン」)。 | 3.1.0 / 7.0.1 を採用 | +| Kit に依存する他パッケージ | 依存パッケージの配布コードを全件走査し、Kit の API(`$app/*`・`@sveltejs/kit`)を import するのは Kit・adapter・Superforms のみと確認した。svelte-meta-tags は Svelte のコンポーネントだけで、upstream は Kit 3.1 で開発している。super-sitemap は Kit の API を使わず、Kit 3 でも変わらないルートの規約(`src/routes/**/+page*.svelte`)だけに依存する(→「周辺ツールと依存の Kit 3 対応」)。 | 支障なし(E2E で確認) | +| 作業規模 | 変更ファイル数の大半は alias の機械置換(`$lib` 282ファイル・`$features` 144ファイル)で、スクリプトで再現できる。Kit 3 本体の実変更は設定・型 export・state・`resolve`・フォームの数〜十数ファイル。 | 中(段階実施が前提) | +| 回帰リスク | フォーム応答の HTTP status、別ページ宛 action、`$app/stores` の実行時例外、`dynamicCompileOptions` の移設。既存 E2E が守るのはログイン後 redirect・問題集編集/並べ替え・投票・ログアウト成功で、失敗系は未整備。 | 中〜高(E2E 追加が前提) | +| 戻し方 | 各 PR を通常の revert で戻せる。DB schema / migration は対象外。 | 低 | + +## Superforms 3.0.0(#4115) + +#4115 は 2.31.0 → 3.0.0 の依存・lockfile 更新(2ファイル)のみで、2026-10-18 に staging へマージされた(`dc9f2471`)。マージ前に preview deploy での5画面の確認と、手元の E2E(5本の spec・56件、すべて成功)を行った。Superforms 2.x は bugfix のみの維持になり、Kit 3 には v3 が必須(公式の upgrade paths)。 +head `5833c1d0`(2026-10-18 に staging `f02b7238` の上へ rebase)の lockfile の差分は、Superforms 本体と optional 依存の更新に限られる(ArkType 2.2.2 → 2.2.8、@ark/schema・@ark/util 0.56.1 → 0.56.7、arkregex 0.0.7 → 0.0.13、Vine 3.0.1 → 4.4.0・compiler 3.0.0 → 4.2.0 と新規の @poppinss/types 1.2.1、camelcase 8 → 9、Valibot 1.4.2 → 1.5.0・@valibot/to-json-schema 1.7.1 → 1.8.0、Joi 17.13.4 → 17.13.8、TypeBox 1.3.4 → 1.4.2)。2.31.0 の optional 依存が引いていた `effect@3.22.2` は消え、override(`effect@>=3.0.0 <3.20.0` → 3.22.3)の解決先 3.22.3 に統合された。devalue・esm-env と、Kit・Svelte・Vite の解決版は変わらない。 +この lockfile を pnpm 12.14.0 の `install --lockfile-only` で再解決しても書き換えは出ず、supply-chain policy の検査も通った。Kit 2 の CI 成功は、Kit 3 での成功を意味しない。 + +### リリースノートの全項目と影響 + +| 公式ノートの項目 | 本プロジェクトへの影響 | +| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| Kit `^2.12.0` と `^3.0.0`、Svelte `^5.56.4` に対応 | 利点。Kit 2.70.3・Svelte 5.57.2 のまま先行反映でき、Kit 3 移行時に Superforms を再更新しない。Svelte 4 と Kit 1 の peer は消えるが未使用。 | +| ArkType を beta から `^2.0.0` へ | 未使用(バリデータは Zod 4 の `zod4`)。対応不要。 | +| Vine `^4.0.0` に対応 | 未使用。対応不要。 | +| transport 型を Kit 2・3 の両方で通るよう修正 | `transport` オプション未指定。型は Superforms 内の独自 `Transport` に移り、Kit の型 import への依存が減る。アプリ側の変更なし。 | +| 「破壊的変更なし」 | 公開 API(`superValidate` / `superForm` / `zod4` / 各 export パス)は不変。ただし配布コードには次節の挙動変更がある。 | + +途中の prerelease は、3.0.0-next.0(2026-08-27)が Kit 3 対応、3.0.0-next.1(2026-09-30)が 2.31.0 と同じ `actionResult` の `transport` 追加。3.0.0 の GA 版に v3 固有の機能追加はない。 + +### 配布コード差分(リリースノートに載らない変更) + +2.31.0 と 3.0.0 の tarball を比較した。`exports` マップは不変(`imports` の `#lib` が Superforms の内部用に増えただけ)。 + +| 変更 | 内容 | 本プロジェクトへの影響 | +| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `$app/stores` → `$app/state` + `toStore` | `superForm` 内の page / navigating ストアを `$app/state` から `toStore` で再構築する。`$app/state` は Kit 2.12 以降で使える。 | アプリのコード変更は不要。`superForm` を使う5画面(login・signup・workbooks create / edit・account_transfer)で内部の page 同期が変わるため、Kit 2 上で確認が要る。Kit 3 では `$app/stores` が例外になるので、2.31.0 は使えない。 | +| `$app/environment` の `browser` → `esm-env` の `BROWSER` | 5ファイルで置換。`esm-env` は Svelte の依存で lockfile に 1.2.2 が既にある。 | 影響なし。SSR・Vitest(node) では false、client build では true。 | +| `/server` に `import '$app/server'` を追加 | `sveltekit-superforms/server` が server-only になる(client からの import は build エラー)。 | build は影響なし。`/server` を import する本アプリの6ファイルのうち、`auth_forms.ts` は2つの `+page.server.ts` からのみ import され、残りも `+page.server.ts` か単体テスト。単体テスト `schema.test.ts` は実体を import する。Kit 3 の Vitest でも解決できた(Kit 3 の spike で unit test 2726件が成功)。 | +| root entry が旧構文の `SuperDebug.svelte` を import(3.0.0 でも `export let` 13箇所) | root の `sveltekit-superforms` を import すると `SuperDebug.svelte` が巻き込まれる。本アプリでは `account_transfer/+page.svelte` と3つの `+page.server.ts` が該当。 | `dynamicCompileOptions`(node_modules の runes を未指定にする回避策)が引き続き必要。`account_transfer/+page.svelte` を `/client` からの import にすれば client bundle から外せるが、移行の必須条件ではない。 | +| `actionResult` が Kit の `json` ではなく `Response.json` を使用 | Kit 3 で `json` が非推奨になるのを先回りしたもの。 | 影響なし。 | +| `goto(url, { ...params })` → `goto(url)` | Kit 3 の `goto` 引数変更に整合。 | 影響なし。 | +| ActionResult に `location`、`onError` の error を `App.Error` 向けに正規化 | `customRequest` 経路と `onError` に入る。 | `customRequest` と `onError` は未使用。影響なし。 | +| 依存: devalue `^5.8.1` → `^5.9.4`、`esm-env ^1.2.2` を追加 | lockfile には devalue 5.9.4・esm-env 1.2.2 が既にある。 | 影響なし。Kit 経路の devalue 5.8.1 の宿題は解消しない。 | +| 非推奨が残るもの | `posted`(注記は「Will be removed in v3」だが 3.0.0 に存在)、`flashMessage` 統合、`SPA` の string / failStatus。 | `flashMessage` と `SPA` は未使用。`posted` は `src/lib/types/auth_forms.ts:44` の `AuthForm` 型と `src/lib/utils/auth_forms.ts:77,132` が保持する。3.0.0 では動くので今は変更せず、削除された時に auth_forms の型・util・テストを直す。 | + +`superForm` の呼び出しは5画面のみ。login / signup / account_transfer はオプションなし、workbooks create / edit は `{ dataType: 'json' }` だけ。`SuperDebug`・`flashMessage`・`transport`・`customRequest`・`validators`・`onUpdate*` の指定は src にない。`posted` 以外の非推奨 API は使っていない。 + +### 対応不要と判断したもの + +| 項目 | 根拠 | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 周辺依存 | Kit peer は `^2.12.0 \|\| ^3.0.0`、Svelte は `^5.56.4`、Zod は `^3.25.0 \|\| ^4.0.0`。現行の Kit 2.70.3・Svelte 5.57.2・Zod 4.6.7 はすべて適合。Kit 3 本体が要求する Svelte 5.57.1 も満たす。 | +| 公開 API / import | root・`/client`・`/server`・`/adapters` の exports は不変。既存の `superValidate(data, zod4(schema))`、`superForm(data.form)`、`superForm(initialData, { dataType: 'json' })` を保持する。 | +| 検証・初期値 | 使用中の `zod4` adapter・schemaShape・default options の配布コードは不変。`resetForm / taintedMessage` の既定値を移行のために変更しない。認証フォームの独自 schemaShape 型も即時変更は不要。 | +| 未使用 adapter | ArkType 安定版対応と Vine 4 対応は追加機能。アプリは Zod 4 のため validator の切り替えは推薦しない。 | + +### 新機能・採用推薦 + +v3.0.0 自体が追加する機能は Kit 3 対応と、使っていない validator の対応だけ。このアプリが得る利点は互換性で、新機能の採用は必要ない。 + +| 項目 | このプロジェクトでの用途 | 推薦 | +| ----------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | +| Kit 2・3 両対応 | #4115 を先に Kit 2 上へ入れ、フォームの基準を確認してから Kit 3 移行に進める。 | 採用(最大の利点)。 | +| `$app/state` ベースの内部実装 | Kit 3 の `$app/stores` スタブを避ける。アプリ側のコード変更は不要。 | v3 に上げるだけで得られる。 | +| ArkType 安定版・Vine 4 | Zod 4 を使うため不要。 | 見送り。 | +| `actionResult` の `transport`(2.31.0〜) | カスタム型を form action の応答に載せる場合。現状は未使用。 | 見送り。必要になったら導入する。 | +| flashMessage 統合 | 非推奨。 | 導入しない。 | +| `taintedMessage`(既存機能) | 問題集編集の未保存離脱警告。v3 の新機能ではない。 | 低。Kit 3 の `updated` による更新通知と併せて検討する場合のみ。移行のために既定値は変えない。 | + +## Kit 3.1.0 / adapter-vercel 7.0.1 + +### 対象バージョン + +| リリース | 内容 | 本プロジェクトへの影響 | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Kit 3.0.1(2026-10-06) | patch 1件: 生成型の相対 import が `nodenext` で動くよう修正(#17351)。 | 非該当。`tsconfig.json` は `moduleResolution: bundler`。 | +| Kit 3.1.0(2026-10-14) | minor 2件: CSP のハッシュ報告キーワード対応(#17398)、server tracing 有効時に `+server` ハンドラーの `sveltekit.endpoint` span を出力(#17325)。patch 6件: プロジェクトが `server` ディレクトリ配下にある場合に全モジュールを server-only 扱いする誤りの修正(#17376)、service worker 更新失敗時の復旧継続(#17440)、prerender の MIME 推定(#17381)、内容が同じ生成ファイルは書き換えない(#17310)、iframe の focus(#17297)、Storybook の警告抑制(#17373)。 | 破壊的変更なし。#17376 は 3.0.x の不具合(判定を絶対パス全体に対して行っていた)なので 3.1.0 以上を採用する根拠になる。#17310 は Vitest を `vite dev` と並行して動かしても、生成ファイルの再書き込みで dev server が再読込されなくなる開発体験の改善。CSP・service worker・Storybook は未使用で、`export const prerender` の指定もない。本アプリの `+server` は `(admin)/workbooks/order/+server.ts` の POST のみ。 | +| adapter-vercel 7.0.1(同日) | patch 1件: 解決済み runtime で検出した Bun の minor 版を保持(#17331)。 | 非該当(Node 24)。 | + +Kit 3.0.0 と 3.1.0 の tarball も比較した。`package.json` は版番号だけが変わり、dependencies・peer・engines は不変。差分のある `package.json` 以外の23ファイルは、docs URL のアンカー更新と型の追従が大半。実行時の変更は次に限られる。 + +- server-only 判定を、絶対パス全体ではなくプロジェクトルートからの相対パスに対して行う(`is_server_only_path`)。`node_modules` とルート外は対象外。 +- `+server` ハンドラーの `sveltekit.endpoint` span(server tracing 有効時のみ)と、CSP の `report-sha*` キーワード。 +- service worker 更新を try/catch で保護、iframe が focus を持たない時は自動 focus しない。 +- 生成ファイルの比較先を、プロセス内の記憶からディスク上のファイルへ変更。 +- adapter 向けの MIME 型収集で `prerendered` と `client` のディレクトリを個別に走査する。 + +`kit.alias` の検証と deprecated 警告(`core/config/options.js`)、`$lib` 削除エラーの条件(`vite/index.js` の `removed_modules`)、tsconfig の `paths` 生成、`$app/forms`・`$app/navigation`・`$app/state` の実装、cookie、redirect に差分はない。 + +### Kit 3 の spike(2026-10-19) + +staging(`e3d1145c`、PR A・PR A2 のマージ後)のコピーで `npx sv@1.1.1 migrate sveltekit-3` を Phase 1 の範囲(`svelte-config`・`lib-alias` と、自動で実行される前提タスク `package-json`・`tsconfig`)で実行し、Kit 3.1.0・adapter-vercel 7.0.1 に上げた。 + +- `sv migrate` のタスクは12個: `package-json`・`tsconfig`(前提)、`svelte-config`、`environment`、`paths`、`external-redirects`、`shallow-routing`、`params`、`imports`、`lib-alias`、`app-state`、`collect-migration-instructions`(自動化できない作業を `MIGRATION_TASKS.md` に書き出す)。前提タスクは `--tasks` で指定できず、自動で実行される。 +- 出力のうち、そのままでは使えなかったもの: + - `svelte.config.js` の `vitePlugin` を `sveltekit({ vitePlugin: { dynamicCompileOptions } })` に移した。Kit 3 は `config.vitePlugin` を削除しており、`svelte-kit sync` が `config_option_removed_vite_plugin`(「Pass `vite-plugin-svelte` options directly to the `sveltekit(...)` Vite plugin」)で失敗する。`dynamicCompileOptions` は `sveltekit({...})` の直下に置く。 + - Kit と adapter を `^3.0.0`・`^7.0.0` の範囲指定にした(本プロジェクトは版を固定する)。 + - 実在しない `src/lib/index.js` への bare `#lib` を `package.json#imports` に足した。 + - `lib-alias` が Markdown の中の `$lib` まで `#lib` に置換した。`architecture.md` の「組み込みの `$lib` を削除」が「組み込みの `#lib` を削除」になって文意が壊れ、過去の dev-notes 8件も書き換えた。 + - 既存のコメントを無関係な位置へ移し、`//` のコメントを `/* */` に書き換えた。 +- `tsconfig.json` は `extends: "$app/tsconfig"` になり、`allowJs`・`checkJs`・`isolatedModules`・`moduleResolution: bundler`・`skipLibCheck`・`verbatimModuleSyntax` などが削られた。生成された `node_modules/$app/tsconfig.json` がこれらをすべて提供し、`paths` は空なので、削っても結果は同じ。`types` は extends で置き換わるため `$app/types` を自分で含める。 +- 結果: build(`.vc-config.json` は `nodejs24.x`・`hnd1`・memory 3008・maxDuration 30、framework は sveltekit 3.1.0)・unit test(2726件)・lint は成功。check は 20 errors で、すべて Phase 2 の項目(`$app/environment`、`Handle`・`SubmitFunction` の型の移動、`$app/stores`、不要になった `@ts-expect-error`、`resolve()` の引数、汎用の `Actions` 型での `params`)。内訳は計画の Phase 2 に記載。警告は Kit 2 と同じ 30件。 +- `$app/environment` は Kit 3 の型からは消えている(`Cannot find module`)。build は通るが、Phase 2 で `$app/env` に置き換える必要がある。 +- lockfile からは Kit 2 の経路の `devalue@5.8.1` が消え、`cookie` は 2.0.1 になった。 + +Phase 2(branch `#4192`)の作業で分かった Kit 3.1.0 の挙動: + +- `resolve()` は、`/` で始まる引数を route ID として扱い、`(group)` を除いて `[param]` を埋める。`?`・`#` 以降はそのまま残る。それ以外は pathname として base を前に付ける。型が受け付けるのは、既知の route ID・pathname と、それに `` `?${string}` ``・`` `#${string}` `` を付けたテンプレート文字列だけで、`string` は渡せない。route ID に `/signup` のようなグループを含まない形は無く(`/(auth)/signup`)、pathname は先頭に `/` を付けない。 +- `goto()` は非推奨の `replaceState` を `replace` として受け付け続ける(`opts.replace ?? opts.replaceState`、開発時に警告)。 +- 実行時の `$app/environment` は `../env/index.js` を再 export するだけで残っている。型は消えているので、import は `$app/env` に変える必要がある。テストで `$app/env` を mock すると、`$app/environment` から import するコードにも効く。 +- `page.url` は型の上で読み取り専用で、`searchParams` は `ReadonlyURLSearchParams`。`URLSearchParams` や `URL` を受け取る関数には、そのまま渡せない。 +- route 専用でない汎用の `Actions` 型(`@sveltejs/kit`)では、`params` の値が `string | undefined` になる。 + +Phase 3 の E2E で確かめた挙動(Kit 2 との比較は staging のコピーで実行): + +- enhanced の form action が `fail(400)` を返すと、Kit 3 は HTTP 400 を返す(Kit 2 は 200)。JSON で返すかの判定は `accept` ヘッダーのままで、本文の ActionResult は `{ type: 'failure', status: 400 }`。 +- 別ページ宛の enhanced action(Header のログアウト)は、失敗しても action のページへ遷移する。未ログインでログアウトすると、Kit 3 は `/logout` の `load` の redirect を経て `/` に着く(Kit 2 は元のページに留まっていた)。 +- ゲストログイン(`AuthForm` が `fetch` で直接 POST する)は、成功時の応答が HTTP 200 の redirect で Kit 2 と同じ。失敗時は Kit 3 では HTTP 400 になり、`response.ok` が false なのでログイン画面に留まる。Kit 2 では 200 が返り、失敗してもホームへ遷移していた。 +- 本アプリは `handleError` を定義していない。Kit 3 の既定の実装は、予期しない例外(500)のスタックはサーバーに記録し、404・403 などの想定済みのエラーは記録しない。500 の `+error.svelte` には `Internal Error` が出る。 +- session cookie は Kit 3(cookie v2)でも `path: /`・`httpOnly`・`secure`・`sameSite: Lax` のまま。 + +### 破壊的変更の影響(要対応) + +| 項目 | 本プロジェクトでの到達箇所と必要な対応 | リスク | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| Kit / adapter の同時更新 | adapter 7 の Kit peer は `^3.0.0-next.0`。adapter 7 は `builder.generateServerInstance` を呼ぶため、片方だけの更新は Build が失敗する。 | 高 | +| Superforms | Kit 3.0.0 の `$app/stores` は export が残るが、`page / navigating / updated` の購読と `getStores` が例外を投げるスタブ。Superforms 2.31.0 のままだと build は通り、`superForm()` を使う画面が実行時に失敗する。3.0.0 で解消するが、Kit 3 上のフォーム回帰検証は必要。 | 高・回帰検証 | +| 設定の Vite 集約 | `svelte.config.js` がサポート外。`kit.adapter`・`preprocess`・`compilerOptions` を `sveltekit({...})` 直下へ移す。`vitePlugin.dynamicCompileOptions` も直下へ移し、node_modules に runes を強制しない現行挙動を保つ。 | 高 | +| `$lib` の削除と alias | 組み込みの `$lib` と `files.lib` が削除される。alias で維持はできるが、`kit.alias` は deprecated(→「alias から subpath imports への置換」)。 | 中 | +| tsconfig の生成先 | `tsconfig.json` の extends を `$app/tsconfig` へ変更する。既存の `src / prisma / *.config.ts` の include と型設定を保持し、generated file を手編集しない。 | 中 | +| 型 export の移動 | `src/hooks.server.ts` の `Handle` は `@sveltejs/kit/hooks` へ、管理タスク画面2件の `SubmitFunction` は `$app/forms` へ移す。依存ライブラリ側の型 import も確認する。`Page` 等は `$app/state`、navigation の型は `$app/navigation` に移る。 | 中 | +| `$app/stores` の削除 | `KanbanBoard.svelte` に `page / $page` が残る。Kit 3 では購読時に例外になる。初期化だけ読む値と URL 更新に追従させる値を区別し、filter の変更で未保存の並べ替え状態を消さないよう `$app/state` と Runes に移す。 | 中 | +| `goto`・shallow routing | `workbooks/+page.svelte` の `{ replaceState: true }` は `{ replace: true }` に変わる。Kanban の `replaceState` は deprecated で、`goto(..., { shallow: true, replace: true, state: page.state })` が後継。shallow navigation も hooks を発火する。`goto` は app 内 route に解決できない URL を reject する。 | 中 | +| `resolve` の pathname と route ID | leading `/` は route ID にのみ許される。問題集の保存 URL・Kanban filter URL・navbar 定数は pathname と query/hash を区別して変換する。`resolve('/workbooks/[slug]', { slug })` の route ID 指定は保持し、`/login` 等は route group を含む ID か leading slash のない pathname として扱う。既存の `@ts-expect-error` が不要になれば除去する。 | 中 | +| form action の HTTP status | 公式: 「Enhanced form action responses now use the HTTP status code passed to `fail(...)` instead of always returning `200`.」`fail(400/403/500)` の enhanced 応答が実 status になる。Superforms・独自 `use:enhance` の validation failure、権限拒否、redirect に影響する。 | 高 | +| 別ページ宛の enhanced action | 公式: 「Forms with `use:enhance` that specify an `action` on a different page will navigate to that page upon submission」。該当は `Header.svelte:135` のログアウトのみ(`/logout?/logout`)。成功時は `redirect(303, '/')` で挙動不変。未ログインでの失敗 `fail(401)` は、Kit 2 では画面に何も出なかったが、Kit 3 では `/logout` へ遷移し、`load` の redirect で `/` に着く。害は小さいが仕様変更。他の `use:enhance` は `?/xxx` か空の action で同一ページ宛。`TagForm` の `/tags?/update` は `use:enhance` なしの通常 POST で対象外。 | 中 | +| server-only 境界とエラー挙動 | `src/routes`・`static` 内を除く `server/` ディレクトリと、ファイル名に `server` セグメントを持つファイル(`server.ts`・`*.server.ts`)が保護対象になる。該当は `src/lib/server/` の2ファイル、`src/features/*/server/` の6ファイル、`hooks.server.ts` の計9ファイルで、Kit 3 で新たに保護されるのは features の6ファイル。全 `.svelte` とユニバーサル load(100ファイル)を起点に import グラフを辿り、これらに届く経路が無いことを静的に確認した(PR B の build でも、server-only の境界のエラーは出なかった)。`handleError` は expected error・rendering error も受け、`App.Error` は status を含む。本アプリは `handleError` を定義しておらず、Kit 3 の既定の実装は想定済みエラーをログに出さない。 | 低〜中 | +| 非推奨 env / response API | `$app/environment` を使う source 4ファイルとテストの mock 4ファイルは `$app/env` が後継。`$env/*` は `$app/env/private / public`、`json / text` は標準の Response API が後継だが、旧 env alias は残る。型・環境変数の参照タイミングが変わる変更は機械置換に混ぜない。 | 中 | + +### 対応不要と判断したもの + +| 確認項目 | 根拠・確認限界 | +| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 最低環境 | Node 24.x、TypeScript 6.0.3、Svelte 5.57.2、Vite 8.3.4、vite-plugin-svelte 7.3.1 は Kit 3.1.0 の要件を満たす。svelte-check 4.7.7 と Tailwind Vite 4.3.3 の peer も現行版に合う。 | +| adapter runtime | 現状は edge / Node 20 を指定しておらず、Node 24 の CI。[adapter 7 の runtime 実装](https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-vercel%407.0.0/packages/adapter-vercel/utils.js) は Node 22/24 を許容。memory 3008・maxDuration 30・region hnd1 は残せる。外部の Vercel project 設定までは repo だけで断定しない。 | +| cookie v2 / path default | 認証 cookie は ASCII の `auth_session`、set/delete は明示的に `path: '/'`。cookie 型の直接 import はない。 | +| CSRF / 外部 redirect | `csrf.checkOrigin` は未設定で、`trustedOrigins` の追加も不要。Kit 3 は Content-Type なしの cross-origin 変更リクエストを CSRF として拒否するが、本アプリの `fetch` による POST(`vote_grade.ts`・`AuthForm` のゲストログイン・`UpdatingDropdown`・`UpdatingModal`・`kanban.ts`)はすべて same-origin。ログイン・登録は既存の `isSameOriginRedirect` で destination を検証している。Kit 3 は現在の origin を参照して外部を判定するため、同一 origin の絶対 URL を一律に external 扱いしない。[redirect 検証実装](https://github.com/sveltejs/kit/blob/%40sveltejs/kit%403.0.0/packages/kit/src/exports/url.js) に沿って既存の guard を維持し、安易に `external: true` を追加しない。 | +| remote functions / matcher / service worker | remote functions、旧 `src/params/`、service worker、instrumentation、custom adapter / Node server は未使用。query の event access 制約、single-flight mutation、field.as、Builder の直接操作、polyfills 等はアプリ側には非該当。 | +| その他の削除 API・設定 | `preloadData / preloadCode` の直接使用、`base / assets / resolveRoute`、`SSRManifest`、`data-sveltekit-*="off"`、`noScroll / keepFocus`、旧 tracing flags、ISR / split runtime の明示設定、`csp` 設定は見つからない。`error(status, {...})` の独自 App.Error 拡張もない。 | +| 周辺の UI / SEO パッケージ | svelte-meta-tags 5.0.3・super-sitemap 1.0.12 とも対応不要と判断した(→「周辺ツールと依存の Kit 3 対応」)。 | +| 配備検知・同一 URL の再読込 | `version.pollInterval` は1時間が default。同一 URL のリンククリックで load が refresh される。直接の設定変更は不要だが、長時間開いたフォームと同一ページ再読込の UX は確認が要る。 | +| Major Changes 76項目の照合 | 上表と「要対応」で扱った項目以外は該当なし。`export const config`・外部への `redirect`・`error(status, {...})`・204 応答・`x-sveltekit-` で始まる query・`*.remote.*` ファイル・`$app/types`(`Pathname`・`Asset`)・`Page` やナビゲーション型の import・`delta`・`form.error`・`preloadData / preloadCode` の使用は無い。`invalidateAll` はコメントでの言及のみ。`ORIGIN` 環境変数の削除は adapter-node 向けで非該当。`goto` の呼び出しは `resolve()` の結果・`homeHref`・action の redirect 先(`(admin)/tasks/+page.svelte:47` の `result.location`)で、いずれも app 内の URL。`RouteId` が `+page`・`+server` を持つルートに限られる変更は、`+layout.svelte` の `navigating.from.route.id` の比較には影響しない(`resolve()` の型は PR B で扱う)。 | + +### 便利な機能・採用推薦 + +| 機能 | このプロジェクトでの用途 | 推薦 | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| feature の server-only 保護 | `src/features/*/server/` が自動的に client import を拒否し、現在の配置規約を framework が検査できる。 | 高。移行で自動的に得られる利点。build の境界エラーを抑制せず修正する。 | +| `goto` の shallow / `refreshAll` | Kanban の filter URL を history に反映しながら未保存の並べ替え状態を保ち、更新時も `page.state` を維持できる。 | 中。必須移行で挙動を検証し、必要な箇所だけ置き換える。一般の問題集 filter には server load が必要なので shallow を使わない。 | +| `updated.current` による更新通知 | 長時間開いた問題集編集・管理画面で新しい deployment を検知し、古い画面のまま操作し続けることを減らす。 | 中。移行後の別タスクで「保存してから再読込」を案内し、自動リロードによる入力消失を避ける。Vercel の skew protection が有効だと受動的な検知が漏れるため、採用前に設定を確認する(→「確認限界」)。 | +| `tracing.server` と instrumentation | memory 3008 を必要とする問題集や SSR の遅延・失敗の調査。adapter 7 は instrumentation 評価前に env を設定する。Kit 3.1.0 は `+server` の span も出す。 | 中。導入は別タスクで観測目的と exporter を決める。移行時には有効化しない。 | +| rendering error 境界・source maps | 画面の描画失敗を既存の `+error.svelte` と hooks で扱いやすくする。 | 中。まず既存のエラーページを検証し、production の map 対応は adapter の実際の出力で確認する。 | +| `$app/manifest`・typed params・remote functions | service worker、typed matcher、フォーム再設計の基盤。 | 低。現時点の必要性が薄いため本移行では追加しない。 | + +### ビルド時間とサイズ(2026-10-19) + +Kit 3 でビルド速度やサイズの恩恵があるかを測った。結論は、恩恵は無く、どちらもわずかに悪化するが実用上の差は無い。Kit 3 の移行の理由は、保守の継続(adapter 7 が Kit 3 を前提にする)と開発時の検査の強化(server-only の保護・`resolve()` の型など)で、性能ではない。 + +比較は、Kit 2 の `staging`(`e3d1145c`)のコピーと、Kit 3 の `#4192`(`444e4daf`)。同じマシンで `pnpm build` を交互に3回ずつ実行した。初回表示の JS は、それぞれの `vite preview` を Playwright で開き、`resourceType` が script の応答の本文を合計した(未ログイン)。gzip は level 9 で自前に計算した。 + +| 項目 | Kit 2 | Kit 3 | 差 | +| ------------------------------------------- | --------------------- | --------------------- | ---------------- | +| `pnpm build` の時間(3回) | 10.8・12.3・10.0 秒 | 14.0・15.3・12.9 秒 | 平均 +3 秒 | +| うち Vite が報告する時間(2段の合計) | 約 8.0 秒 | 約 5.8 秒 | −2 秒 | +| `/` の初回表示の JS | 23本・234.2 KB (gzip) | 25本・238.0 KB (gzip) | +3.8 KB(+1.6%) | +| `/problems` の初回表示の JS | 39本・263.4 KB (gzip) | 42本・267.3 KB (gzip) | +3.9 KB(+1.5%) | +| client JS の合計(`_app/immutable`) | 76本・396.8 KB (gzip) | 79本・398.9 KB (gzip) | +0.5% | +| framework の入口(`immutable/entry`) | 3.8 KB (gzip) | 2.4 KB (gzip) | −37% | +| Vercel の関数(`.vercel/output/functions`) | 478ファイル・35.82 MB | 473ファイル・35.77 MB | −0.1% | + +- build は、Vite の段は速くなったが、残りの処理(adapter による Vercel 向けの出力などと推測、内訳は未計測)が長くなり、全体で約3秒遅い。build 全体が十数秒なので CI への影響は小さい。 +- 入口のファイルは小さくなったが、chunk が2〜3本増えており、コードが別の chunk に移っただけで、ブラウザーが読む量は約4 KB 増えた。 +- 1台・3回の計測で、配信時の圧縮(Vercel 側)は再現していない。Kit 2 と Kit 3 の相対差として扱う。 + +依存の変化(`staging` と `#4192` の `pnpm-lock.yaml` の `packages` を比較): + +- lockfile 全体は 878 → 872 パッケージ。消えたのは9件(`@sveltejs/kit@2.70.3`・`@sveltejs/adapter-vercel@6.3.4`・`@sveltejs/acorn-typescript@1.0.10`・`@types/cookie@0.6.0`・`cookie@0.7.2`・`devalue@5.8.1`・`kleur@4.1.5`・`picomatch@4.0.4`・`set-cookie-parser@3.1.0`)、増えたのは3件(`@sveltejs/kit@3.1.0`・`@sveltejs/adapter-vercel@7.0.1`・`cookie@2.0.1`)。 +- devalue・picomatch・kleur は依存から消えたのではなく、重複していた古い版が消えた。Kit 2 の `devalue@^5.8.1` が他の依存の 5.9.4 と別に解決されていたが、Kit 3 は `^5.9.4` を求めるので 5.9.4 の1つにまとまった。picomatch(4.0.5・4.0.7・2.3.2)と kleur(3.0.3)も他の依存の版が残る。 +- Kit の直接の依存は 12 → 8。消えたのは `@sveltejs/acorn-typescript`・`acorn`・`@types/cookie`・`kleur`・`set-cookie-parser`、増えたのは `@rolldown/pluginutils`。`cookie` は `^0.6.0` → `^2.0.1`、`magic-string` は `^0.30.5` → `^1.2.3`。 +- 消えた版に既知の未修正の脆弱性があるかは調べていない。セキュリティ上の恩恵としては扱わない。 + +## 周辺ツールと依存の Kit 3 対応 + +`node_modules/.pnpm` の全パッケージの配布コード(`.js`・`.mjs`・`.cjs`・`.svelte`・`.ts`)を走査し、`$app/*` と `@sveltejs/kit` の import を探した。該当は Kit 本体・adapter-vercel・Superforms だけだった。そのうえで、Kit の規約や設定ファイルに依存しうるものを個別に確認した。 + +| 対象 | 確認結果 | 判定 | +| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| svelte-meta-tags 5.0.3 | peer は Svelte `^5.0.0` のみ。配布物は `MetaTags.svelte`・`JsonLd.svelte` と純粋な関数で、Kit への依存はない。upstream は開発環境を Kit 3.0(#2298、2026-10-04)・3.1(#2337)に上げており、5.0.3(2026-10-06)はその後の公開。 | 対応不要 | +| super-sitemap 1.0.12 | peer は Svelte `>=4 <6` のみで、Kit の API は使わない。ルートは Vite の `import.meta.glob('/src/routes/**/+page*.svelte')` で列挙し、応答は標準の `Response` で返す。Kit 3 はルートの規約を変えないので影響しない。upstream は Kit 3 対応を明言しておらず、Kit 3 の `$app/manifest` でルートを取る案が open の issue #59 にある。 | 対応不要(`e2e/sitemap.spec.ts` で確認) | +| super-sitemap 2.x | 最新は 2.0.6(2.0.0 は 2026-06-25)。root の export が無くなり、`super-sitemap/sveltekit` などフレームワーク別の export に変わる major 更新。Kit 3 移行の必須条件ではない。Renovate から 2.x の PR が来ていない理由は未確認。 | 本移行では更新しない | +| eslint-plugin-svelte 3.23.1 | recommended の `svelte/no-navigation-without-resolve` は Kit 1〜3 を条件に有効で、現行の Kit 2 でも既に有効。3.23.1 の変更は、Kit 3 でも有効のままにするためのもの。Kit 1〜2 限定の `no-goto-without-base`・`no-navigation-without-base` は recommended に入っていない。routes の判定は `svelte.config.js` が無ければ既定の `src/routes`。 | lint 結果は変わらない | +| svelte-eslint-parser 1.9.0 | 親ディレクトリの `svelte.config.js` を自動で読み、`compilerOptions.runes` を runes の判定に使う。Kit 2.70.3 のコピー上で `svelte.config.js` を外して ESLint を実行し、指摘が 0 件のまま変わらないことを確認した。 | 対応不要 | +| svelte-check 4.7.7 | 4.7.6 で Kit 3 の設定集約(Vite plugin への移設)に対応済み。 | 対応不要 | +| flowbite-svelte 1.33.1・@dnd-kit/svelte 0.5.1・@lucide/svelte 1.59.0 | Kit の API を import せず、peer は Svelte のみ。node_modules の runes は `dynamicCompileOptions` で従来どおり未指定のまま。 | 対応不要 | +| 既存 E2E の期待値 | `workbook_edit.spec.ts` は form action を直接 POST し、応答を ActionResult の JSON として検査する。JSON で返すかの判定は Kit 3.1 でも `accept` ヘッダーのままで、非所有者への 403 は `fail()` ではなく `error(FORBIDDEN)` 由来なので、Kit 2・3 とも 403 になる。`workbook_order.spec.ts` の 400 は `+server.ts` の応答で、form action の変更とは無関係。 | 期待値は変わらない | + +## alias から subpath imports への置換 + +### Kit 3 での alias の扱い + +- 組み込みの `$lib` と `files.lib` は削除される。残した `files.lib` はエラーになる。 +- `$lib` は alias で維持できる。Kit 3.0.0 のソースは、組み込みの `$lib` が無いときのエラーで「To keep using `$lib`, add `alias: { '$lib': 'src/lib' }` to your SvelteKit config.」と案内する。alias は Vite の解決と tsconfig の `paths` の生成に引き続き使われる。 +- ただし `kit.alias` 自体が deprecated で、build 時に「The `alias` option is deprecated, and will be removed in a future version of SvelteKit. Use subpath imports instead」という趣旨の警告が出る。`@`・`$features` も同じ扱い。削除時期は示されていない。 +- 3.0.0 → 3.1.0 でこれらの扱いに変化はない(→「対象バージョン」)。 + +### Kit 3 が `$lib` を `#lib` に変えた理由 + +Kit の issue・PR・docs から読み取った経緯。 + +- **独自の alias より標準の仕組み:** 発端は Rich Harris の issue [#16182](https://github.com/sveltejs/kit/issues/16182)「replace `$lib` with `#lib`」(2026-06-26)。`$lib` は Kit 独自の alias だが、`package.json` の `imports` は TypeScript を含めて広く対応された標準の仕組みなので、`src/lib` はそちらで指す。`$app/*` は Kit の提供物、`#lib/*` は利用者のコード、と役割も分かれる。[PR #16360](https://github.com/sveltejs/kit/pull/16360) が 2026-07-17 に version-3 へ入った。 +- **`$lib` の実害:** Vite を通らない場所(`vite.config.ts`・ESLint の設定・Node のスクリプト)では `$lib` が解決されず、Node が package 内部の名前として認める `#` 始まりの名前と混在しがちだった([#15234](https://github.com/sveltejs/kit/issues/15234))。monorepo では、ライブラリ側のコードの `$lib` が実行中のアプリの `src/lib` に解決される問題が2021年から残っていた([#2879](https://github.com/sveltejs/kit/issues/2879)、2026-08-03 に close)。`package.json#imports` は import したファイルに最も近い `package.json` を基準に解決するので、この問題が起きない。 +- **`#` である理由:** Node の規格で、`package.json` の `imports` の名前は `#` で始まる必要がある。Vite と TypeScript は設定なしで解決する([`$lib` の docs](https://github.com/sveltejs/kit/blob/main/documentation/docs/98-reference/26-%24lib.md))。`lib` という名前は `$lib`・`src/lib` との連続性によるもので、技術的な必然はない。 +- **`alias` の非推奨:** Rich Harris の issue [#16468](https://github.com/sveltejs/kit/issues/16468) が「People really should just be using subpath imports」「Using The Platform」として `alias` 設定を非推奨にした([PR #16470](https://github.com/sveltejs/kit/pull/16470)、警告のみで動作は継続)。alias を使い続けたい場合の代替として Vite の `resolve.tsconfigPaths` が挙げられている。 +- **拡張子が必須になった理由:** 当初は tsconfig の `paths` にも `#lib` を入れて拡張子なしで書けたが、Vite を通らない場合(esbuild でまとめる場合など)に「it'll break in weird ways」ため「More harm than good」として外された([PR #16736](https://github.com/sveltejs/kit/pull/16736))。その結果、Node の標準どおりに拡張子が必要になった。PR の本文は拡張子の必須化そのものの理由を明記しておらず、これは `paths` を外したことからの帰結と読んだ。Playwright のランナーで `#lib` が解決できないのも、この「Vite を通らない場合」に当たる。 + +単一アプリの本プロジェクトでは monorepo の問題は関係しない。得られる利点は、Vite・Vitest・TypeScript・tsx(seed)が同じ標準の仕組みで import を解決するようになることと、`$features` の置換(PR A2)を含めて `alias` 非推奨の流れに沿えること。 + +### 置換の対象 + +| | `$lib` → `#lib` | `$features`・`@` → `#features` | +| ------------ | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| ファイル | `src` 配下の282 | `src` 配下の144 | +| specifier | 676(`from` 651・`vi.mock` 25) | 286(`$features` 284 = `from` 277・動的 import 1(`src/app.d.ts` の型参照)・`vi.mock` 6、`@/features` 2) | +| 拡張子の内訳 | 拡張子なし573・`.svelte` 101・`.js` 2 | `$features` は拡張子なし235・`.svelte` 49 | +| e2e / prisma | 参照なし | 参照なし | +| docs の記述 | 5ファイル9行(`architecture.md`・`agent-rules/testing.md`・`testing-e2e.md`・`server-cache.md`・`prisma-db.md`) | 2ファイル5行(`architecture.md`・`agent-rules/testing-e2e.md`) | + +`@` の参照は2件とも `@/features/...` だった。alias を定義しているのは `svelte.config.js` だけで、tsconfig の `paths` はそこから生成される。 + +### 拡張子の規約 + +- TS モジュールは `.js`(`$lib/types/task` → `#lib/types/task.js`)。TypeScript と Vite が `.ts` の実体に対応付ける。 +- コンポーネントは `.svelte` のまま。 +- runes モジュール(実体が `.svelte.ts`)は `.svelte.js`(`$lib/stores/local_storage_helper.svelte` → `#lib/stores/local_storage_helper.svelte.js`)。末尾の `.svelte` を拡張子と見なすと型エラーが5件出たため、[codemod_subpath_imports.py](./codemod_subpath_imports.py) は実在ファイルで拡張子を判定する。 +- 既存の `.js` 指定(2件)はそのまま。 +- E2E の spec では `#lib` を使わず、相対 import のままにする。Playwright のランナーは `#lib/*.js` を `src/lib/*.js` に解決するが `.ts` の実体に対応付けないため、`Cannot find module` で失敗する(2026-10-18 に実測)。 +- `package.json#imports` には `#lib/*` と `#features/*` だけを置き、実在しない `src/lib/index.js` への bare `#lib` は追加しない。 + +### コピー上の検証(2026-10-18) + +Kit 2.70.3 上のリポジトリのコピー(`staging` の `f02b7238`、`node_modules` は本体へのシンボリックリンク)に codemod を適用した。`$features` 側は `$lib` の置換に続けて適用し、`svelte.config.js` から `@`・`$features`・`$features/*` の alias を外した。リポジトリ本体は変更していない。 + +| 確認 | `$lib` → `#lib` | `$features`・`@` → `#features` | +| ----------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| 置換 | 未解決 0・残り 0。再実行で変化なし。 | 未解決 0・残り 0。再実行で変化なし。 | +| `svelte-check`(`pnpm check` 相当) | 置換によるエラー 0。残る1件はコピーに生成物 `prisma/.fabbrica` が無いための差で、本体では出ない。 | 同左。 | +| 単体テスト | 97ファイル・2726テストがすべて成功(`vi.mock('#lib/...')` を含む)。 | 同左。 | +| `vite build` + adapter-vercel | 成功。 | 成功。 | +| `tsx`(`db:seed` の import 経路) | seed が推移的に import する `#lib/server/database.js`・`#lib/types/task.js` を解決できる。 | seed が import する features の4モジュール(`initializers`・`crud`・`password`・`fixtures/workbooks`)を解決できる。 | +| `oxlint` / `eslint` | 置換による指摘なし。oxlint の警告1件は本体にもある既存のもの。 | 同左。 | +| `prettier --check` | `src/lib/clients/aizu_online_judge/clients.ts` が import の行幅超過。 | 加えて `KanbanTabBar.svelte`・`workbooks/create/+page.server.ts` が行幅超過。 | + +E2E・dev server・CI は実行していない。 + +### 再流入の防止 + +- `$lib`: Kit 2 は `$lib` を組み込みで解決するため、置換後に新しいコードへ `$lib` が混ざっても Kit 3 に上げるまで気付かれない。`.oxlintrc.json` の `no-restricted-imports`(`{ "patterns": [{ "regex": "^\\$lib(/|$)" }] }`)で `.ts` の `$lib` import を検出でき、`#lib` は通ることを oxlint 1.88.0 で確認した。`.oxlintrc.json` は `.svelte` を対象外にしているので、`.svelte` は ESLint の同じ規則で検出し、こちらも `$lib` を検出して `#lib` を通すことを確認した。構文ごとの検出範囲も実測した。oxlint(`.ts`)は `import type`・`export … from`・`export * from`・動的 `import()` を検出し、ESLint(`.svelte`)は `import type` を検出するが動的 `import()` は検出しない。`vi.mock('$lib/...')` は関数呼び出しなので、どちらの規則でも検出されない。この穴は、alias の変更時と PR B の spike 前に grep で残りが 0件であることを確かめて補う。 +- `$features`・`@`: `svelte.config.js` から alias を外すと、残った `$features/...` の import は `svelte-check` で `Cannot find module` になる(コピーで確認)。リント規則は要らない。 +- 作業中の別 branch は、rebase 後に codemod を再実行すれば新規ファイルの alias も置換される。衝突するのは、置換した import 行かその隣接行を同じ branch が変更している箇所に限られる。 + +### 気付いた既存の問題 + +`src/features/tasks/components/contest-table/TaskTableBodyCell.svelte` が `@/features/votes/components/VotableGrade.svelte` を import している。tasks feature から votes feature への import で、AGENTS.md が禁じる feature 間 import にあたる。`@` alias で見えにくくなっていた。置換しても違反は残る。 + +## 参照資料 + +- Kit: [公式移行ガイド](https://svelte.dev/docs/kit/migrating-to-sveltekit-3)、[Kit 3.0.0 リリース](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.0.0)、[Kit 3.0.1](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.0.1)・[3.1.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.1.0) のリリース、npm の 3.0.0 / 3.1.0 tarball のソース。 +- adapter: [adapter 7.0.0 リリース](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fadapter-vercel%407.0.0)、[adapter-vercel の CHANGELOG](https://github.com/sveltejs/kit/blob/main/packages/adapter-vercel/CHANGELOG.md)(7.0.1)。 +- 最低版と peer: [Kit の manifest](https://github.com/sveltejs/kit/blob/%40sveltejs/kit%403.0.0/packages/kit/package.json)・[adapter の manifest](https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-vercel%407.0.0/packages/adapter-vercel/package.json) と npm の registry を、リリースノートより優先した。リリース内の初期記述では Svelte 5.56.4 だが、最終的な Kit 3.0.0 の最低版は 5.57.1。 +- Superforms: [v3.0.0 リリース](https://github.com/ciscoheat/sveltekit-superforms/releases/tag/v3.0.0) と、npm の 2.31.0 / 3.0.0 tarball の差分。 +- `$lib` → `#lib` の経緯: Kit の issue [#16182](https://github.com/sveltejs/kit/issues/16182)・[#16468](https://github.com/sveltejs/kit/issues/16468)・[#15234](https://github.com/sveltejs/kit/issues/15234)・[#2879](https://github.com/sveltejs/kit/issues/2879)、PR [#16360](https://github.com/sveltejs/kit/pull/16360)・[#16470](https://github.com/sveltejs/kit/pull/16470)・[#16736](https://github.com/sveltejs/kit/pull/16736)、[`$lib` の docs](https://github.com/sveltejs/kit/blob/main/documentation/docs/98-reference/26-%24lib.md)。 +- 周辺パッケージ: インストール済みの配布物(svelte-meta-tags・super-sitemap・eslint-plugin-svelte・svelte-eslint-parser ほか)、super-sitemap の [issue #59](https://github.com/jasongitmail/super-sitemap/issues/59)、svelte-meta-tags の [#2298](https://github.com/oekazuma/svelte-meta-tags/pull/2298)・[#2337](https://github.com/oekazuma/svelte-meta-tags/pull/2337)、npm の registry。 + +## 確認限界 + +- Kit 3 上では、branch `#4192` で build・unit test・lint・check・E2E の全件を実行し、仮に deploy した preview で手動確認を行った(2026-10-21)。production への配備は未確認。 +- Kit 3.0.0 と 3.1.0 の比較は tarball のソースを読んだもの。Kit 3 上の `#lib`・`#features` の解決は、spike の build・unit test・check で確かめた。 +- alias 置換の検証は Kit 2.70.3・Vite 8.3.4・TypeScript 6.0.3・Vitest 5.0.5 のコピー上で行い、E2E・dev server・CI は含まない。 +- `npx sv migrate sveltekit-3` が `$lib` を `#lib` に自動置換するかは未確認。公式ガイドは自動変換の対象を明記していない。 +- `$lib` → `#lib` の経緯は、Kit の issue・PR の本文を WebFetch の要約経由で読んだもので、コメントでの議論は全部は読んでいない。 +- `kit.alias` の削除時期は公式に示されていない。「a future version」とだけある。 +- `svelte.dev` の移行ガイドはこの調査環境から接続できなかったため、同じ内容の `documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md`(GitHub)を引用した。Kit 3.0.0 の Major Changes は release ページで76項目すべてを確認したが、各項目の詳細は移行ガイドとの照合までで、PR 単位の差分は読んでいない。 +- Superforms の release ページは adapter 一覧と release assets が読み込めなかった。adapter 側の変更は tarball の peer と配布コードの差分で補った。 +- `Header.svelte` 以外の enhanced action が同一ページ宛であることは、`action=` 属性の走査で確認した。実行時の遷移先までは確認していない。 +- `vite.config.ts` への移設後に `dynamicCompileOptions` が node_modules の旧構文コンポーネントへ効くか、`import '$app/server'` が Kit 3 の Vitest で解決できるかは、実行して初めて分かる。→ Kit 3 の spike で確かめた(`sveltekit({...})` の直下に置けば build が通り、unit test 2726件も成功した)。 +- server-only への到達は、型のみの import を除き、alias と相対パスを解決した静的な import グラフで確認した。Kit の build による判定ではない。 +- ESLint の `svelte.config.js` 有無の比較は Kit 2.70.3 のコピー上で行った。 +- super-sitemap の Kit 3 対応は upstream が明言していない。 +- 移行ガイドによると、Kit 3 の配備検知(`updated`)は Vercel の skew protection があると受動的な検知が漏れることがある。本プロジェクトの Vercel 側の設定は repo から分からない。 + +## 前回調査からの訂正 + +2026-10-03・10-06 時点の調査から、次の点を改めた。 + +| 項目 | 前回の記述 | 正しくは | 計画への反映 | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | +| `$app/stores` | Kit 3 で削除される。 | export は残り、購読と `getStores` で例外を投げるスタブ。Superforms 2.31.0 のまま Kit 3 に上げると、build は通りフォームのある5画面が実行時に落ちる。 | v3 必須の根拠を補強。失敗は build ではなく実行時に出ると明記した。 | +| 別ページ宛の enhanced action | 現状の named action はすべて同じページ宛。 | `Header.svelte` のログアウト(`../../logout?/logout`)は別ページ宛で、Kit 3 の挙動変更に該当する。 | ログアウト失敗の E2E を追加した。 | +| `$lib` → `#lib` | Kit 3 移行で必須の作業。 | alias で `$lib` を維持できる(Kit 3.0.0 のエラー文言が案内)。ただし `kit.alias` は deprecated。 | 必須ではないが、共同開発者の新機能追加の前に先行 PR で置換する方針にした。 | +| 移行の規模 | `$lib` 278ファイルを含む大規模な移行。 | ファイル数の大半は再現可能な機械置換で、Kit 3 本体の実変更は数〜十数ファイル。 | PR を4本に分けた。 | +| 移行先バージョン | Kit 3.0.0 / adapter 7.0.0。 | 3.0.1・3.1.0・7.0.1 が出ており、3.1.0 に server-only 判定の修正が入っている。 | 移行先を 3.1.0 / 7.0.1 に変更した。 | +| 周辺パッケージ | svelte-meta-tags・super-sitemap は Kit peer を宣言しないので、移行後に検証する(中身は未確認)。 | 配布コードを確認し、どちらも Kit の API に依存しないと分かった。super-sitemap はルートの規約に依存するが、Kit 3 で不変。svelte-meta-tags は upstream が Kit 3 で開発している。 | 対応不要と判断し、E2E での確認は残した。 | +| 破壊的変更の網羅 | Kit 3.0.0 の CHANGELOG の先頭約40項目で照合した。 | Major Changes は76項目。残りも照合し、周辺ツール(eslint-plugin-svelte・svelte-eslint-parser・svelte-check)と既存 E2E への影響も追加で確認した。 | 新たな要対応はなし。確認結果を調査に追記した。 | +| #4115 の状態 | head `b59c0870` の CI が成功。 | 2026-10-18 に今の staging の上へ rebase され、head `5833c1d0` でも build・preview が成功。 | 判定は不変。 | +| 周辺 PR | Vercel CLI #4103 と Lefthook #4102 は先に取り込める。 | 2026-10-03 のレビュー時点で反映済み。 | 計画から削除した。 | +| 現行版と件数 | Svelte 5.57.1・Zod 4.6.5・svelte-meta-tags 5.0.2・svelte-check 4.7.6、`$lib` 278・`$features` 142ファイル。 | Svelte 5.57.2・Zod 4.6.7・svelte-meta-tags 5.0.3・svelte-check 4.7.7、`$lib` 282・`$features` 144ファイル(その後の依存更新とコード追加による変化で、誤りではない)。 | 判定は不変。 | From 9224fbbc8449347f16668aad9b477709dd0df417 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Wed, 21 Oct 2026 06:15:39 +0000 Subject: [PATCH 11/11] docs: remove sveltekit-3-adapter-vercel-7-upgrade dev notes Co-Authored-By: Claude Sonnet 5.5 --- .../codemod_subpath_imports.py | 92 ----- .../plan.md | 338 ------------------ .../summary.md | 85 ----- .../survey.md | 327 ----------------- 4 files changed, 842 deletions(-) delete mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py delete mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md delete mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md delete mode 100644 docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py deleted file mode 100644 index 63dd5333a..000000000 --- a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/codemod_subpath_imports.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Rewrite alias specifiers to package.json subpath imports with an explicit extension. - -Usage: python3 -I codemod_subpath_imports.py - -- lib: `$lib/...` -> `#lib/...` -- features: `$features/...` and `@/features/...` -> `#features/...` - -The rewrite is idempotent, so it can be re-run on a branch after rebasing onto the change. -""" -import os -import re -import sys - -root = sys.argv[1] -target = sys.argv[2] - -# (specifier prefix, source directory relative to the root, replacement prefix) -RULES = { - 'lib': [('$lib/', 'src/lib', '#lib/')], - 'features': [ - ('$features/', 'src/features', '#features/'), - ('@/features/', 'src/features', '#features/'), - ], -} - -if target not in RULES: - sys.exit(f'unknown target: {target} (expected one of {", ".join(RULES)})') - -rules = [ - (re.compile(r"""(['"])""" + re.escape(prefix) + r"""([^'"]+)\1"""), os.path.join(root, source), replacement) - for prefix, source, replacement in RULES[target] -] - -unresolved = [] -changed_files = 0 -changed_specs = 0 -file_changes = 0 -current_file = '' - - -def resolve(base_dir, path): - # a `.js` specifier is kept as is because TypeScript and Vite map it to the `.ts` source; - # other existing files keep their extension (e.g. `.svelte` components) - if path.endswith('.js') or os.path.isfile(os.path.join(base_dir, path)): - return path - # `x.svelte` that only exists as `x.svelte.ts` is a runes module and becomes `x.svelte.js` - candidates = [ - (path + '.ts', path + '.js'), - (path + '.svelte.ts', path + '.svelte.js'), - (path + '/index.ts', path + '/index.js'), - (path + '.js', path + '.js'), - ] - for source, specifier in candidates: - if os.path.isfile(os.path.join(base_dir, source)): - return specifier - return None - - -def make_replacer(base_dir, replacement): - def replace_specifier(match): - global changed_specs, file_changes - resolved = resolve(base_dir, match.group(2)) - if resolved is None: - unresolved.append((current_file, match.group(0))) - return match.group(0) - file_changes += 1 - changed_specs += 1 - return f'{match.group(1)}{replacement}{resolved}{match.group(1)}' - - return replace_specifier - - -for base in ('src', 'e2e', 'prisma'): - for dirpath, dirnames, filenames in os.walk(os.path.join(root, base)): - dirnames[:] = [name for name in dirnames if name not in ('node_modules', '.svelte-kit')] - for filename in filenames: - if not filename.endswith(('.ts', '.svelte', '.js')): - continue - full = os.path.join(dirpath, filename) - current_file = os.path.relpath(full, root) - text = open(full, encoding='utf-8').read() - file_changes = 0 - new_text = text - for pattern, base_dir, replacement in rules: - new_text = pattern.sub(make_replacer(base_dir, replacement), new_text) - if file_changes: - open(full, 'w', encoding='utf-8').write(new_text) - changed_files += 1 - -print(f'files changed: {changed_files}, specifiers changed: {changed_specs}, unresolved: {len(unresolved)}') -for item in unresolved: - print('UNRESOLVED', item) diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md deleted file mode 100644 index 8547cc815..000000000 --- a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/plan.md +++ /dev/null @@ -1,338 +0,0 @@ -# SvelteKit 3 / adapter-vercel 7 移行計画 - -## 概要 - -SvelteKit 2.70.3 → 3.1.0、adapter-vercel 6.3.4 → 7.0.1 に移行する。Kit 3 対応に必要な Superforms 2.31.0 → 3.0.0 の更新と、alias(`$lib`・`$features`・`@`)から `package.json#imports`(`#lib`・`#features`)への置換も含める。 -外部の依存ブロッカーはなく、移行は可能と判断した。根拠・影響範囲・検証結果は [survey.md](./survey.md) にまとめた。 -2026-10-21 時点で、#4115・PR A(#4190)・PR A2(#4191)はマージ済み。PR B は branch `#4192` で実装・手元の確認・preview での手動確認まで終わり、push と production の確認が残る。要点は [summary.md](./summary.md) にまとめた。計画を立てた時の基準は 2026-10-18 JST の `origin/staging`(`f02b7238`)。 - -## PR の構成と順序 - -| 順 | PR | 内容 | 土台 | マージの条件 | -| --- | ----- | ----------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------ | -| 1 | #4115 | Superforms 2.31.0 → 3.0.0(依存と lockfile のみ) | Kit 2.70.3 | 2026-10-18 にマージ済み(`dc9f2471`)。マージ前に5画面と既存 E2E を確認した(Phase 0)。 | -| 2 | PR A | `$lib` → `#lib`(282ファイル・676 specifier の機械置換) | Kit 2.70.3 | #4115 の確認後。共同開発者の新機能追加より前、遅くとも1か月以内(Phase A)。 | -| 3 | PR A2 | `$features`・`@` → `#features`(144ファイル・286 specifier の機械置換) | Kit 2.70.3 | PR A のマージ後、PR B より前(Phase A2)。 | -| 4 | PR B | Kit 3.1.0・adapter-vercel 7.0.1 と、設定・型・state・フォームの移行 | Kit 3.1.0 | Phase 1 の spike と Phase 3 の E2E が通ってから preview・production へ進む(Phase 1〜4)。 | - -1本ずつマージするのは、不具合が出た時に原因の PR を切り分けるため。CI は E2E を実行しないので、各段階の実ブラウザー確認と E2E は手元で行う。 - -## 実装フェーズ - -### Phase 0: #4115(Superforms 3.0.0) - -- [x] PR 本文の rebase のチェックボックスで Renovate に rebase させ、今の staging の上で CI を再実行する(workflow の re-run は当時の合成コミットを使うので、rebase の代わりにならない)。2026-10-18 に rebase し、head `5833c1d0` は staging `f02b7238` の直上。CI は build (24)・preview 成功、production は skipped。 -- [x] lockfile の差分が Superforms 本体と optional validator に限られることを確認する。pnpm 12.14.0 の再解決で書き換えは出ず、supply-chain policy の検査も通った(survey の「Superforms 3.0.0(#4115)」)。 -- [x] マージ前に、v3 で内部のページ状態の同期が変わるため、`superForm` を使う5画面を実ブラウザーで確認する。CI は E2E を実行しないので、PR の branch を手元で checkout して既存 E2E(`signin / redirect_after_login / votes / workbook_edit / workbook_order`)も流す。 - - 画面の確認は、#4115 の CI が作った preview deploy でも行える。preview は本番とは別の DB(`PREVIEW_DATABASE_URL`)を使う。 - - staging ドメインは他の PR の CI でも付け替わる(`ci.yml` の alias の step の条件が `github.ref != 'refs/heads/main'` のため)。確認には #4115 のデプロイ URL を使う。 - - 結果(2026-10-18): 5画面は #4115 の preview deploy で確認した。E2E は head `5833c1d0` を手元で checkout し(Superforms 3.0.0・Playwright 1.64.1、ローカルの `test_db`)、5本の spec の56件がすべて成功した。 - -| 画面 | 確認すること | -| -------------------- | --------------------------------------------------------------------------- | -| ログイン・新規登録 | 入力エラー時に入力値が残る、成功後に redirect される、ゲストログイン。 | -| 問題集の作成・編集 | 問題リスト(JSON の入れ子データ)が送信・再表示される、失敗時に入力が残る。 | -| 管理: アカウント移行 | 成功・失敗のメッセージ、一般ユーザーが実行できない。 | - -- [x] 問題がなければ staging にマージする。production は `main` への push でしか deploy されないので、staging へのマージは本番に影響しない。2026-10-18 にマージした(merge commit `dc9f2471`、tree は検証した head `5833c1d0` と同一)。staging への push の [CI](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/actions/runs/39128896093) も成功した。 -- [x] 挙動の変更が必要になったら、Phase 3 の確認表にあるテストを先に書き、対応する層だけを修正する。依存だけの更新では新規 unit test を追加しない。 - - 結果: 不要だった。使用中の API(`superValidate`・`superForm`・`zod4`)は不変で、preview での5画面の確認・手元の E2E 56件・staging の CI がすべて通った。マージ後の `svelte-check` も 0 errors・30 warnings で、2.31.0 のときと件数が変わらない。 - -### Phase A: PR A(`$lib` → `#lib`、Kit 2 上) - -- [x] `package.json` に `"imports": { "#lib/*": "./src/lib/*" }` を追加する。 -- [x] [codemod_subpath_imports.py](./codemod_subpath_imports.py) の `lib` モード(`python3 -I codemod_subpath_imports.py lib`)で置換し、`pnpm format` を実行する。拡張子の規約は survey の「拡張子の規約」に従う。 -- [x] `$lib` の import を禁止する `no-restricted-imports` を、`.ts` 向けに `.oxlintrc.json`、`.svelte` 向けに `eslint.config.mjs` へ追加する。Kit 2 は `$lib` を組み込みで解決するので、規則が無いと新しいコードの `$lib` に PR B まで気付けない。 -- [x] `docs/guides/architecture.md`・`agent-rules/testing.md`・`testing-e2e.md`・`server-cache.md`・`prisma-db.md` の `$lib` 記述を、`#lib` と拡張子の規約に更新する。 -- [x] `pnpm check / lint / test:unit --run / build` と既存 E2E(`signin / votes / workbook_edit / workbook_order`)で確認する。置換・設定・docs の変更なのでテスト先行は省略する。 - - 結果(2026-10-18、branch `refactor/lib-subpath-imports`): lint 成功(oxlint の既存の警告1件のみ)、check は 0 errors・30 warnings(置換前と同数)、unit test は97ファイル・2726件が成功、build 成功。E2E は `redirect_after_login` も含めた5本の spec の56件が成功。seed の import 経路も tsx で解決できる。変更は290ファイル(置換282・`package.json`・`.oxlintrc.json`・`eslint.config.mjs`・docs 5ファイル)。 -- [ ] 共同開発者の新機能の着手前、遅くとも1か月以内にマージし、連絡する。作業中の branch は rebase 後に codemod を再実行してもらう。codemod は repo に入れず、PR A の本文に全文を貼って共有する(レビュー手順・rebase 手順と一緒に記載)。 -- [x] cross-review は、手編集ではなくスクリプト出力なので AGENTS.md の除外規定(生成物・機械的変更)に照らして要否を判断する。レビュー者は `staging` で codemod と `pnpm format` を再実行し、diff の一致を確かめられる。 - - 結果(2026-10-18、commit `f629666a`): import の規約という共有 architecture の変更に当たるため、Codex の cross-review を受けた。staging(`dc9f2471`)で codemod と prettier を再実行すると `src/`・`e2e/`・`prisma/` が commit と一致することも確認した。Critical・Severe の指摘はなく、誤った置換も見つからなかった。 - - Minor(解消済み): lint 規則は `vi.mock('$lib/...')` と `.svelte` 内の動的 `import('$lib/...')` を検出しない(`.ts` の `import type`・`export … from`・`export * from`・動的 `import()` は oxlint が検出することを実測した)。`architecture.md` の「禁止している」は、この検出範囲を補足すると正確になる。 - - PR A では `architecture.md` に検出範囲を補足した。Kit 3 では、`vi.mock('$lib/...')` は Vitest が `module_removed_lib` で失敗し、`.svelte` の動的 `import('$lib/...')` は check が失敗するので、検出漏れ自体が無くなった。PR B(`444e4daf`)で `architecture.md` をこの挙動に合わせた(Phase 4 の cross-review の項目を参照)。 - - Nit: `git diff --check` の結果が review の資料に無かった。commit の範囲で実行して問題なし。 - -### Phase A2: PR A2(`$features`・`@` → `#features`、Kit 2 上) - -- [x] PR A のマージ後に着手し、`package.json#imports` に `"#features/*": "./src/features/*"` を追加する。PR A は #4190(`485ce861`)でマージ済み。branch は `refactor/features-subpath-imports`。 -- [x] codemod の `features` モードで `$features/...` と `@/features/...` を置換し、`pnpm format` を実行する。 -- [x] `rg -n '\$lib|\$features|@/features' src e2e prisma` で残りが 0件であることを確かめる。lint 規則と型チェックは `vi.mock` の specifier と `.svelte` 内の動的 `import()` を検出しないため。 -- [x] `svelte.config.js` の `alias` を削除する(`@`・`$features`・`$features/*` と、PR A で参照が 0 になった `$lib` の2行)。残った `$features` は型チェックで失敗するので、リント規則は要らない。 -- [x] `docs/guides/architecture.md`・`agent-rules/testing-e2e.md` の `$features` 記述を更新する。あわせて `svelte.config.js` と `tsconfig.json` の alias に関するコメントも更新した。 -- [ ] 確認・連絡・cross-review は Phase A と同じ。PR B の前にマージする。 - - 確認の結果(2026-10-18、未 commit の作業ツリー): lint 成功(oxlint の既存の警告1件のみ)、check は 0 errors・30 warnings(変更前と同数)、unit test は97ファイル・2726件が成功、build 成功、E2E は5本の spec の56件が成功、seed の import 経路も tsx で解決できる。staging(`485ce861`)で codemod と prettier を再実行すると、git 管理下の `src/`・`e2e/`・`prisma/` が作業ツリーと一致する。変更は149ファイル(置換144・`package.json`・`svelte.config.js`・`tsconfig.json`・docs 2ファイル)。 - - alias を外した後の確認: `$features` を import すると型チェックで `Cannot find module` になる。Kit 2 の生成 tsconfig には `$lib` の paths だけが残る(組み込み。再流入は PR A の lint 規則で防ぐ)。 - - Codex の cross-review(未 commit の diff): Critical・Severe の指摘なし、置換の誤りなし。Minor は、`architecture.md:147` の grep の例が `@/` を含まない点と、リポジトリ全体の残存確認が無い点。全体を検索すると `.coderabbit.yaml:28` に `$lib/server/database` が残っていた(PR A での取りこぼし)。`git diff --check` は問題なし。grep の例に `@/` を加え、`.coderabbit.yaml` を `#lib/server/database.js` に直して commit した(`bbb40c1e`、150ファイル)。 - -本 PR のスコープ外: - -- `TaskTableBodyCell.svelte` の feature 間 import(survey の「気付いた既存の問題」)。置換しても違反は残るが、直すのは別タスクとする。 - -### Phase 1: PR B・設定と module 解決 - -- [x] 独立した作業環境で `npx sv migrate sveltekit-3` の spike を行い、`pnpm build / check / lint / test:unit --run` が通ることを確認する。通らない場合は原因を切り分けるまで本線に取り込まず、Superforms 側の fork や shim で埋めない。特に次の3点を見る。 - - Vitest が Kit 3 で `$app/server` を解決できるか(`src/features/workbooks/zod/schema.test.ts` が Superforms の `/server` を実体で import する)。解決できなければ、同テストの import を root の `sveltekit-superforms` に切り替える。→ 解決できた(unit test 2726件が成功)。 - - 移設後の `dynamicCompileOptions` が node_modules の `SuperDebug.svelte` に効き、build が通るか。→ `sveltekit({...})` の直下に置けば効き、build が通る。`sv migrate` が出力した `vitePlugin: { dynamicCompileOptions }` は、Kit 3 では `config.vitePlugin` が削除済みで `svelte-kit sync` が失敗する。 - - `#lib`・`#features` が Kit 3 でも TypeScript / Vite / Vitest で解決され、新しい警告が出ないか。`sv migrate` が `$lib` に触れる場合は、PR A の結果と食い違わないか確認する。spike の前に `rg -n '\$lib|\$features|@/' src e2e prisma` が 0件であることも確かめる(PR A・PR A2 の後に入った `vi.mock` などを拾うため)。→ 0件。`#lib`・`#features` は Kit 3 でも解決でき、新しい警告も無い(build の警告は chunk サイズの1件のみ)。 -- [x] Kit 3.1.0・adapter-vercel 7.0.1 に更新し、`svelte.config.js` を `vite.config.ts` に集約する(→「設計方針」の設定例)。tsconfig の extends を `$app/tsconfig` に変える。 -- [x] 設定・機械的な import 変更はテスト先行を省略し、`pnpm build / check / lint` と既存 unit test で確認する。 - - `#4192` への反映後も spike と同じ結果(check の 20 errors も同じ箇所)。check の失敗は Phase 2 で直す方針(2026-10-19 に決定)。commit `76a6d0c3`(5ファイル、未 push)。この commit 単体では CI の check が落ちるので、Phase 2 の後に push する。 - - spike の結果(2026-10-19、`sv` 1.1.1、Kit 3.1.0・adapter-vercel 7.0.1): build・unit test・lint は成功、check は 20 errors(すべて Phase 2 の項目。下記)。build の `.vc-config.json` は `nodejs24.x`・`hnd1`・memory 3008・maxDuration 30。 - - `sv migrate` の出力のうち採用しなかったもの: `vitePlugin` の入れ子(上記)、Kit と adapter の `^` 範囲指定(`3.1.0`・`7.0.1` に固定)、実在しない `src/lib/index.js` への bare `#lib`、Markdown の `$lib` の置換(`architecture.md` の文意が壊れ、過去の dev-notes も書き換えた)、既存コメントの移動と `/* */` への書き換え。`tsconfig.json` は codemod の形(`$app/tsconfig` が削った設定をすべて提供する)に、既存の説明コメントを残した。 - - `tsconfig.json` から消えた9個(`allowJs`・`checkJs`・`esModuleInterop`・`forceConsistentCasingInFileNames`・`isolatedModules`・`moduleResolution`・`resolveJsonModule`・`skipLibCheck`・`verbatimModuleSyntax`)は、`$app/tsconfig` が同じ値を設定するので、重複として消した。デフォルトが厳しくなったからではない。`tsc --showConfig` で Kit 2(`staging`)と Kit 3 の実際の設定を比べ、9個とも同じ値であることを確かめた(2026-10-21)。 - - 実際の設定で違うのは、Kit 3 の基本設定から来る4つだけ: `paths` から `$lib` が消えた(意図どおり)、`types` に `$app/types` が増えた(`types` は継承されず上書きされるため)、`moduleDetection: "force"`(すべてのファイルをモジュールとして扱う)、`allowImportingTsExtensions: true`(`.ts` の拡張子の import を許す。`.js` と書く規約は変えない)。 - - 残したのは、基本設定に無い `strict: true` と、Kit 2 から引き継いだ `incremental`・`sourceMap`・`outDir`。`outDir` は svelte-check の `--tsgo` を壊すので、[CI の高速化](../../2026-10-20/ci-and-typecheck-speedup/plan.md)の Phase 2 で消す。`sourceMap` は `noEmit` では効かないので、同じ時に消してよい。 - -### Phase 2: PR B・ルート型と state - -- [x] `Handle` を `@sveltejs/kit/hooks`、`SubmitFunction` を `$app/forms` に移す。`$app/environment` を `$app/env` に揃える(テストの mock も含む)。 - - 結果: ソース4ファイルとテストの mock 4ファイルを `$app/env` に揃え、コメントの docs リンクも `$app-env` に直した。Kit 3 の実行時の `$app/environment` は `../env/index.js` を再 export するだけなので、mock を先に `$app/env` へ変えてもテストは通り続けた(mock は両方の名前に効く)。 -- [x] 修正の前に、E2E `keeps kanban edits while replacing filter URL`・`restores saved workbook filters without adding a history entry` を追加する。 - - Kanban のテストは `keeps kanban state while replacing filter URL` という名前で `workbook_order.spec.ts` に追加した。ドラッグは Playwright では不安定なので、並べ替えの状態が消えない仕組み(絞り込みの変更で履歴が増えず、`__data.json` の再取得も起きない)を検証する。修正前は `$app/stores` の例外でページが描画されず失敗した。 - - 復元のテストは `workbooks_list.spec.ts` に追加した。Kit 3.1.0 の `goto` は非推奨の `replaceState` も `replace` として受け付ける(`opts.replace ?? opts.replaceState`)ため、修正前から通る回帰の監視用のテストになる。 - - どちらも、壊れた実装に一時的に差し替えて失敗することを確かめた(Kanban は `shallow`・`replace` をそれぞれ外す2通り、復元は `replace` を外す1通り)。 -- [x] `KanbanBoard.svelte` の `$app/stores` を `$app/state` と Runes に移し、`replaceState` を `goto(..., { shallow: true, replace: true, state: page.state })` へ移す。`workbooks/+page.svelte` の `{ replaceState: true }` を `{ replace: true }` にする。 - - Kit 3 の `page.url` は型の上で読み取り専用(`searchParams` は `ReadonlyURLSearchParams`)なので、`kanban.ts` の `parseInitialCategories`・`parseInitialGrades` の引数を `Pick`、`buildUpdatedUrl` の引数を `Pick` に広げた(型だけの変更で、既存の unit test 36件が成功)。 -- [x] `resolve()` の呼び出しを、route ID と pathname(leading slash なし)の区別に合わせる。純粋な URL 変換を足す場合は既存の `workbook_url_params.ts / kanban.ts` を先に調べ、隣接 unit test を先行する。 - - Kit 3 の `resolve()` は、`/` で始まる引数を route ID(`(group)` を除き `[param]` を埋める。`?`・`#` 以降は残す)、それ以外を pathname として扱う。型は既知の route ID・pathname と、それに `?`・`#` を付けたテンプレート文字列だけを受け付ける。 - - 定数を渡す6か所(`HOME_PAGE`・`EDIT_PROFILE_PAGE` など)は値がそのまま route ID なので、Kit 2 の回避用の `@ts-expect-error` を外した。`resolve('/votes', {})` の `{}` も外した。 - - `buildWorkbooksUrl` の戻り値の型を `` `/workbooks?${string}` `` にして、`workbooks/+page.svelte` の3か所の `@ts-expect-error` を外した。復元は `` `/workbooks?${savedUrl.searchParams}` `` で組み立てる(保存される値は常に `buildWorkbooksUrl` の出力)。 - - `KanbanBoard` は route ID `` `/(admin)/workbooks/order?${...}` `` で組み立てる。 - - `AuthForm` の `alternativePageLink`(`buildSignupPath()` が実行時に作る `/signup?redirectTo=...` など)は型で表せないので、`@ts-expect-error` を残し、理由を Kit 3 に合わせて書き直した。新しい URL ユーティリティは追加していないので、追加の unit test は無い。 -- [x] `page.url` を直接変更していないことを確認する。Kit 3 の `page.url` は型の上で読み取り専用なので、check が 0 errors で通ることで確かめた。 -- [x] Phase 1 の check で見つかった Kit 3 の型の変更を直す。`(admin)/tasks/[task_id]/+page.server.ts` は `Actions` を `@sveltejs/kit` の汎用型から import しているため `params.task_id` が `string | undefined` になる。`./$types` の `Actions` に替える(同じ書き方の他の7ファイルは `params` を使わないので任意)。 - -Phase 1 の check の 20 errors(Phase 2 で直す): - -| 分類 | 件数 | 箇所 | -| ------------------------------------------------------------------------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `$app/environment` が見つからない | 4 | `features/auth/server/auth.ts`・`lib/stores/local_storage_helper.svelte.ts`・`features/workbooks/stores/replenishment_workbook.svelte.ts`・`lib/actions/handle_dropdown.ts` | -| `Handle` が無い(+ 暗黙の any 2件) | 3 | `hooks.server.ts` | -| `SubmitFunction` が無い(+ 暗黙の any 2件) | 4 | `(admin)/tasks/+page.svelte`・`(admin)/tasks/_components/TaskTableForImport.svelte` | -| `$app/stores` が見つからない | 1 | `(admin)/workbooks/order/_components/KanbanBoard.svelte` | -| 不要になった `@ts-expect-error`(Kit 2 の `RouteId` の型の不具合の回避) | 6 | `VotableGrade.svelte`・`AuthForm.svelte`(2件)・`TagForm.svelte`・`users/[username]/+page.svelte`・`votes/[slug]/+page.svelte` | -| `resolve()` の引数の数 | 1 | `votes/[slug]/+page.svelte:45` の `resolve('/votes', {})`(`{}` は Kit 2 の回避策) | -| `params` の型 | 1 | `(admin)/tasks/[task_id]/+page.server.ts:71`(上記) | - -`sv migrate` の Phase 2 向けタスク(`environment`・`imports`・`app-state`・`shallow-routing`・`paths`)は、Phase 1 と同じく本線に直接かけず、コピーで出力を確かめてから採用する。 - -- 結果(2026-10-19): `environment`・`imports`・`app-state`・`shallow-routing` の出力は計画どおりで、同じ変更を手で入れた。`paths` は何も変更しなかった。整形の副作用(`x: x` → `x`、空要素の自己閉じ化、`tsconfig.json` のコメント削除)は採用しなかった。`hooks.server.ts` は import が既存コメントの上に移ったので、元の位置で import 元だけを変えた。 -- Phase 2 の確認(2026-10-19、commit `a046e268`、23ファイル、未 push): check は 0 errors・30 warnings(変更前と同数)、lint 成功(oxlint の既存の警告1件のみ)、unit test は97ファイル・2726件が成功、build 成功(deprecated の警告なし)。E2E は `signin`・`redirect_after_login`・`votes`・`workbook_edit`・`workbook_order`・`workbooks_list` の82件が成功、1件は既存の spec がデータ不足で自ら skip したもの。 -- Phase 4 への申し送り: `agent-rules/sveltekit.md` の「Static: `resolve('/')` + `@ts-expect-error`(AppTypes merging quirk)」は Kit 3 では不要になった。上記の `resolve()` の扱いに合わせて書き換える。 - -### Phase 3: PR B・フォーム・認証・error - -- [x] 先に E2E `keeps form values after a failed enhanced submission`・`returns the fail status for an enhanced submission`・`redirects to home when logging out without a session` を書く。 - - 3本とも `signin.spec.ts` に追加した。Kit 3 の branch では3本とも成功する。staging(`e3d1145c`、Kit 2)のコピーでも流し、`returns the fail status ...`(Kit 2 は 200)と `redirects to home ...`(Kit 2 は `/workbooks` に留まる)が失敗することを確かめた。どちらも Kit 3 の挙動の変化を検出できる。`keeps form values ...` は Kit 2 でも成功する(挙動は同じ)。 -- [x] 次の表の確認を行う。一般ユーザーが管理 action を実行できないことも確認する。 - - 結果は表の下に記載。 - -| 層・責務 | 対象 | 確認・テスト名 | -| -------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 共通 util / 型 | src/lib/utils/auth_forms.ts、src/lib/types/auth_forms.ts、src/test/lib/utils/auth_forms.test.ts | 既存 util を再利用し、schemaShape・errors・posted・valid の契約を保持する。unit test は Superforms を mock するので、統合対応の証拠にはしない。 | -| 認証ルート・UI(層1) | login / signup、src/lib/components/AuthForm.svelte | 実際の superForm / enhance を通して `keeps form values after a failed enhanced submission` と既存の redirect_after_login spec。入力エラー・認証失敗・成功時の session と redirectTo を確認する。ゲストログインは `fetch` 直送なので別経路として確認する。 | -| 管理ルート・UI(層1) | (admin)/account_transfer の server / page | `rejects account transfer by a non-admin user`。権限拒否・validation failure・成功時の message と入力状態を確認する。action は `fail()` ではなく通常の返り値で失敗を表すため、status の挙動変更は受けない。 | -| 問題集ルート・UI(層1) | workbooks/create、workbooks/edit/[slug]、WorkBookForm.svelte | JSON の nested data と workBookTasks が送信・再表示されること、失敗時の入力保持、成功後の再初期化を、既存の workbook_edit / workbook_order と `keeps nested workbook tasks after a failed submission` で確認する。 | -| 検証フォーム(層1) | problems、votes/[slug]、workbooks/[slug] の server load/action | zod4 の errors・constraints と投票結果の表示を既存の votes spec で確認する。superValidate だけを使う経路と、独自 enhance の経路を区別する。 | -| Kit 3 の応答・state(層1) | 上記フォーム全体、Header のログアウト | `returns the fail status for an enhanced submission` で fail(400/403/500) を確認し、戻る/進む、再 load、redirect、onError の表示も見る。ログアウトは成功を既存の `signin.spec.ts`、未ログインでの失敗を `redirects to home when logging out without a session` で固定する。 | - -確認表の結果(2026-10-19、Kit 3.1.0 の branch `#4192`、commit `2c420dac`、未 push): - -- 共通 util / 型: 変更は不要だった。`auth_forms` の unit test は Superforms を mock するので、統合の確認は下の E2E で行った。 -- 認証: `keeps form values after a failed enhanced submission`(失敗のメッセージが出て、ユーザー名が残る)と既存の `redirect_after_login` が成功。ゲストログインのボタンは本番用のパスワードを送るため、ローカルの seed では成功の経路を流せない(`AuthForm` の HACK)。成功時の応答は HTTP 200 の redirect で Kit 2 と同じ。失敗時は Kit 3 では HTTP 400 になり `response.ok` が false なのでログイン画面に留まる(Kit 2 は 200 が返り、失敗してもホームへ遷移していた)。 -- 管理: 新しい `account_transfer.spec.ts` に `rejects account transfer by a non-admin user`(ページを開くと別ページへ redirect され、直接 POST すると `/login` への redirect が返る)と、管理者で新旧に同じ名前を入れると検証のメッセージが出て入力が残るテストを追加した。万一ガードが壊れていても実データをコピーしないよう、存在しないユーザー名を使う。成功の経路はデータを書き換えるので流していない。 -- 問題集: `keeps nested workbook tasks after a failed submission` を `workbook_edit.spec.ts` に追加した(管理者が編集ページでタイトルを2文字にして送信すると、検証のメッセージが出て、タイトルと問題リストが残り、保存されない)。編集の action は検証の失敗を `fail()` ではなく通常の返り値で返すので、status の変更は受けない。作成ページは問題の検索の操作が要るので UI では流していない。成功後の再初期化もデータを書き換えるので流していない。 -- 検証フォーム: 既存の `votes` spec が成功。 -- Kit 3 の応答・state: `returns the fail status for an enhanced submission`(`fail(400)` が HTTP 400 になる)と `redirects to home when logging out without a session` が成功。戻るは Phase 2 の復元のテスト、redirect は既存の `redirect_after_login` が守る。エラーページは上記の 404・403・500 で確かめた。enhanced form の action が `error()` を投げたときの UI の表示(Superforms の `onError`)は、既存の直接 POST のテスト(`workbook_edit` の 403)で status だけを確かめている。 -- 一般ユーザーが管理 action を実行できないこと: アカウント移行(上記)と、既存の `workbook_edit` の直接 POST(403)で確かめた。 -- E2E 全体(18本の spec): 116件が成功、1件は既存の spec がデータ不足で自ら skip したもの。lint も成功。 - -- [x] 404 / 403 / 500 と `+error.svelte` の表示、`handleError` に expected error も届くことを確認する。cookie は secure / httpOnly / sameSite を維持し、session が続くことを実フォームで確認する。 - - 404・403 は新しい `error_pages.spec.ts` で固定した(未知のルートで 404、一般ユーザーが `/workbooks/create` を開くと `load` の `error(403)`)。 - - 500 は、必ず例外を投げる一時的なルートと確認用の spec を置いて確かめ、確認後に両方とも削除した(commit していない)。status は 500 で、`+error.svelte` に「エラーが発生しました」・`500`・`Internal Error`・「ホームに戻る」が出た。 - - `handleError` は本アプリでは定義していないので、Kit 3 の既定の実装が使われる。500 のときはサーバーに例外のスタックが出て、404・403 のときは出なかった(既定の実装は想定済みのエラーを記録しない)。 - - cookie は `keeps the session cookie attributes after login` で固定した(`auth_session` が `path: /`・`httpOnly`・`secure`・`sameSite: Lax`。ログイン後に session が必要な `/users/edit` を開けることも確かめる)。 - -### Phase 4: PR B・配備と規約 - -- [x] robots / sitemap / meta tags と、anonymous・logged-in・admin の SSR を確認する。 - - robots・sitemap は Phase 3 の E2E 全件(`robots.spec.ts`・`sitemap.spec.ts` を含む)で成功。 - - `pnpm build` + `pnpm preview` に curl で10パスを取得した(2026-10-19)。anonymous は公開ページが 200、保護ページが `/login?redirectTo=...` への 307。guest は利用者ページが 200、admin ページが 307。admin はすべて 200。 - - 200 のページはすべて title・description・twitter・og:* を持つ。`pageMetaTags` を設定するルートは無いので、全ページが共通の tag になるのは仕様どおり。header は anonymous にログイン・アカウント作成のリンク、guest・admin に利用者名を SSR で出す。 -- [ ] Vercel preview の build / deploy と、生成された `.vc-config.json` の runtime・region・memory・maxDuration を確認する。production は preview と別に確認する。 - - ローカルの build で生成された `.vc-config.json` は `nodejs24.x`・`hnd1`・memory 3008・maxDuration 30、framework は sveltekit 3.1.0。 - - preview と production は push 後に人が確認する(agent は push しない)。 -- [x] `docs/guides/architecture.md`(feature server の保護・alias の説明)、`agent-rules/sveltekit.md`(pathname 規則)、`auth.md`(redirect)を移行後の仕様に更新する。 - - architecture.md: Kit 3 は `src/routes`・`static` 外のすべての `server/` と `server.*`・`*.server.*` を server-only として扱う(`exports/vite/utils.js` の `is_server_only_path` で確認)。alias は `sveltekit()` に置かない。`$lib` の lint 禁止は Kit 2 の習慣への案内として残す。 - - sveltekit.md: `resolve()` は route ID(`/` 始まり、group を含めて書く)と pathname(leading slash なし)を区別し、`string` は渡せない。Kit 2 の回避用の `@ts-expect-error` は不要で、実行時の pathname を渡す `AuthForm.svelte` だけ理由付きで残る。`redirect()` は外部 location で throw する(`exports/url.js` の `validate_redirect_location`)ので、利用者入力は従来どおり `isSameOriginRedirect()` で検証する。 - - auth.md: 同じ `redirect()` の変更を Security に記載。 - - Kit 2 の記述の全体の検索(AGENTS.md・CONTRIBUTING.md・agent-rules・設定・source): AGENTS.md の stack 表記、CONTRIBUTING.md の version と設定場所、testing.md の `$app/environment` の mock、testing-e2e.md の form action の応答(`fail(N)` の status、別ページの action)、`eslint.config.mjs` のコメント、`src/app.d.ts` の旧 URL を更新した。残る言及は意図した履歴の注記だけ。 - - `e2e/signin.spec.ts` の生の `JSON.parse` を testing-e2e.md の型付き `deserializeActionResult` に置き換えた。 - - commit `b5e822aa`(未 push)。check 0 errors / 30 warnings、lint 成功、unit 2726件成功、signin E2E 6件成功、`git diff --check` 問題なし。 -- [x] PR B は共有 architecture の変更に当たりうるため、AGENTS.md に従って cross-review の要否を判断する(実装した agent とは別の agent、不可なら CodeRabbit)。 - - 共有の設定・型・docs を変えるので必要と判断し、Codex で `staging...b5e822aa` を review した(2026-10-19、`codex exec -s read-only`、資料は prompt に埋め込み)。Critical・Severe は無し。 - - Minor(確認済み): `architecture.md:78` は、`src/routes`・`static` の除外が `server.*`・`*.server.*` にもかかるように読める。実装では除外は `server/` ディレクトリの判定だけにかかる。 - - Minor(確認済み): `workbook_order.spec.ts` の `keeps kanban state while replacing filter URL` は未保存の drag を作らないので、`__data.json` の再取得と履歴の追加は検出できるが、ローカル状態を初期データに戻す回帰は検出できない。テスト名が検証範囲より広い。 - - Minor(可能性あり): `workbook_edit.spec.ts` の `keeps nested workbook tasks after a failed submission` は問題リストを変えずに失敗させるので、nested data の送信が壊れてサーバーの元データに戻る回帰を通しうる。 - - 確認事項は解消: prettier・lefthook・`ci.yml` を含む repo 全体に `svelte.config.js`・`.svelte-kit/tsconfig` への参照は無い。Vercel の配備確認は上の項目で扱う。 - - ユーザーの選択で3件とも対応した。`architecture.md:78` は、ファイル名による判定(場所を問わない)と `server/` ディレクトリによる判定(`src/routes`・`static` を除く)を分けて書いた。Kanban のテスト名は `replaces filter URL without reloading data or adding history` に絞った(drag の E2E は不安定になりやすいので足さない)。workbook_edit のテストは、最後の行を削除してから失敗させ、失敗後も削除後の4行が残ることを確認する `keeps edited nested workbook tasks after a failed submission` にした。 - - 変異の確認: 編集ページの `superForm` に、更新のたびに元の問題リストへ戻す `onUpdate` を一時的に入れると、新しいテストは最後の assertion(5行が表示)で失敗した。元のテストは元のリストと比べるので、この変異を通す。変異は戻した。 - - 同じ時に、`architecture.md` の 6節(subpath imports)が Kit 3 で使うもの・使わないものを区別しにくいという指摘を受け、「使う」「使わない」「拡張子」の箇条書きと OK・NG の例に再編した。 - - format・lint・check(0 errors / 30 warnings)・`git diff --check` は成功。E2E は workbook_edit・workbook_order の 21件が成功。 - - 6節の lint の段落も Kit 2 前提(lint だけが `$lib` を止める)だったので、一時ファイルで Kit 3 の挙動を確かめて書き直した(2026-10-19)。`$lib` は `vi.mock('$lib/...')` でも Vitest が `module_removed_lib` で失敗し(Kit の Vite plugin の `resolveId` が `$lib` を検出する)、`.svelte` の動的 `import('$lib/...')` は check が `Cannot find module` で失敗する。`vi.mock('$features/...')` は解決されないまま無視され、本物のモジュールが使われる。残る穴はこれだけなので、`rg` の確認はその理由で残した。一時ファイルは削除した。 - -### 手動確認: PR B の preview・配備 - -unit test と E2E が通らない操作のうち、Kit 3 の変更が当たるものを手で確認する。1〜7 は preview で、成功条件の smoke test(ログイン・投票・編集)と一緒に行う。 -依存パッケージで Kit 3 が削除した `$app/stores`・`$app/environment` を参照するのは eslint-plugin-svelte の lint 規則だけで、実行時に壊れるものは無い(2026-10-19 に `node_modules` を検索)。 - -優先度・高(E2E が無く、Kit 3 の変更が直接当たる): - -- [x] 1. 提出状況の更新: /problems・問題集の詳細・利用者ページで、ドロップダウンとモーダルから状態を AC などに変え、表示が変わり、リロード後も残る。最もよく使う操作だが E2E が無く、`use:enhance` の form action は Kit 3 で失敗時の応答(`fail(N)` の status)が変わった。 - - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 -- [x] 2. admin の問題インポート(/tasks): コンテストの取得(`?/fetch`)から問題の作成(`?/create`)まで通す。Phase 2 で型の import を直した独自の enhance callback と `goto(result.location)` を通る。外部 API を使うので E2E が無い。 - - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 -- [x] 3. 新規登録とアカウント設定: signup の入力エラーの表示と登録の成功、AtCoder ID の検証コードの発行・確認・リセット。どれも `fail(N)` の status が変わったフォーム。 - - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 - - 「確認」は devcontainer では必ず失敗し、Kit 3 では HTTP 500 として見える(2026-10-21 に確認)。#4061(`6696b604`)で、秘密の値をコンテナに置かないため `CONFIRM_API_URL` を設定しない方針にし、`compose.yaml` で空の `.devcontainer/empty.env` を `.env` に重ねている。そのため `process.env.CONFIRM_API_URL` が無く、action が `fail(500)` を返す。seed の `admin`・`guest` は連携済みなので、開発には支障が無い。 - - 同じリクエストを Kit 2 に送ると、応答の中身は同じ `{"type":"failure","status":500,…}` で、HTTP status だけが 200 だった。Kit 3 で壊れたのではなく、`fail(N)` の status がそのまま返るようになって見えただけ。「発行」と「リセット」は手元の Kit 2・Kit 3 とも 200 で成功した。 - - 「確認」の成功は、`CONFIRM_API_URL` が設定された Vercel の preview で確かめる。→ Kit 3 を仮に deploy した preview で、本人確認が成功した(2026-10-21、ユーザーが確認)。 - - PR B のスコープ外(Kit 2 からある問題で、別 PR で直す。調査は [atcoder-verification-failures/survey.md](../../2026-10-21/atcoder-verification-failures/survey.md)): - - 本番の DB で、本人確認が完了していないケースがときどきある。検証コードに有効期限は無く、失敗の理由がログにも画面にも残らないため、原因はまだ決められない。 - - `users/edit/+page.svelte` は `load` の `data.message`(常に空)だけを表示し、action が返す `form.message` を表示しないので、成功・失敗のメッセージが画面に出ない。 - -優先度・中: - -- [x] 4. 選択状態の保存(`browser` の import 元を `$app/env` に変えた store): /problems のコンテスト種別のタブと問題一覧のタブを切り替えてリロードし、選択が残る。/workbooks で条件を選び、別のページから戻ると条件が復元される。unit test は mock なので、実際のブラウザーでの保存は未確認。 - - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 -- [x] 5. admin の残りの画面(admin でログインして操作する): - - 結果: Kit 3 を仮に deploy した preview で、下の3画面の操作をすべて確認した(2026-10-21、ユーザーが確認)。 - - グレードの設定(/tasks/grade): 問題のグレードを変えると保存され、リロード後も残る。`use:enhance` の form action(`?/setTaskGrade`)で、失敗時の status が変わったフォーム。 - - 問題の編集(/tasks/[task_id]、例: `/tasks/abc001_a`): グレードを変えて保存(`?/update`)すると保存され、別の問題の値は変わらない。Phase 2 で `Actions` を `@sveltejs/kit` の汎用の型から `./$types` に替えた画面(`params.task_id` が `string | undefined` になるのを直した、型だけの変更)。 - - タグの編集(/tags/[tag_id]): タグ名を変えて保存すると保存される。「タグ一覧へもどる」と、問題へのリンク(Phase 2 で `resolve()` を直した `TagForm.svelte`)が正しいページを開く。 - - `/tasks/[task_id]` は devcontainer では存在する問題でも 500 になるので、preview で確認する(2026-10-21 に確認)。`load` が表示のたびに `taskTagsApiService.getTaskTags()` で Google Apps Script(`script.google.com`)からタグを取得し、失敗するとそのまま投げる。devcontainer の外向き通信の firewall(`30b5f207`、`.devcontainer/init-firewall.sh`)は `script.google.com` を許可していないので、`fetch failed`(`EHOSTUNREACH`)で失敗する。Kit 2(`staging` のコピー)でも同じく 500 で、Kit 3 の regression ではない。→ Kit 3 を仮に deploy した preview で、`/tasks/[task_id]` の閲覧とグレードの保存を確認した(2026-10-21、ユーザーが確認)。 - - PR B のスコープ外(Kit 2 からある問題で、別 PR で直す): - - タグの取得に失敗すると、問題の編集画面全体が 500 になる。本番でも Google Apps Script が遅い・落ちていると起きうる。取得に失敗したら DB のタグだけで表示する案がある。 - - Google Apps Script の URL が `src/lib/constants/urls.ts` ではなく `tasktagsApiService.ts` に直接書かれている。firewall の一覧は「アプリが呼ぶ外部 API(`urls.ts`)」を基準にしているため、漏れた。`urls.ts` に移すか、firewall で許可するか(Apps Script の応答は `script.googleusercontent.com` へ転送されるはずで、エージェントが出られる範囲が広がる)を決める。 -- [x] 6. 問題集の作成(/workbooks/create): 入力エラーを出し、直して保存すると問題リストも保存される。編集は E2E があるが、作成には無い。 - - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 -- [x] 7. 画面遷移: header のドロップダウン(利用者メニュー・外部リンク)が開閉する。別のページへの遷移では spinner が出て、同じページでの条件変更では出ない。ブラウザーの戻る・進むが正しく動く。 - - 結果: Kit 3 を仮に deploy した preview で確認した(2026-10-21、ユーザーが確認)。 - -ローカル: - -- [x] 8. `pnpm dev`: 起動・HMR と、terminal とブラウザーの console に新しい警告が出ないか。build の警告は確認済みだが、dev server は未確認(「検証手順と完了条件」の「build と dev に出る新しい警告・エラーはすべて調査する」)。 - - 結果(2026-10-21): `#4192`(`cfa16889`)を `git archive` したコピーで `vite dev` を起動し、Playwright で anonymous・guest・admin の16ページを開いた(初回と2回目)。起動と HMR は正常で、新しい警告は無い。 - - ブラウザーの console は、Kit 2(`staging` のコピー)と同じ3種類だけ: firewall が `fonts.googleapis.com`・`www.googletagmanager.com` を許可しないための `net::ERR_ADDRESS_UNREACHABLE`、flowbite-svelte の `Tabs` の deprecated の警告2種(`ulClass`・`contentClass`)。 - - HMR: `/about` のコンポーネントを書き換えると、ページ全体を再読み込みせずに反映され、元に戻した変更も同じく反映された。console のエラーは無い。 - - terminal: 依存の事前スキャンが `src/routes/about/SectionSnippets.svelte` の snippet の export で `PARSE_ERROR` になり、依存の事前バンドルを飛ばす(Kit 2 も同じ9件)。そのため初回の表示では、依存を見つけるたびに `optimized dependencies changed. reloading` が起きる。PR B のスコープ外(Kit 2 から)。 - - 注意: 同じディレクトリで dev server を2つ動かすと、Vite の依存のキャッシュ(`node_modules/.vite`)を共有して壊れる(`Failed to hydrate`・`lifecycle_outside_component`・`Cannot read properties of undefined (reading 'call')`)。今回、ユーザーの `pnpm dev`(`127.0.0.1:5173`)と確認用の dev server(`[::1]:5173`)が重なって起き、別のコピーで動かすと消えた。Kit 3 の問題ではない。 - -配備時: - -- [ ] 9. デプロイをまたぐタブ: production のデプロイ前にタブを開いておき、デプロイ後にそのタブでページ遷移とフォーム送信をする。メジャー更新なので、古い client と新しい server が食い違うのはこの時。 -- [ ] 10. 配備後の Vercel のログ: 最初の数日、500 や見慣れないエラーが出ていないか。Kit 3 の既定の `handleError` は想定外のエラーだけを記録する。 - -### 手動確認の自動化: PR B(手動確認の1・4) - -手動確認の1(提出状況の更新)と4(/problems のタブの保存)を E2E にする(2026-10-21 にユーザーが決定)。どちらも今は E2E が無く、DB の後片付けが要らないか、元に戻せる。本番のコードは変えない(ユーザーの判断)。 - -- [x] `e2e/submission_status.spec.ts` の `keeps the updated status after reload`: guest で /problems の既定の ABS の表を開き、seed で唯一の ABS の問題 `practice_1`(Welcome to AtCoder)のセルのドロップダウンで、今と違う状態を選ぶ。`/problems?/update` の POST を待ってリロードし、ドロップダウンを開き直して、選んだ項目にチェックのアイコンがあることを確かめる。最後に元の状態へ戻す。 - - セルの状態は背景色でしか表されないので、リロード後のドロップダウンのチェックのアイコンで確かめる。DOM の構造に依存するが、本番のコードを変えないためにこの方法をとる。 - - ドロップダウンは自前の `fetch` で送信し、`response.ok` も結果の種類も見ずに画面を更新する(Kit 2 から)。画面だけでは保存の失敗を検出できないので、必ずリロードしてから確かめる。 -- [x] `e2e/problems_tabs.spec.ts` の `keeps the selected problem list tab after reload`: 「グレード別」のタブを選んでリロードし、`aria-selected="true"` のままであることを確かめる(flowbite の `TabItem` は `role="tab"`・`aria-selected` を出す)。 -- [x] `e2e/problems_contest_table.spec.ts` の `keeps the selected contest type after reload`: 「ABC 319 〜」のグループ(利用者が最も多い)を選んでリロードし、その表の見出し(AtCoder Beginner Contest 319 〜(7 問制))が出ることを確かめる。最初は xDPC で書いたが、ユーザーの指摘で ABC 319 〜 に替え、変異の確認もやり直した。既定は `abs` なので、保存されていなければ失敗する。ボタンの選択状態は CSS の class でしか表されないので、表示される内容で確かめる。 -- [x] 変異で検出力を確かめる: `updateTaskResult` が保存しないようにすると提出状況のテストが、`LocalStorageWrapper` が保存しないようにするとタブの2つのテストが失敗すること。変異は戻す。 -- 結果(2026-10-21): 3つとも `--repeat-each=3` で9回すべて成功。変異(`updateTaskResult` が保存しない・`LocalStorageWrapper` が保存しない)を入れると、3つともリロード後の assert で失敗し、既存の xDPC のテストは通った(ABC 319 〜 に替えた後も、`LocalStorageWrapper` の変異で失敗することを確認)。変異は戻した。format・lint(既存の警告1件のみ)・check(0 errors / 30 warnings)・`git diff --check` は成功。E2E の全件は 119 passed / 1 skipped(前回の 116 に3つ増えた)。 - -- 既存の動作を確かめるテストなので、テスト先行(RED)は無い。変異で代わりに検出力を確かめる。 - -残り・保留(2026-10-21 にユーザーが決定): - -- 残りタスク(別 PR): 3(signup の入力エラーの表示と、Alice での発行 → リセット。「確認」は外部 API が要るので service の unit test のまま)、5 のうち /tasks/grade とタグの編集(値を戻して E2E にできる)、6(問題集の作成。作った問題集を消す後片付けが要る)。 -- 保留: 2(問題のインポートはサーバーから外部 API を呼ぶので Playwright で差し替えられず、DB に問題を作る)、5 のうち `/tasks/[task_id]`(devcontainer では firewall で 500 になる。タグの取得を直す別 PR の後)、7(spinner は一瞬しか出ず assert が不安定。利用者メニューの開閉はログアウトの E2E で通っている)。 -- E2E は CI では流していない(`ci.yml` の TODO)。追加したテストが回帰を防ぐのは、手元で流したときだけ。 - -### Phase 5: 任意機能(別タスク) - -- [ ] 必須移行の配備後に、更新通知(`updated.current`)や tracing を選ぶ。候補と推薦度は survey の「便利な機能・採用推薦」。移行の成功条件には混ぜない。 -- [ ] 更新通知を採用する場合は、先に Vercel の skew protection が有効かを確認する。有効だと古い版のユーザーの通信は古いデプロイに送られ、ページ遷移などの通常の通信では新しいデプロイを検知できないことがある。確認先は Vercel のプロジェクト設定か、CI の preview ジョブで `vercel build` が出力する `config.json` の `__vdpl`(repo からは分からない)。 - -## 設計方針 - -- **Superforms を最初に入れる:** 3.0.0 は Kit 2 と 3 の両方に対応するので、Kit 2 上で先に入れてフォームの基準を確認できる。後で Kit 3 のフォームに不具合が出た時、原因を Kit 3 側に絞れる。 -- **alias の置換を Kit 3 本体から切り離す:** `$lib` は Kit 3 でも alias で維持できるが、`kit.alias` 自体が deprecated。共同開発者が1か月以内に新機能を追加するため、新しい `$lib` の import が増える前に置換する。`#lib` が Kit 2 でも解決されることはコピー上で確認済みなので、Kit 3 を待たずに先行できる。 -- **`$features`・`@` は PR A と分ける:** PR A が既に282ファイルあるため。PR A2 で alias の参照を 0 にすれば、PR B は `alias` を使わずに済み、`kit.alias` の deprecated 警告も出ない。 -- **移行先は Kit 3.1.0 / adapter 7.0.1:** 3.1.0 に 3.0.x の server-only 判定の修正が入っている。 -- **Kit と adapter は同じ PR で更新する:** adapter 7 は Kit 3 を peer に要求し、`builder.generateServerInstance` を呼ぶ。片方だけの更新は Build が失敗する。 - -設定移行は既存の値を保つ。`vite.config.ts` の `plugins` を次の形にし、現行の `optimizeDeps / test` の設定は残す。 -`alias` は書かない。`$lib` の alias を残すと Kit 3 で `$lib` が黙って復活し、新しいコードの逆戻りを build で検知できなくなる。`dynamicCompileOptions` は、Superforms の root entry が巻き込む旧構文の `SuperDebug.svelte` のために残す。adapter の runtime は実行環境の Node 24 に合わせて生成結果を確認する。 - -```typescript -import adapter from '@sveltejs/adapter-vercel'; -import { sveltekit } from '@sveltejs/kit/vite'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - plugins: [ - tailwindcss(), - sveltekit({ - preprocess: vitePreprocess(), - adapter: adapter({ - regions: ['hnd1'], - memory: 3008, - maxDuration: 30, - }), - compilerOptions: { runes: true }, - dynamicCompileOptions({ filename }) { - if (filename.includes('node_modules')) { - return { runes: undefined }; - } - }, - }), - ], - // Keep the existing optimizeDeps and test options here. -}); -``` - -公式の `npx sv migrate sveltekit-3` は PR B の作業時に独立した作業環境で使い、自動変換しきれない設定・`dynamicCompileOptions`・query/hash・依存側の旧 API は手で確認する。alias の置換は PR A・PR A2 の自前の codemod で済ませる。 - -### 却下した代替案 - -- Kit と adapter を別々にマージ: peer と Builder API が不整合になり、実際に #4104・#4105 の Build が失敗した。 -- #4105・#4104 の 3.0.0 / 7.0.0 をそのまま採用: 3.1.0 / 7.0.1 に server-only 判定などの修正が出ている。 -- Superforms 2.31.0 のまま Kit 3 へ: Kit 3 の `$app/stores` は購読で例外を投げるスタブで、install と build は通っても `superForm()` を使う画面が実行時に失敗する。 -- `$app/stores` の互換 shim や依存の独自 fork を恒久運用: フォームと認証の依存に保守負担を持ち込む。公式 v3 が対応済みのため不要。 -- `$lib` を alias で維持して `#lib` を先送り: Kit 3 では動くが、共同開発者の新機能が `$lib` の import を増やすほど置換の衝突と手戻りが増える。 -- `$lib` → `#lib` を Kit 3 本体の PR に混ぜる: 282ファイルの機械置換が数ファイルの実変更を埋もれさせ、Kit 3 の回帰と置換の不具合を切り分けられなくなる。 -- `$features`・`@` を PR A に含める: PR A がさらに大きくなり、レビューと共同開発者の rebase が重くなる。 -- `$features` を alias のまま Kit 3 へ持ち込む: 動くが deprecated 警告が残り、alias が削除される時に同じ置換を Kit 3 の後でやり直すことになる。 -- alias の置換に公式の `sv migrate` を使う: 自動置換の対象が明記されておらず、Kit 2 上の先行 PR では使えない。実在ファイルで拡張子を判定する自前の codemod なら、結果を再現・検証できる。 -- 移行と同時に全フォームを remote functions へ置換: 実験機能で変更範囲が大きく、現行 Superforms + Zod の設計も変えるため別の検討にする。 -- 移行と同時に URL utility や UI を再設計: removed API への対応に絞り、既存フォームの業務仕様は維持する。 -- `posted` や `flashMessage` など Superforms の非推奨 API を移行と同時に整理: 3.0.0 で動作するため、削除された時に対応する。 -- super-sitemap を移行と同時に 2.x へ更新: export が変わる major 更新で、Kit 3 移行の必須条件ではない。1.0.12 は Kit の API に依存しない。 - -## 検証手順と完了条件 - -各 PR で次を実行する。 - -```bash -pnpm install -pnpm build -pnpm lint -pnpm check -pnpm test:unit --run -pnpm test:e2e -- e2e/signin.spec.ts e2e/redirect_after_login.spec.ts e2e/votes.spec.ts e2e/workbook_edit.spec.ts e2e/workbook_order.spec.ts e2e/robots.spec.ts e2e/sitemap.spec.ts -git diff --check -``` - -- E2E は既存の test DB・認証 fixture を用意する。`playwright.config.ts` は build + preview を起動するため、CI の Node 24 build / unit test だけで完了扱いにしない。 -- PR B では `kit.alias` を使わないため、build と dev に出る新しい警告・エラーはすべて調査する。 -- PR B は preview の `pull / build / deploy --prebuilt / alias set` に加え、production の `build --prod / deploy --prod --prebuilt` と、ログイン・投票・編集の smoke test も成功条件に含める。 -- 失敗時は該当 PR を通常の revert で戻し、Kit 2 / adapter 6 の成果物に戻せる状態を保つ。DB schema / migration はこの計画の対象外。 -- codemod は repo に入れず、PR A の本文に全文を貼って共有する。PR A2 の `features` モードも同じスクリプトを使う。 diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md deleted file mode 100644 index d773007b7..000000000 --- a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/summary.md +++ /dev/null @@ -1,85 +0,0 @@ -# SvelteKit 3 移行のまとめ - -SvelteKit 2.70.3 → 3.1.0、adapter-vercel 6.3.4 → 7.0.1 への移行の要点(2026-10-21 時点)。 -作業の記録は [plan.md](./plan.md)、調査の詳細は [survey.md](./survey.md) にある。 - -## いまの状況 - -| PR | 内容 | 状態 | -| ---------------------- | ------------------------------ | ---------------------------------- | -| #4115 | Superforms 2.31.0 → 3.0.0 | マージ済み | -| #4190(PR A) | `$lib` → `#lib` | マージ済み | -| #4191(PR A2) | `$features`・`@` → `#features` | マージ済み | -| PR B(branch `#4192`) | Kit 3 本体 | 手元と preview で確認済み、未 push | - -- 確認済み: check・lint・unit test・build・E2E(119件)、preview での手動確認、Codex のレビュー(重大な指摘なし)。 -- 残り: push と CI、production への配備と確認(→「残りタスク」)。 - -## なぜ移行したか - -- adapter-vercel 7 以降は Kit 3 が前提で、Kit 2 のままでは Vercel 向けの更新を取り込めない。 -- 速度やサイズの恩恵は無い(build は約3秒遅く、初回表示の JS は約4 KB 増)。得られるのは、開発時の検査の強化。 - -## 主な決定 - -- PR を4本に分け、1本ずつマージした。不具合が出た時に原因を切り分けるため。 - - Superforms は Kit 2・3 の両方に対応するので、最初に Kit 2 上で入れた。 - - alias の置換は、共同開発者が新機能を足す前に、Kit 2 上で先に済ませた。 -- Kit と adapter は同時に更新する。片方だけだと build が失敗する。 -- 版は 3.1.0 / 7.0.1 に固定した。3.1.0 に server-only の判定の修正が入っている。 -- alias は使わず、`package.json` の `imports`(`#lib`・`#features`)にした。Kit 3 で alias は非推奨。 -- 公式の `sv migrate` はコピーで試して一部だけ採用し、alias の置換は自前の codemod([codemod_subpath_imports.py](./codemod_subpath_imports.py))で行った。 -- 却下した案は plan.md の「却下した代替案」にある。 - -## 知っておきたい Kit 3 の挙動 - -- `fail(400)` は HTTP 400 で返る(Kit 2 は 200)。 -- 別ページの action を持つフォーム(ログアウト)は、失敗しても action のページへ移る。 -- `$app/stores` は使うと実行時に例外になるので、`$app/state` を使う。`$app/environment` は `$app/env` に変わった。 -- `resolve()` に渡せるのは実在する route だけ。`/` 始まりは route ID として扱われ、group も含めて書く。 -- `redirect()` は外部の URL へは throw する。 -- `server/` ディレクトリと `*.server.*` のファイルは、client から import できない(`features/*/server/` も対象)。 -- `svelte.config.js` は使えず、置くとエラーになる。設定は `vite.config.ts` の `sveltekit()` に書く。 -- `$lib` はどこに書いてもエラーになる。ただし `vi.mock('$features/...')` だけはエラーにならず、黙って無視される。 -- import の拡張子は、TS が `.js`、runes モジュールが `.svelte.js`。E2E では `#lib` が解決できないので相対 import にする。 - -根拠になったコードと、Kit 2 との比較の結果は survey.md と plan.md(Phase 2〜4)にある。 - -## 開発環境での注意 - -- E2E は CI で流していない。手元で流す。 -- devcontainer では次のことが起きるが、Kit 3 の不具合ではない。 - - AtCoder ID の「確認」が必ず失敗する。秘密の値を置かない方針で、`CONFIRM_API_URL` が無いため(#4061)。 - - `/tasks/[task_id]` が 500 になる。firewall が Google Apps Script への通信を通さないため。 - - 同じディレクトリで dev server を2つ動かすと、Vite のキャッシュを共有して壊れる。 - -## 残りタスク - -PR B: - -- [ ] push し、CI が通ることを確かめる(push は人が行う)。 -- [ ] preview と production の build・deploy と smoke test(preview は仮の deploy で確認済み)。 -- [ ] 配備時の確認: デプロイ前から開いていたタブの動作と、配備後の Vercel のログ。 -- [ ] 共同開発者への連絡(PR A・A2 のマージと、作業中の branch での codemod の再実行)。 - -別 PR(計画あり): - -- [CI と型チェックの高速化、actions の SHA 固定](../../2026-10-20/ci-and-typecheck-speedup/plan.md) -- [Prettier から oxfmt への移行(#2226)](../../2026-10-21/oxfmt-migration/plan.md) -- [AtCoder ID の本人確認がときどき失敗する件](../../2026-10-21/atcoder-verification-failures/survey.md) - -別 PR(計画はまだ無い): - -- `/tasks/[task_id]` が、タグの取得に失敗すると 500 になる。 -- E2E の追加(signup、`/tasks/grade` とタグの編集、問題集の作成)。 -- `TaskTableBodyCell.svelte` が別の feature(votes)を import している。 -- 提出状況のドロップダウンが、サーバーの応答を確かめずに画面を更新する。セルの状態が色だけで表される。 -- 任意機能の検討(更新通知、tracing)。 -- 細かいもの: dev server の依存の事前スキャンのエラー、flowbite-svelte の `Tabs` の非推奨の props、Superforms の `posted`。 - -## 出典 - -- [SvelteKit 3 の公式移行ガイド](https://svelte.dev/docs/kit/migrating-to-sveltekit-3) -- リリース: [Kit 3.0.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.0.0)・[3.1.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.1.0)、[adapter-vercel 7.0.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fadapter-vercel%407.0.0)、[Superforms v3.0.0](https://github.com/ciscoheat/sveltekit-superforms/releases/tag/v3.0.0) -- `$lib` が `#lib` になった経緯: issue [#16182](https://github.com/sveltejs/kit/issues/16182)、PR [#16360](https://github.com/sveltejs/kit/pull/16360) ほか(survey.md の「Kit 3 が `$lib` を `#lib` に変えた理由」) -- 挙動の根拠にした Kit のコードと実測は、survey.md と plan.md に記載。 diff --git a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md b/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md deleted file mode 100644 index 1f8f857d2..000000000 --- a/docs/dev-notes/2026-10-03/sveltekit-3-adapter-vercel-7-upgrade/survey.md +++ /dev/null @@ -1,327 +0,0 @@ -# SvelteKit 3 / adapter-vercel 7 移行調査 - -## 概要 - -[移行計画](./plan.md) の根拠となる調査。Renovate の [#4105](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/4105)(Kit 2.70.3 → 3.0.0)と [#4104](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/4104)(adapter-vercel 6.3.4 → 7.0.0)は、それぞれ片方だけを更新していたため 2026-10-03 時点で Node 24 CI の Build が失敗し、現在は closed。2026-10-18 時点で open の依存 PR は [#4115](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/4115)(Superforms 3.0.0)のみ。 -調査は 2026-10-03 に始め、10-06・10-18 に追加した。基準は 2026-10-18 JST の `origin/staging`(`f02b7238`)。方法は manifest・配布コード・ソースの静的調査と、alias の置換をリポジトリのコピー上で試した検証。2026-10-19 には Kit 3 の spike で build・unit test・lint・check を実行した(→「Kit 3 の spike」)。その後、branch `#4192` で E2E の全件を流し、Kit 3 を仮に deploy した preview で手動確認も行った(2026-10-21、plan の「手動確認」)。要点は [summary.md](./summary.md) にまとめた。 -前回までの誤認と古くなった記述は、末尾の「前回調査からの訂正」にまとめた。 - -## 移行可否の判断 - -**外部の依存ブロッカーはなく、移行は可能。** - -| 判断軸 | 状況 | 判定 | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | -| Superforms の Kit 3 対応 | 3.0.0(2026-10-04)が Kit `^2.12.0 \|\| ^3.0.0` に対応。#4115 は 2026-10-18 に staging へマージ済み(`dc9f2471`)。マージ前の head `5833c1d0` の CI は build (24)・preview 成功で、手元の E2E も成功した。公開 API は不変。 | 解消 | -| 最低環境 | Kit 3.1.0 の要件(Node >=22.17・Vite ^8.0.12・Svelte ^5.57.1・TypeScript ^6.0.0・vite-plugin-svelte ^7.0.0)を、現行の Node 24.x・Vite 8.3.4・Svelte 5.57.2・TypeScript 6.0.3・plugin 7.3.1 が満たす。adapter 7.0.1 の peer は Kit `^3.0.0-next.0`。 | 解消 | -| 移行先の成熟度 | Kit は 3.0.0 の後に 3.0.1・3.1.0、adapter は 7.0.1 が出た。いずれも form・cookie・navigation の挙動には触れない(→「対象バージョン」)。 | 3.1.0 / 7.0.1 を採用 | -| Kit に依存する他パッケージ | 依存パッケージの配布コードを全件走査し、Kit の API(`$app/*`・`@sveltejs/kit`)を import するのは Kit・adapter・Superforms のみと確認した。svelte-meta-tags は Svelte のコンポーネントだけで、upstream は Kit 3.1 で開発している。super-sitemap は Kit の API を使わず、Kit 3 でも変わらないルートの規約(`src/routes/**/+page*.svelte`)だけに依存する(→「周辺ツールと依存の Kit 3 対応」)。 | 支障なし(E2E で確認) | -| 作業規模 | 変更ファイル数の大半は alias の機械置換(`$lib` 282ファイル・`$features` 144ファイル)で、スクリプトで再現できる。Kit 3 本体の実変更は設定・型 export・state・`resolve`・フォームの数〜十数ファイル。 | 中(段階実施が前提) | -| 回帰リスク | フォーム応答の HTTP status、別ページ宛 action、`$app/stores` の実行時例外、`dynamicCompileOptions` の移設。既存 E2E が守るのはログイン後 redirect・問題集編集/並べ替え・投票・ログアウト成功で、失敗系は未整備。 | 中〜高(E2E 追加が前提) | -| 戻し方 | 各 PR を通常の revert で戻せる。DB schema / migration は対象外。 | 低 | - -## Superforms 3.0.0(#4115) - -#4115 は 2.31.0 → 3.0.0 の依存・lockfile 更新(2ファイル)のみで、2026-10-18 に staging へマージされた(`dc9f2471`)。マージ前に preview deploy での5画面の確認と、手元の E2E(5本の spec・56件、すべて成功)を行った。Superforms 2.x は bugfix のみの維持になり、Kit 3 には v3 が必須(公式の upgrade paths)。 -head `5833c1d0`(2026-10-18 に staging `f02b7238` の上へ rebase)の lockfile の差分は、Superforms 本体と optional 依存の更新に限られる(ArkType 2.2.2 → 2.2.8、@ark/schema・@ark/util 0.56.1 → 0.56.7、arkregex 0.0.7 → 0.0.13、Vine 3.0.1 → 4.4.0・compiler 3.0.0 → 4.2.0 と新規の @poppinss/types 1.2.1、camelcase 8 → 9、Valibot 1.4.2 → 1.5.0・@valibot/to-json-schema 1.7.1 → 1.8.0、Joi 17.13.4 → 17.13.8、TypeBox 1.3.4 → 1.4.2)。2.31.0 の optional 依存が引いていた `effect@3.22.2` は消え、override(`effect@>=3.0.0 <3.20.0` → 3.22.3)の解決先 3.22.3 に統合された。devalue・esm-env と、Kit・Svelte・Vite の解決版は変わらない。 -この lockfile を pnpm 12.14.0 の `install --lockfile-only` で再解決しても書き換えは出ず、supply-chain policy の検査も通った。Kit 2 の CI 成功は、Kit 3 での成功を意味しない。 - -### リリースノートの全項目と影響 - -| 公式ノートの項目 | 本プロジェクトへの影響 | -| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| Kit `^2.12.0` と `^3.0.0`、Svelte `^5.56.4` に対応 | 利点。Kit 2.70.3・Svelte 5.57.2 のまま先行反映でき、Kit 3 移行時に Superforms を再更新しない。Svelte 4 と Kit 1 の peer は消えるが未使用。 | -| ArkType を beta から `^2.0.0` へ | 未使用(バリデータは Zod 4 の `zod4`)。対応不要。 | -| Vine `^4.0.0` に対応 | 未使用。対応不要。 | -| transport 型を Kit 2・3 の両方で通るよう修正 | `transport` オプション未指定。型は Superforms 内の独自 `Transport` に移り、Kit の型 import への依存が減る。アプリ側の変更なし。 | -| 「破壊的変更なし」 | 公開 API(`superValidate` / `superForm` / `zod4` / 各 export パス)は不変。ただし配布コードには次節の挙動変更がある。 | - -途中の prerelease は、3.0.0-next.0(2026-08-27)が Kit 3 対応、3.0.0-next.1(2026-09-30)が 2.31.0 と同じ `actionResult` の `transport` 追加。3.0.0 の GA 版に v3 固有の機能追加はない。 - -### 配布コード差分(リリースノートに載らない変更) - -2.31.0 と 3.0.0 の tarball を比較した。`exports` マップは不変(`imports` の `#lib` が Superforms の内部用に増えただけ)。 - -| 変更 | 内容 | 本プロジェクトへの影響 | -| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `$app/stores` → `$app/state` + `toStore` | `superForm` 内の page / navigating ストアを `$app/state` から `toStore` で再構築する。`$app/state` は Kit 2.12 以降で使える。 | アプリのコード変更は不要。`superForm` を使う5画面(login・signup・workbooks create / edit・account_transfer)で内部の page 同期が変わるため、Kit 2 上で確認が要る。Kit 3 では `$app/stores` が例外になるので、2.31.0 は使えない。 | -| `$app/environment` の `browser` → `esm-env` の `BROWSER` | 5ファイルで置換。`esm-env` は Svelte の依存で lockfile に 1.2.2 が既にある。 | 影響なし。SSR・Vitest(node) では false、client build では true。 | -| `/server` に `import '$app/server'` を追加 | `sveltekit-superforms/server` が server-only になる(client からの import は build エラー)。 | build は影響なし。`/server` を import する本アプリの6ファイルのうち、`auth_forms.ts` は2つの `+page.server.ts` からのみ import され、残りも `+page.server.ts` か単体テスト。単体テスト `schema.test.ts` は実体を import する。Kit 3 の Vitest でも解決できた(Kit 3 の spike で unit test 2726件が成功)。 | -| root entry が旧構文の `SuperDebug.svelte` を import(3.0.0 でも `export let` 13箇所) | root の `sveltekit-superforms` を import すると `SuperDebug.svelte` が巻き込まれる。本アプリでは `account_transfer/+page.svelte` と3つの `+page.server.ts` が該当。 | `dynamicCompileOptions`(node_modules の runes を未指定にする回避策)が引き続き必要。`account_transfer/+page.svelte` を `/client` からの import にすれば client bundle から外せるが、移行の必須条件ではない。 | -| `actionResult` が Kit の `json` ではなく `Response.json` を使用 | Kit 3 で `json` が非推奨になるのを先回りしたもの。 | 影響なし。 | -| `goto(url, { ...params })` → `goto(url)` | Kit 3 の `goto` 引数変更に整合。 | 影響なし。 | -| ActionResult に `location`、`onError` の error を `App.Error` 向けに正規化 | `customRequest` 経路と `onError` に入る。 | `customRequest` と `onError` は未使用。影響なし。 | -| 依存: devalue `^5.8.1` → `^5.9.4`、`esm-env ^1.2.2` を追加 | lockfile には devalue 5.9.4・esm-env 1.2.2 が既にある。 | 影響なし。Kit 経路の devalue 5.8.1 の宿題は解消しない。 | -| 非推奨が残るもの | `posted`(注記は「Will be removed in v3」だが 3.0.0 に存在)、`flashMessage` 統合、`SPA` の string / failStatus。 | `flashMessage` と `SPA` は未使用。`posted` は `src/lib/types/auth_forms.ts:44` の `AuthForm` 型と `src/lib/utils/auth_forms.ts:77,132` が保持する。3.0.0 では動くので今は変更せず、削除された時に auth_forms の型・util・テストを直す。 | - -`superForm` の呼び出しは5画面のみ。login / signup / account_transfer はオプションなし、workbooks create / edit は `{ dataType: 'json' }` だけ。`SuperDebug`・`flashMessage`・`transport`・`customRequest`・`validators`・`onUpdate*` の指定は src にない。`posted` 以外の非推奨 API は使っていない。 - -### 対応不要と判断したもの - -| 項目 | 根拠 | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 周辺依存 | Kit peer は `^2.12.0 \|\| ^3.0.0`、Svelte は `^5.56.4`、Zod は `^3.25.0 \|\| ^4.0.0`。現行の Kit 2.70.3・Svelte 5.57.2・Zod 4.6.7 はすべて適合。Kit 3 本体が要求する Svelte 5.57.1 も満たす。 | -| 公開 API / import | root・`/client`・`/server`・`/adapters` の exports は不変。既存の `superValidate(data, zod4(schema))`、`superForm(data.form)`、`superForm(initialData, { dataType: 'json' })` を保持する。 | -| 検証・初期値 | 使用中の `zod4` adapter・schemaShape・default options の配布コードは不変。`resetForm / taintedMessage` の既定値を移行のために変更しない。認証フォームの独自 schemaShape 型も即時変更は不要。 | -| 未使用 adapter | ArkType 安定版対応と Vine 4 対応は追加機能。アプリは Zod 4 のため validator の切り替えは推薦しない。 | - -### 新機能・採用推薦 - -v3.0.0 自体が追加する機能は Kit 3 対応と、使っていない validator の対応だけ。このアプリが得る利点は互換性で、新機能の採用は必要ない。 - -| 項目 | このプロジェクトでの用途 | 推薦 | -| ----------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | -| Kit 2・3 両対応 | #4115 を先に Kit 2 上へ入れ、フォームの基準を確認してから Kit 3 移行に進める。 | 採用(最大の利点)。 | -| `$app/state` ベースの内部実装 | Kit 3 の `$app/stores` スタブを避ける。アプリ側のコード変更は不要。 | v3 に上げるだけで得られる。 | -| ArkType 安定版・Vine 4 | Zod 4 を使うため不要。 | 見送り。 | -| `actionResult` の `transport`(2.31.0〜) | カスタム型を form action の応答に載せる場合。現状は未使用。 | 見送り。必要になったら導入する。 | -| flashMessage 統合 | 非推奨。 | 導入しない。 | -| `taintedMessage`(既存機能) | 問題集編集の未保存離脱警告。v3 の新機能ではない。 | 低。Kit 3 の `updated` による更新通知と併せて検討する場合のみ。移行のために既定値は変えない。 | - -## Kit 3.1.0 / adapter-vercel 7.0.1 - -### 対象バージョン - -| リリース | 内容 | 本プロジェクトへの影響 | -| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Kit 3.0.1(2026-10-06) | patch 1件: 生成型の相対 import が `nodenext` で動くよう修正(#17351)。 | 非該当。`tsconfig.json` は `moduleResolution: bundler`。 | -| Kit 3.1.0(2026-10-14) | minor 2件: CSP のハッシュ報告キーワード対応(#17398)、server tracing 有効時に `+server` ハンドラーの `sveltekit.endpoint` span を出力(#17325)。patch 6件: プロジェクトが `server` ディレクトリ配下にある場合に全モジュールを server-only 扱いする誤りの修正(#17376)、service worker 更新失敗時の復旧継続(#17440)、prerender の MIME 推定(#17381)、内容が同じ生成ファイルは書き換えない(#17310)、iframe の focus(#17297)、Storybook の警告抑制(#17373)。 | 破壊的変更なし。#17376 は 3.0.x の不具合(判定を絶対パス全体に対して行っていた)なので 3.1.0 以上を採用する根拠になる。#17310 は Vitest を `vite dev` と並行して動かしても、生成ファイルの再書き込みで dev server が再読込されなくなる開発体験の改善。CSP・service worker・Storybook は未使用で、`export const prerender` の指定もない。本アプリの `+server` は `(admin)/workbooks/order/+server.ts` の POST のみ。 | -| adapter-vercel 7.0.1(同日) | patch 1件: 解決済み runtime で検出した Bun の minor 版を保持(#17331)。 | 非該当(Node 24)。 | - -Kit 3.0.0 と 3.1.0 の tarball も比較した。`package.json` は版番号だけが変わり、dependencies・peer・engines は不変。差分のある `package.json` 以外の23ファイルは、docs URL のアンカー更新と型の追従が大半。実行時の変更は次に限られる。 - -- server-only 判定を、絶対パス全体ではなくプロジェクトルートからの相対パスに対して行う(`is_server_only_path`)。`node_modules` とルート外は対象外。 -- `+server` ハンドラーの `sveltekit.endpoint` span(server tracing 有効時のみ)と、CSP の `report-sha*` キーワード。 -- service worker 更新を try/catch で保護、iframe が focus を持たない時は自動 focus しない。 -- 生成ファイルの比較先を、プロセス内の記憶からディスク上のファイルへ変更。 -- adapter 向けの MIME 型収集で `prerendered` と `client` のディレクトリを個別に走査する。 - -`kit.alias` の検証と deprecated 警告(`core/config/options.js`)、`$lib` 削除エラーの条件(`vite/index.js` の `removed_modules`)、tsconfig の `paths` 生成、`$app/forms`・`$app/navigation`・`$app/state` の実装、cookie、redirect に差分はない。 - -### Kit 3 の spike(2026-10-19) - -staging(`e3d1145c`、PR A・PR A2 のマージ後)のコピーで `npx sv@1.1.1 migrate sveltekit-3` を Phase 1 の範囲(`svelte-config`・`lib-alias` と、自動で実行される前提タスク `package-json`・`tsconfig`)で実行し、Kit 3.1.0・adapter-vercel 7.0.1 に上げた。 - -- `sv migrate` のタスクは12個: `package-json`・`tsconfig`(前提)、`svelte-config`、`environment`、`paths`、`external-redirects`、`shallow-routing`、`params`、`imports`、`lib-alias`、`app-state`、`collect-migration-instructions`(自動化できない作業を `MIGRATION_TASKS.md` に書き出す)。前提タスクは `--tasks` で指定できず、自動で実行される。 -- 出力のうち、そのままでは使えなかったもの: - - `svelte.config.js` の `vitePlugin` を `sveltekit({ vitePlugin: { dynamicCompileOptions } })` に移した。Kit 3 は `config.vitePlugin` を削除しており、`svelte-kit sync` が `config_option_removed_vite_plugin`(「Pass `vite-plugin-svelte` options directly to the `sveltekit(...)` Vite plugin」)で失敗する。`dynamicCompileOptions` は `sveltekit({...})` の直下に置く。 - - Kit と adapter を `^3.0.0`・`^7.0.0` の範囲指定にした(本プロジェクトは版を固定する)。 - - 実在しない `src/lib/index.js` への bare `#lib` を `package.json#imports` に足した。 - - `lib-alias` が Markdown の中の `$lib` まで `#lib` に置換した。`architecture.md` の「組み込みの `$lib` を削除」が「組み込みの `#lib` を削除」になって文意が壊れ、過去の dev-notes 8件も書き換えた。 - - 既存のコメントを無関係な位置へ移し、`//` のコメントを `/* */` に書き換えた。 -- `tsconfig.json` は `extends: "$app/tsconfig"` になり、`allowJs`・`checkJs`・`isolatedModules`・`moduleResolution: bundler`・`skipLibCheck`・`verbatimModuleSyntax` などが削られた。生成された `node_modules/$app/tsconfig.json` がこれらをすべて提供し、`paths` は空なので、削っても結果は同じ。`types` は extends で置き換わるため `$app/types` を自分で含める。 -- 結果: build(`.vc-config.json` は `nodejs24.x`・`hnd1`・memory 3008・maxDuration 30、framework は sveltekit 3.1.0)・unit test(2726件)・lint は成功。check は 20 errors で、すべて Phase 2 の項目(`$app/environment`、`Handle`・`SubmitFunction` の型の移動、`$app/stores`、不要になった `@ts-expect-error`、`resolve()` の引数、汎用の `Actions` 型での `params`)。内訳は計画の Phase 2 に記載。警告は Kit 2 と同じ 30件。 -- `$app/environment` は Kit 3 の型からは消えている(`Cannot find module`)。build は通るが、Phase 2 で `$app/env` に置き換える必要がある。 -- lockfile からは Kit 2 の経路の `devalue@5.8.1` が消え、`cookie` は 2.0.1 になった。 - -Phase 2(branch `#4192`)の作業で分かった Kit 3.1.0 の挙動: - -- `resolve()` は、`/` で始まる引数を route ID として扱い、`(group)` を除いて `[param]` を埋める。`?`・`#` 以降はそのまま残る。それ以外は pathname として base を前に付ける。型が受け付けるのは、既知の route ID・pathname と、それに `` `?${string}` ``・`` `#${string}` `` を付けたテンプレート文字列だけで、`string` は渡せない。route ID に `/signup` のようなグループを含まない形は無く(`/(auth)/signup`)、pathname は先頭に `/` を付けない。 -- `goto()` は非推奨の `replaceState` を `replace` として受け付け続ける(`opts.replace ?? opts.replaceState`、開発時に警告)。 -- 実行時の `$app/environment` は `../env/index.js` を再 export するだけで残っている。型は消えているので、import は `$app/env` に変える必要がある。テストで `$app/env` を mock すると、`$app/environment` から import するコードにも効く。 -- `page.url` は型の上で読み取り専用で、`searchParams` は `ReadonlyURLSearchParams`。`URLSearchParams` や `URL` を受け取る関数には、そのまま渡せない。 -- route 専用でない汎用の `Actions` 型(`@sveltejs/kit`)では、`params` の値が `string | undefined` になる。 - -Phase 3 の E2E で確かめた挙動(Kit 2 との比較は staging のコピーで実行): - -- enhanced の form action が `fail(400)` を返すと、Kit 3 は HTTP 400 を返す(Kit 2 は 200)。JSON で返すかの判定は `accept` ヘッダーのままで、本文の ActionResult は `{ type: 'failure', status: 400 }`。 -- 別ページ宛の enhanced action(Header のログアウト)は、失敗しても action のページへ遷移する。未ログインでログアウトすると、Kit 3 は `/logout` の `load` の redirect を経て `/` に着く(Kit 2 は元のページに留まっていた)。 -- ゲストログイン(`AuthForm` が `fetch` で直接 POST する)は、成功時の応答が HTTP 200 の redirect で Kit 2 と同じ。失敗時は Kit 3 では HTTP 400 になり、`response.ok` が false なのでログイン画面に留まる。Kit 2 では 200 が返り、失敗してもホームへ遷移していた。 -- 本アプリは `handleError` を定義していない。Kit 3 の既定の実装は、予期しない例外(500)のスタックはサーバーに記録し、404・403 などの想定済みのエラーは記録しない。500 の `+error.svelte` には `Internal Error` が出る。 -- session cookie は Kit 3(cookie v2)でも `path: /`・`httpOnly`・`secure`・`sameSite: Lax` のまま。 - -### 破壊的変更の影響(要対応) - -| 項目 | 本プロジェクトでの到達箇所と必要な対応 | リスク | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| Kit / adapter の同時更新 | adapter 7 の Kit peer は `^3.0.0-next.0`。adapter 7 は `builder.generateServerInstance` を呼ぶため、片方だけの更新は Build が失敗する。 | 高 | -| Superforms | Kit 3.0.0 の `$app/stores` は export が残るが、`page / navigating / updated` の購読と `getStores` が例外を投げるスタブ。Superforms 2.31.0 のままだと build は通り、`superForm()` を使う画面が実行時に失敗する。3.0.0 で解消するが、Kit 3 上のフォーム回帰検証は必要。 | 高・回帰検証 | -| 設定の Vite 集約 | `svelte.config.js` がサポート外。`kit.adapter`・`preprocess`・`compilerOptions` を `sveltekit({...})` 直下へ移す。`vitePlugin.dynamicCompileOptions` も直下へ移し、node_modules に runes を強制しない現行挙動を保つ。 | 高 | -| `$lib` の削除と alias | 組み込みの `$lib` と `files.lib` が削除される。alias で維持はできるが、`kit.alias` は deprecated(→「alias から subpath imports への置換」)。 | 中 | -| tsconfig の生成先 | `tsconfig.json` の extends を `$app/tsconfig` へ変更する。既存の `src / prisma / *.config.ts` の include と型設定を保持し、generated file を手編集しない。 | 中 | -| 型 export の移動 | `src/hooks.server.ts` の `Handle` は `@sveltejs/kit/hooks` へ、管理タスク画面2件の `SubmitFunction` は `$app/forms` へ移す。依存ライブラリ側の型 import も確認する。`Page` 等は `$app/state`、navigation の型は `$app/navigation` に移る。 | 中 | -| `$app/stores` の削除 | `KanbanBoard.svelte` に `page / $page` が残る。Kit 3 では購読時に例外になる。初期化だけ読む値と URL 更新に追従させる値を区別し、filter の変更で未保存の並べ替え状態を消さないよう `$app/state` と Runes に移す。 | 中 | -| `goto`・shallow routing | `workbooks/+page.svelte` の `{ replaceState: true }` は `{ replace: true }` に変わる。Kanban の `replaceState` は deprecated で、`goto(..., { shallow: true, replace: true, state: page.state })` が後継。shallow navigation も hooks を発火する。`goto` は app 内 route に解決できない URL を reject する。 | 中 | -| `resolve` の pathname と route ID | leading `/` は route ID にのみ許される。問題集の保存 URL・Kanban filter URL・navbar 定数は pathname と query/hash を区別して変換する。`resolve('/workbooks/[slug]', { slug })` の route ID 指定は保持し、`/login` 等は route group を含む ID か leading slash のない pathname として扱う。既存の `@ts-expect-error` が不要になれば除去する。 | 中 | -| form action の HTTP status | 公式: 「Enhanced form action responses now use the HTTP status code passed to `fail(...)` instead of always returning `200`.」`fail(400/403/500)` の enhanced 応答が実 status になる。Superforms・独自 `use:enhance` の validation failure、権限拒否、redirect に影響する。 | 高 | -| 別ページ宛の enhanced action | 公式: 「Forms with `use:enhance` that specify an `action` on a different page will navigate to that page upon submission」。該当は `Header.svelte:135` のログアウトのみ(`/logout?/logout`)。成功時は `redirect(303, '/')` で挙動不変。未ログインでの失敗 `fail(401)` は、Kit 2 では画面に何も出なかったが、Kit 3 では `/logout` へ遷移し、`load` の redirect で `/` に着く。害は小さいが仕様変更。他の `use:enhance` は `?/xxx` か空の action で同一ページ宛。`TagForm` の `/tags?/update` は `use:enhance` なしの通常 POST で対象外。 | 中 | -| server-only 境界とエラー挙動 | `src/routes`・`static` 内を除く `server/` ディレクトリと、ファイル名に `server` セグメントを持つファイル(`server.ts`・`*.server.ts`)が保護対象になる。該当は `src/lib/server/` の2ファイル、`src/features/*/server/` の6ファイル、`hooks.server.ts` の計9ファイルで、Kit 3 で新たに保護されるのは features の6ファイル。全 `.svelte` とユニバーサル load(100ファイル)を起点に import グラフを辿り、これらに届く経路が無いことを静的に確認した(PR B の build でも、server-only の境界のエラーは出なかった)。`handleError` は expected error・rendering error も受け、`App.Error` は status を含む。本アプリは `handleError` を定義しておらず、Kit 3 の既定の実装は想定済みエラーをログに出さない。 | 低〜中 | -| 非推奨 env / response API | `$app/environment` を使う source 4ファイルとテストの mock 4ファイルは `$app/env` が後継。`$env/*` は `$app/env/private / public`、`json / text` は標準の Response API が後継だが、旧 env alias は残る。型・環境変数の参照タイミングが変わる変更は機械置換に混ぜない。 | 中 | - -### 対応不要と判断したもの - -| 確認項目 | 根拠・確認限界 | -| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 最低環境 | Node 24.x、TypeScript 6.0.3、Svelte 5.57.2、Vite 8.3.4、vite-plugin-svelte 7.3.1 は Kit 3.1.0 の要件を満たす。svelte-check 4.7.7 と Tailwind Vite 4.3.3 の peer も現行版に合う。 | -| adapter runtime | 現状は edge / Node 20 を指定しておらず、Node 24 の CI。[adapter 7 の runtime 実装](https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-vercel%407.0.0/packages/adapter-vercel/utils.js) は Node 22/24 を許容。memory 3008・maxDuration 30・region hnd1 は残せる。外部の Vercel project 設定までは repo だけで断定しない。 | -| cookie v2 / path default | 認証 cookie は ASCII の `auth_session`、set/delete は明示的に `path: '/'`。cookie 型の直接 import はない。 | -| CSRF / 外部 redirect | `csrf.checkOrigin` は未設定で、`trustedOrigins` の追加も不要。Kit 3 は Content-Type なしの cross-origin 変更リクエストを CSRF として拒否するが、本アプリの `fetch` による POST(`vote_grade.ts`・`AuthForm` のゲストログイン・`UpdatingDropdown`・`UpdatingModal`・`kanban.ts`)はすべて same-origin。ログイン・登録は既存の `isSameOriginRedirect` で destination を検証している。Kit 3 は現在の origin を参照して外部を判定するため、同一 origin の絶対 URL を一律に external 扱いしない。[redirect 検証実装](https://github.com/sveltejs/kit/blob/%40sveltejs/kit%403.0.0/packages/kit/src/exports/url.js) に沿って既存の guard を維持し、安易に `external: true` を追加しない。 | -| remote functions / matcher / service worker | remote functions、旧 `src/params/`、service worker、instrumentation、custom adapter / Node server は未使用。query の event access 制約、single-flight mutation、field.as、Builder の直接操作、polyfills 等はアプリ側には非該当。 | -| その他の削除 API・設定 | `preloadData / preloadCode` の直接使用、`base / assets / resolveRoute`、`SSRManifest`、`data-sveltekit-*="off"`、`noScroll / keepFocus`、旧 tracing flags、ISR / split runtime の明示設定、`csp` 設定は見つからない。`error(status, {...})` の独自 App.Error 拡張もない。 | -| 周辺の UI / SEO パッケージ | svelte-meta-tags 5.0.3・super-sitemap 1.0.12 とも対応不要と判断した(→「周辺ツールと依存の Kit 3 対応」)。 | -| 配備検知・同一 URL の再読込 | `version.pollInterval` は1時間が default。同一 URL のリンククリックで load が refresh される。直接の設定変更は不要だが、長時間開いたフォームと同一ページ再読込の UX は確認が要る。 | -| Major Changes 76項目の照合 | 上表と「要対応」で扱った項目以外は該当なし。`export const config`・外部への `redirect`・`error(status, {...})`・204 応答・`x-sveltekit-` で始まる query・`*.remote.*` ファイル・`$app/types`(`Pathname`・`Asset`)・`Page` やナビゲーション型の import・`delta`・`form.error`・`preloadData / preloadCode` の使用は無い。`invalidateAll` はコメントでの言及のみ。`ORIGIN` 環境変数の削除は adapter-node 向けで非該当。`goto` の呼び出しは `resolve()` の結果・`homeHref`・action の redirect 先(`(admin)/tasks/+page.svelte:47` の `result.location`)で、いずれも app 内の URL。`RouteId` が `+page`・`+server` を持つルートに限られる変更は、`+layout.svelte` の `navigating.from.route.id` の比較には影響しない(`resolve()` の型は PR B で扱う)。 | - -### 便利な機能・採用推薦 - -| 機能 | このプロジェクトでの用途 | 推薦 | -| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| feature の server-only 保護 | `src/features/*/server/` が自動的に client import を拒否し、現在の配置規約を framework が検査できる。 | 高。移行で自動的に得られる利点。build の境界エラーを抑制せず修正する。 | -| `goto` の shallow / `refreshAll` | Kanban の filter URL を history に反映しながら未保存の並べ替え状態を保ち、更新時も `page.state` を維持できる。 | 中。必須移行で挙動を検証し、必要な箇所だけ置き換える。一般の問題集 filter には server load が必要なので shallow を使わない。 | -| `updated.current` による更新通知 | 長時間開いた問題集編集・管理画面で新しい deployment を検知し、古い画面のまま操作し続けることを減らす。 | 中。移行後の別タスクで「保存してから再読込」を案内し、自動リロードによる入力消失を避ける。Vercel の skew protection が有効だと受動的な検知が漏れるため、採用前に設定を確認する(→「確認限界」)。 | -| `tracing.server` と instrumentation | memory 3008 を必要とする問題集や SSR の遅延・失敗の調査。adapter 7 は instrumentation 評価前に env を設定する。Kit 3.1.0 は `+server` の span も出す。 | 中。導入は別タスクで観測目的と exporter を決める。移行時には有効化しない。 | -| rendering error 境界・source maps | 画面の描画失敗を既存の `+error.svelte` と hooks で扱いやすくする。 | 中。まず既存のエラーページを検証し、production の map 対応は adapter の実際の出力で確認する。 | -| `$app/manifest`・typed params・remote functions | service worker、typed matcher、フォーム再設計の基盤。 | 低。現時点の必要性が薄いため本移行では追加しない。 | - -### ビルド時間とサイズ(2026-10-19) - -Kit 3 でビルド速度やサイズの恩恵があるかを測った。結論は、恩恵は無く、どちらもわずかに悪化するが実用上の差は無い。Kit 3 の移行の理由は、保守の継続(adapter 7 が Kit 3 を前提にする)と開発時の検査の強化(server-only の保護・`resolve()` の型など)で、性能ではない。 - -比較は、Kit 2 の `staging`(`e3d1145c`)のコピーと、Kit 3 の `#4192`(`444e4daf`)。同じマシンで `pnpm build` を交互に3回ずつ実行した。初回表示の JS は、それぞれの `vite preview` を Playwright で開き、`resourceType` が script の応答の本文を合計した(未ログイン)。gzip は level 9 で自前に計算した。 - -| 項目 | Kit 2 | Kit 3 | 差 | -| ------------------------------------------- | --------------------- | --------------------- | ---------------- | -| `pnpm build` の時間(3回) | 10.8・12.3・10.0 秒 | 14.0・15.3・12.9 秒 | 平均 +3 秒 | -| うち Vite が報告する時間(2段の合計) | 約 8.0 秒 | 約 5.8 秒 | −2 秒 | -| `/` の初回表示の JS | 23本・234.2 KB (gzip) | 25本・238.0 KB (gzip) | +3.8 KB(+1.6%) | -| `/problems` の初回表示の JS | 39本・263.4 KB (gzip) | 42本・267.3 KB (gzip) | +3.9 KB(+1.5%) | -| client JS の合計(`_app/immutable`) | 76本・396.8 KB (gzip) | 79本・398.9 KB (gzip) | +0.5% | -| framework の入口(`immutable/entry`) | 3.8 KB (gzip) | 2.4 KB (gzip) | −37% | -| Vercel の関数(`.vercel/output/functions`) | 478ファイル・35.82 MB | 473ファイル・35.77 MB | −0.1% | - -- build は、Vite の段は速くなったが、残りの処理(adapter による Vercel 向けの出力などと推測、内訳は未計測)が長くなり、全体で約3秒遅い。build 全体が十数秒なので CI への影響は小さい。 -- 入口のファイルは小さくなったが、chunk が2〜3本増えており、コードが別の chunk に移っただけで、ブラウザーが読む量は約4 KB 増えた。 -- 1台・3回の計測で、配信時の圧縮(Vercel 側)は再現していない。Kit 2 と Kit 3 の相対差として扱う。 - -依存の変化(`staging` と `#4192` の `pnpm-lock.yaml` の `packages` を比較): - -- lockfile 全体は 878 → 872 パッケージ。消えたのは9件(`@sveltejs/kit@2.70.3`・`@sveltejs/adapter-vercel@6.3.4`・`@sveltejs/acorn-typescript@1.0.10`・`@types/cookie@0.6.0`・`cookie@0.7.2`・`devalue@5.8.1`・`kleur@4.1.5`・`picomatch@4.0.4`・`set-cookie-parser@3.1.0`)、増えたのは3件(`@sveltejs/kit@3.1.0`・`@sveltejs/adapter-vercel@7.0.1`・`cookie@2.0.1`)。 -- devalue・picomatch・kleur は依存から消えたのではなく、重複していた古い版が消えた。Kit 2 の `devalue@^5.8.1` が他の依存の 5.9.4 と別に解決されていたが、Kit 3 は `^5.9.4` を求めるので 5.9.4 の1つにまとまった。picomatch(4.0.5・4.0.7・2.3.2)と kleur(3.0.3)も他の依存の版が残る。 -- Kit の直接の依存は 12 → 8。消えたのは `@sveltejs/acorn-typescript`・`acorn`・`@types/cookie`・`kleur`・`set-cookie-parser`、増えたのは `@rolldown/pluginutils`。`cookie` は `^0.6.0` → `^2.0.1`、`magic-string` は `^0.30.5` → `^1.2.3`。 -- 消えた版に既知の未修正の脆弱性があるかは調べていない。セキュリティ上の恩恵としては扱わない。 - -## 周辺ツールと依存の Kit 3 対応 - -`node_modules/.pnpm` の全パッケージの配布コード(`.js`・`.mjs`・`.cjs`・`.svelte`・`.ts`)を走査し、`$app/*` と `@sveltejs/kit` の import を探した。該当は Kit 本体・adapter-vercel・Superforms だけだった。そのうえで、Kit の規約や設定ファイルに依存しうるものを個別に確認した。 - -| 対象 | 確認結果 | 判定 | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | -| svelte-meta-tags 5.0.3 | peer は Svelte `^5.0.0` のみ。配布物は `MetaTags.svelte`・`JsonLd.svelte` と純粋な関数で、Kit への依存はない。upstream は開発環境を Kit 3.0(#2298、2026-10-04)・3.1(#2337)に上げており、5.0.3(2026-10-06)はその後の公開。 | 対応不要 | -| super-sitemap 1.0.12 | peer は Svelte `>=4 <6` のみで、Kit の API は使わない。ルートは Vite の `import.meta.glob('/src/routes/**/+page*.svelte')` で列挙し、応答は標準の `Response` で返す。Kit 3 はルートの規約を変えないので影響しない。upstream は Kit 3 対応を明言しておらず、Kit 3 の `$app/manifest` でルートを取る案が open の issue #59 にある。 | 対応不要(`e2e/sitemap.spec.ts` で確認) | -| super-sitemap 2.x | 最新は 2.0.6(2.0.0 は 2026-06-25)。root の export が無くなり、`super-sitemap/sveltekit` などフレームワーク別の export に変わる major 更新。Kit 3 移行の必須条件ではない。Renovate から 2.x の PR が来ていない理由は未確認。 | 本移行では更新しない | -| eslint-plugin-svelte 3.23.1 | recommended の `svelte/no-navigation-without-resolve` は Kit 1〜3 を条件に有効で、現行の Kit 2 でも既に有効。3.23.1 の変更は、Kit 3 でも有効のままにするためのもの。Kit 1〜2 限定の `no-goto-without-base`・`no-navigation-without-base` は recommended に入っていない。routes の判定は `svelte.config.js` が無ければ既定の `src/routes`。 | lint 結果は変わらない | -| svelte-eslint-parser 1.9.0 | 親ディレクトリの `svelte.config.js` を自動で読み、`compilerOptions.runes` を runes の判定に使う。Kit 2.70.3 のコピー上で `svelte.config.js` を外して ESLint を実行し、指摘が 0 件のまま変わらないことを確認した。 | 対応不要 | -| svelte-check 4.7.7 | 4.7.6 で Kit 3 の設定集約(Vite plugin への移設)に対応済み。 | 対応不要 | -| flowbite-svelte 1.33.1・@dnd-kit/svelte 0.5.1・@lucide/svelte 1.59.0 | Kit の API を import せず、peer は Svelte のみ。node_modules の runes は `dynamicCompileOptions` で従来どおり未指定のまま。 | 対応不要 | -| 既存 E2E の期待値 | `workbook_edit.spec.ts` は form action を直接 POST し、応答を ActionResult の JSON として検査する。JSON で返すかの判定は Kit 3.1 でも `accept` ヘッダーのままで、非所有者への 403 は `fail()` ではなく `error(FORBIDDEN)` 由来なので、Kit 2・3 とも 403 になる。`workbook_order.spec.ts` の 400 は `+server.ts` の応答で、form action の変更とは無関係。 | 期待値は変わらない | - -## alias から subpath imports への置換 - -### Kit 3 での alias の扱い - -- 組み込みの `$lib` と `files.lib` は削除される。残した `files.lib` はエラーになる。 -- `$lib` は alias で維持できる。Kit 3.0.0 のソースは、組み込みの `$lib` が無いときのエラーで「To keep using `$lib`, add `alias: { '$lib': 'src/lib' }` to your SvelteKit config.」と案内する。alias は Vite の解決と tsconfig の `paths` の生成に引き続き使われる。 -- ただし `kit.alias` 自体が deprecated で、build 時に「The `alias` option is deprecated, and will be removed in a future version of SvelteKit. Use subpath imports instead」という趣旨の警告が出る。`@`・`$features` も同じ扱い。削除時期は示されていない。 -- 3.0.0 → 3.1.0 でこれらの扱いに変化はない(→「対象バージョン」)。 - -### Kit 3 が `$lib` を `#lib` に変えた理由 - -Kit の issue・PR・docs から読み取った経緯。 - -- **独自の alias より標準の仕組み:** 発端は Rich Harris の issue [#16182](https://github.com/sveltejs/kit/issues/16182)「replace `$lib` with `#lib`」(2026-06-26)。`$lib` は Kit 独自の alias だが、`package.json` の `imports` は TypeScript を含めて広く対応された標準の仕組みなので、`src/lib` はそちらで指す。`$app/*` は Kit の提供物、`#lib/*` は利用者のコード、と役割も分かれる。[PR #16360](https://github.com/sveltejs/kit/pull/16360) が 2026-07-17 に version-3 へ入った。 -- **`$lib` の実害:** Vite を通らない場所(`vite.config.ts`・ESLint の設定・Node のスクリプト)では `$lib` が解決されず、Node が package 内部の名前として認める `#` 始まりの名前と混在しがちだった([#15234](https://github.com/sveltejs/kit/issues/15234))。monorepo では、ライブラリ側のコードの `$lib` が実行中のアプリの `src/lib` に解決される問題が2021年から残っていた([#2879](https://github.com/sveltejs/kit/issues/2879)、2026-08-03 に close)。`package.json#imports` は import したファイルに最も近い `package.json` を基準に解決するので、この問題が起きない。 -- **`#` である理由:** Node の規格で、`package.json` の `imports` の名前は `#` で始まる必要がある。Vite と TypeScript は設定なしで解決する([`$lib` の docs](https://github.com/sveltejs/kit/blob/main/documentation/docs/98-reference/26-%24lib.md))。`lib` という名前は `$lib`・`src/lib` との連続性によるもので、技術的な必然はない。 -- **`alias` の非推奨:** Rich Harris の issue [#16468](https://github.com/sveltejs/kit/issues/16468) が「People really should just be using subpath imports」「Using The Platform」として `alias` 設定を非推奨にした([PR #16470](https://github.com/sveltejs/kit/pull/16470)、警告のみで動作は継続)。alias を使い続けたい場合の代替として Vite の `resolve.tsconfigPaths` が挙げられている。 -- **拡張子が必須になった理由:** 当初は tsconfig の `paths` にも `#lib` を入れて拡張子なしで書けたが、Vite を通らない場合(esbuild でまとめる場合など)に「it'll break in weird ways」ため「More harm than good」として外された([PR #16736](https://github.com/sveltejs/kit/pull/16736))。その結果、Node の標準どおりに拡張子が必要になった。PR の本文は拡張子の必須化そのものの理由を明記しておらず、これは `paths` を外したことからの帰結と読んだ。Playwright のランナーで `#lib` が解決できないのも、この「Vite を通らない場合」に当たる。 - -単一アプリの本プロジェクトでは monorepo の問題は関係しない。得られる利点は、Vite・Vitest・TypeScript・tsx(seed)が同じ標準の仕組みで import を解決するようになることと、`$features` の置換(PR A2)を含めて `alias` 非推奨の流れに沿えること。 - -### 置換の対象 - -| | `$lib` → `#lib` | `$features`・`@` → `#features` | -| ------------ | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| ファイル | `src` 配下の282 | `src` 配下の144 | -| specifier | 676(`from` 651・`vi.mock` 25) | 286(`$features` 284 = `from` 277・動的 import 1(`src/app.d.ts` の型参照)・`vi.mock` 6、`@/features` 2) | -| 拡張子の内訳 | 拡張子なし573・`.svelte` 101・`.js` 2 | `$features` は拡張子なし235・`.svelte` 49 | -| e2e / prisma | 参照なし | 参照なし | -| docs の記述 | 5ファイル9行(`architecture.md`・`agent-rules/testing.md`・`testing-e2e.md`・`server-cache.md`・`prisma-db.md`) | 2ファイル5行(`architecture.md`・`agent-rules/testing-e2e.md`) | - -`@` の参照は2件とも `@/features/...` だった。alias を定義しているのは `svelte.config.js` だけで、tsconfig の `paths` はそこから生成される。 - -### 拡張子の規約 - -- TS モジュールは `.js`(`$lib/types/task` → `#lib/types/task.js`)。TypeScript と Vite が `.ts` の実体に対応付ける。 -- コンポーネントは `.svelte` のまま。 -- runes モジュール(実体が `.svelte.ts`)は `.svelte.js`(`$lib/stores/local_storage_helper.svelte` → `#lib/stores/local_storage_helper.svelte.js`)。末尾の `.svelte` を拡張子と見なすと型エラーが5件出たため、[codemod_subpath_imports.py](./codemod_subpath_imports.py) は実在ファイルで拡張子を判定する。 -- 既存の `.js` 指定(2件)はそのまま。 -- E2E の spec では `#lib` を使わず、相対 import のままにする。Playwright のランナーは `#lib/*.js` を `src/lib/*.js` に解決するが `.ts` の実体に対応付けないため、`Cannot find module` で失敗する(2026-10-18 に実測)。 -- `package.json#imports` には `#lib/*` と `#features/*` だけを置き、実在しない `src/lib/index.js` への bare `#lib` は追加しない。 - -### コピー上の検証(2026-10-18) - -Kit 2.70.3 上のリポジトリのコピー(`staging` の `f02b7238`、`node_modules` は本体へのシンボリックリンク)に codemod を適用した。`$features` 側は `$lib` の置換に続けて適用し、`svelte.config.js` から `@`・`$features`・`$features/*` の alias を外した。リポジトリ本体は変更していない。 - -| 確認 | `$lib` → `#lib` | `$features`・`@` → `#features` | -| ----------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| 置換 | 未解決 0・残り 0。再実行で変化なし。 | 未解決 0・残り 0。再実行で変化なし。 | -| `svelte-check`(`pnpm check` 相当) | 置換によるエラー 0。残る1件はコピーに生成物 `prisma/.fabbrica` が無いための差で、本体では出ない。 | 同左。 | -| 単体テスト | 97ファイル・2726テストがすべて成功(`vi.mock('#lib/...')` を含む)。 | 同左。 | -| `vite build` + adapter-vercel | 成功。 | 成功。 | -| `tsx`(`db:seed` の import 経路) | seed が推移的に import する `#lib/server/database.js`・`#lib/types/task.js` を解決できる。 | seed が import する features の4モジュール(`initializers`・`crud`・`password`・`fixtures/workbooks`)を解決できる。 | -| `oxlint` / `eslint` | 置換による指摘なし。oxlint の警告1件は本体にもある既存のもの。 | 同左。 | -| `prettier --check` | `src/lib/clients/aizu_online_judge/clients.ts` が import の行幅超過。 | 加えて `KanbanTabBar.svelte`・`workbooks/create/+page.server.ts` が行幅超過。 | - -E2E・dev server・CI は実行していない。 - -### 再流入の防止 - -- `$lib`: Kit 2 は `$lib` を組み込みで解決するため、置換後に新しいコードへ `$lib` が混ざっても Kit 3 に上げるまで気付かれない。`.oxlintrc.json` の `no-restricted-imports`(`{ "patterns": [{ "regex": "^\\$lib(/|$)" }] }`)で `.ts` の `$lib` import を検出でき、`#lib` は通ることを oxlint 1.88.0 で確認した。`.oxlintrc.json` は `.svelte` を対象外にしているので、`.svelte` は ESLint の同じ規則で検出し、こちらも `$lib` を検出して `#lib` を通すことを確認した。構文ごとの検出範囲も実測した。oxlint(`.ts`)は `import type`・`export … from`・`export * from`・動的 `import()` を検出し、ESLint(`.svelte`)は `import type` を検出するが動的 `import()` は検出しない。`vi.mock('$lib/...')` は関数呼び出しなので、どちらの規則でも検出されない。この穴は、alias の変更時と PR B の spike 前に grep で残りが 0件であることを確かめて補う。 -- `$features`・`@`: `svelte.config.js` から alias を外すと、残った `$features/...` の import は `svelte-check` で `Cannot find module` になる(コピーで確認)。リント規則は要らない。 -- 作業中の別 branch は、rebase 後に codemod を再実行すれば新規ファイルの alias も置換される。衝突するのは、置換した import 行かその隣接行を同じ branch が変更している箇所に限られる。 - -### 気付いた既存の問題 - -`src/features/tasks/components/contest-table/TaskTableBodyCell.svelte` が `@/features/votes/components/VotableGrade.svelte` を import している。tasks feature から votes feature への import で、AGENTS.md が禁じる feature 間 import にあたる。`@` alias で見えにくくなっていた。置換しても違反は残る。 - -## 参照資料 - -- Kit: [公式移行ガイド](https://svelte.dev/docs/kit/migrating-to-sveltekit-3)、[Kit 3.0.0 リリース](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.0.0)、[Kit 3.0.1](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.0.1)・[3.1.0](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%403.1.0) のリリース、npm の 3.0.0 / 3.1.0 tarball のソース。 -- adapter: [adapter 7.0.0 リリース](https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fadapter-vercel%407.0.0)、[adapter-vercel の CHANGELOG](https://github.com/sveltejs/kit/blob/main/packages/adapter-vercel/CHANGELOG.md)(7.0.1)。 -- 最低版と peer: [Kit の manifest](https://github.com/sveltejs/kit/blob/%40sveltejs/kit%403.0.0/packages/kit/package.json)・[adapter の manifest](https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-vercel%407.0.0/packages/adapter-vercel/package.json) と npm の registry を、リリースノートより優先した。リリース内の初期記述では Svelte 5.56.4 だが、最終的な Kit 3.0.0 の最低版は 5.57.1。 -- Superforms: [v3.0.0 リリース](https://github.com/ciscoheat/sveltekit-superforms/releases/tag/v3.0.0) と、npm の 2.31.0 / 3.0.0 tarball の差分。 -- `$lib` → `#lib` の経緯: Kit の issue [#16182](https://github.com/sveltejs/kit/issues/16182)・[#16468](https://github.com/sveltejs/kit/issues/16468)・[#15234](https://github.com/sveltejs/kit/issues/15234)・[#2879](https://github.com/sveltejs/kit/issues/2879)、PR [#16360](https://github.com/sveltejs/kit/pull/16360)・[#16470](https://github.com/sveltejs/kit/pull/16470)・[#16736](https://github.com/sveltejs/kit/pull/16736)、[`$lib` の docs](https://github.com/sveltejs/kit/blob/main/documentation/docs/98-reference/26-%24lib.md)。 -- 周辺パッケージ: インストール済みの配布物(svelte-meta-tags・super-sitemap・eslint-plugin-svelte・svelte-eslint-parser ほか)、super-sitemap の [issue #59](https://github.com/jasongitmail/super-sitemap/issues/59)、svelte-meta-tags の [#2298](https://github.com/oekazuma/svelte-meta-tags/pull/2298)・[#2337](https://github.com/oekazuma/svelte-meta-tags/pull/2337)、npm の registry。 - -## 確認限界 - -- Kit 3 上では、branch `#4192` で build・unit test・lint・check・E2E の全件を実行し、仮に deploy した preview で手動確認を行った(2026-10-21)。production への配備は未確認。 -- Kit 3.0.0 と 3.1.0 の比較は tarball のソースを読んだもの。Kit 3 上の `#lib`・`#features` の解決は、spike の build・unit test・check で確かめた。 -- alias 置換の検証は Kit 2.70.3・Vite 8.3.4・TypeScript 6.0.3・Vitest 5.0.5 のコピー上で行い、E2E・dev server・CI は含まない。 -- `npx sv migrate sveltekit-3` が `$lib` を `#lib` に自動置換するかは未確認。公式ガイドは自動変換の対象を明記していない。 -- `$lib` → `#lib` の経緯は、Kit の issue・PR の本文を WebFetch の要約経由で読んだもので、コメントでの議論は全部は読んでいない。 -- `kit.alias` の削除時期は公式に示されていない。「a future version」とだけある。 -- `svelte.dev` の移行ガイドはこの調査環境から接続できなかったため、同じ内容の `documentation/docs/60-appendix/35-migrating-to-sveltekit-3.md`(GitHub)を引用した。Kit 3.0.0 の Major Changes は release ページで76項目すべてを確認したが、各項目の詳細は移行ガイドとの照合までで、PR 単位の差分は読んでいない。 -- Superforms の release ページは adapter 一覧と release assets が読み込めなかった。adapter 側の変更は tarball の peer と配布コードの差分で補った。 -- `Header.svelte` 以外の enhanced action が同一ページ宛であることは、`action=` 属性の走査で確認した。実行時の遷移先までは確認していない。 -- `vite.config.ts` への移設後に `dynamicCompileOptions` が node_modules の旧構文コンポーネントへ効くか、`import '$app/server'` が Kit 3 の Vitest で解決できるかは、実行して初めて分かる。→ Kit 3 の spike で確かめた(`sveltekit({...})` の直下に置けば build が通り、unit test 2726件も成功した)。 -- server-only への到達は、型のみの import を除き、alias と相対パスを解決した静的な import グラフで確認した。Kit の build による判定ではない。 -- ESLint の `svelte.config.js` 有無の比較は Kit 2.70.3 のコピー上で行った。 -- super-sitemap の Kit 3 対応は upstream が明言していない。 -- 移行ガイドによると、Kit 3 の配備検知(`updated`)は Vercel の skew protection があると受動的な検知が漏れることがある。本プロジェクトの Vercel 側の設定は repo から分からない。 - -## 前回調査からの訂正 - -2026-10-03・10-06 時点の調査から、次の点を改めた。 - -| 項目 | 前回の記述 | 正しくは | 計画への反映 | -| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | -| `$app/stores` | Kit 3 で削除される。 | export は残り、購読と `getStores` で例外を投げるスタブ。Superforms 2.31.0 のまま Kit 3 に上げると、build は通りフォームのある5画面が実行時に落ちる。 | v3 必須の根拠を補強。失敗は build ではなく実行時に出ると明記した。 | -| 別ページ宛の enhanced action | 現状の named action はすべて同じページ宛。 | `Header.svelte` のログアウト(`../../logout?/logout`)は別ページ宛で、Kit 3 の挙動変更に該当する。 | ログアウト失敗の E2E を追加した。 | -| `$lib` → `#lib` | Kit 3 移行で必須の作業。 | alias で `$lib` を維持できる(Kit 3.0.0 のエラー文言が案内)。ただし `kit.alias` は deprecated。 | 必須ではないが、共同開発者の新機能追加の前に先行 PR で置換する方針にした。 | -| 移行の規模 | `$lib` 278ファイルを含む大規模な移行。 | ファイル数の大半は再現可能な機械置換で、Kit 3 本体の実変更は数〜十数ファイル。 | PR を4本に分けた。 | -| 移行先バージョン | Kit 3.0.0 / adapter 7.0.0。 | 3.0.1・3.1.0・7.0.1 が出ており、3.1.0 に server-only 判定の修正が入っている。 | 移行先を 3.1.0 / 7.0.1 に変更した。 | -| 周辺パッケージ | svelte-meta-tags・super-sitemap は Kit peer を宣言しないので、移行後に検証する(中身は未確認)。 | 配布コードを確認し、どちらも Kit の API に依存しないと分かった。super-sitemap はルートの規約に依存するが、Kit 3 で不変。svelte-meta-tags は upstream が Kit 3 で開発している。 | 対応不要と判断し、E2E での確認は残した。 | -| 破壊的変更の網羅 | Kit 3.0.0 の CHANGELOG の先頭約40項目で照合した。 | Major Changes は76項目。残りも照合し、周辺ツール(eslint-plugin-svelte・svelte-eslint-parser・svelte-check)と既存 E2E への影響も追加で確認した。 | 新たな要対応はなし。確認結果を調査に追記した。 | -| #4115 の状態 | head `b59c0870` の CI が成功。 | 2026-10-18 に今の staging の上へ rebase され、head `5833c1d0` でも build・preview が成功。 | 判定は不変。 | -| 周辺 PR | Vercel CLI #4103 と Lefthook #4102 は先に取り込める。 | 2026-10-03 のレビュー時点で反映済み。 | 計画から削除した。 | -| 現行版と件数 | Svelte 5.57.1・Zod 4.6.5・svelte-meta-tags 5.0.2・svelte-check 4.7.6、`$lib` 278・`$features` 142ファイル。 | Svelte 5.57.2・Zod 4.6.7・svelte-meta-tags 5.0.3・svelte-check 4.7.7、`$lib` 282・`$features` 144ファイル(その後の依存更新とコード追加による変化で、誤りではない)。 | 判定は不変。 |