
Angular
イベント
該当するコンテンツが見つかりませんでした
マガジン

技術ブログ
はじめに こんにちは、セーフィーに新卒で入社し、サーバーサイドエンジニアをしている古谷です。 入社後、同期の坂上さんと一緒に VApp(Vulnerable Application) という、意図的に脆弱性を仕込んだ学習用Webアプリケーションを作りました。社内のセキュリティ学習を目的に始めたプロジェクトでしたが、作ってみると 「Webフレームワークを使っていても、脆弱な実装をしないように気をつけなければならない」 という、当たり前のようで意外と実感しにくいことを実装ベースで確かめられました。 この記事では、VAppを紹介しつつ、それを通して 自分の中でセキュリティとの向き合い方がど
こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 最近、 VoidZero から Vite+ がリリースされました。 Vite+ は Vite 8、Vitest、Oxlint、Oxfmt などを統合した「Web のための統合ツールチェーン」です。 駅メモ!は現在 Vue + Vite 7 + Vitest + ESLint(チーム独自ルール有り)+ Prettier + Stylelint で開発されており、Vite+ のツールはまだ導入していません。 AI で開発が高速化した現代、数十倍速いと謳う Vite+ のツールチェーンは気になります。 ただ、そもそも各ツールがどれくらい Vue 対応しているのか、どう設定すれば良いのかがわからなかったので、テンプレを使って確認することにしました。 Vite+ 経由の vp create vue と従来の pnpm create vue@latest で生成されるプロジェクト設定を比較し、Vue プロジェクト特有の注意点を整理します。 検証環境 プロジェクトの作成 Vite+ 経由 従来の create-vue 生成される設定ファイルの比較 Vite+ プロジェクトの構成 従来の create-vue プロジェクトの構成 共通: eslint.config.ts 共通: Lint 実行順 Linter 比較: Oxlint vs ESLint 検証用コンポーネント Oxlint の結果 ESLint の結果 CSS/Style Lint について 型チェックの違い テスト Formatter 比較: Oxfmt vs Prettier Vue SFC のフォーマット対応 全体比較表 まとめ Linter 型チェック Formatter CSS Lint 検証環境 vp v0.1.16 (Vite+) create-vue v3.22.2 (pnpm create vue@latest) Node.js v24.14.1 pnpm v10.33.0 プロジェクトの作成 Vite+ 経由 vp create vue vue-viteplus ◇ Which package manager would you like to use? pnpm ◇ pnpm@10.33.0 installed ◇ Which agents are you using? Claude Code ◇ Which editor are you using? VSCode ◇ Set up pre-commit hooks to run formatting, linting, and type checking with auto-fixes? Yes Generating project… Running: pnpm dlx create-vue ┌ Vue.js - The Progressive JavaScript Framework │ ◇ Project name (target directory): │ vue-viteplus │ ◇ Use TypeScript? │ Yes │ ◇ Select features to include in your project: │ Vitest (unit testing), Linter (error prevention), Prettier (code formatting) │ ◇ Select experimental features to include in your project: │ Replace Prettier with Oxfmt │ ◇ Skip all example code and start with a blank Vue project? │ No Scaffolding project in /Users/yanai/project/vue-viteplus... │ └ Done. ✔ Merged vue-viteplus/.oxlintrc.json into vue-viteplus/vite.config.ts ✔ Merged vue-viteplus/.oxfmtrc.json into vue-viteplus/vite.config.ts Wrote agent instructions to CLAUDE.md Rewrote imports in 4 files ✔ Merged staged config into vue-viteplus/vite.config.ts ◇ Dependencies installed ◇ Code formatted ◇ Scaffolded vue-viteplus 出力を見ると Running: pnpm dlx create-vue とあり、内部で create-vue を呼んでいることが分かります。create-vue でプロジェクトを生成した後に、Vite+ が以下の変換をかけています。 .oxlintrc.json → vite.config.ts の lint ブロックにマージ .oxfmtrc.json → vite.config.ts の fmt ブロックにマージ vite / vitest の import パスを vite-plus に書き換え pre-commit フック( vp staged )の設定を統合 つまり vp create vue は create-vue のラッパーで、生成物を Vite+ 向けに変換しているだけのようです。 従来の create-vue pnpm create vue@latest vue-create-vue ┌ Vue.js - The Progressive JavaScript Framework │ ◇ Use TypeScript? │ Yes │ ◇ Select features to include in your project: │ Vitest (unit testing), Linter (error prevention), Prettier (code formatting) │ ◇ Select experimental features to include in your project: │ none │ ◇ Skip all example code and start with a blank Vue project? │ No Scaffolding project in /Users/yanai/project/vue-create-vue... │ └ Done. こちらは従来通りのシンプルな Scaffold です。create-vue 側でも「Replace Prettier with Oxfmt」の選択肢が出ますが、今回は Oxfmt との比較のため Prettier を選びました。 生成される設定ファイルの比較 以降のコードブロックは主要部分の抜粋です。 Vite+ プロジェクトの構成 package.json { " scripts ": { " dev ": " vp dev ", " build ": " run-p type-check \" build-only {@} \" -- ", " build-only ": " vp build ", " type-check ": " vue-tsc --build ", " test:unit ": " vp test ", " lint ": " run-s lint:* ", " lint:oxlint ": " vp lint . --fix ", " lint:eslint ": " eslint . --fix --cache ", " format ": " vp fmt src/ " } , " devDependencies ": { " eslint ": " ^10.1.0 ", " eslint-plugin-vue ": " ~10.8.0 ", " eslint-plugin-oxlint ": " ~1.57.0 ", " eslint-config-prettier ": " ^10.1.8 ", " vite ": " catalog: ", " vite-plus ": " catalog: ", " vitest ": " catalog: " } } vite.config.ts: import { defineConfig } from "vite-plus" import vue from "@vitejs/plugin-vue" export default defineConfig( { staged : { "*" : "vp check --fix" , } , fmt : { semi : false , singleQuote : true , } , lint : { plugins : [ "eslint" , "typescript" , "unicorn" , "oxc" , "vue" , "vitest" ] , env : { browser : true } , categories : { correctness : "error" } , options : { typeAware : true , typeCheck : true } , } , plugins : [ vue() ] , } ) defineConfig を 'vite-plus' からインポートしていて、Vite の設定に加え lint (Oxlint)、 fmt (Oxfmt)、 staged (pre-commit フック)の設定が1つのファイルにまとまっています。 vite と vitest は、 pnpm-workspace.yaml の catalog: により @voidzero-dev のものに解決されています。 従来の create-vue プロジェクトの構成 package.json { " scripts ": { " dev ": " vite ", " build ": " run-p type-check \" build-only {@} \" -- ", " build-only ": " vite build ", " type-check ": " vue-tsc --build ", " test:unit ": " vitest ", " lint ": " run-s lint:* ", " lint:oxlint ": " oxlint . --fix ", " lint:eslint ": " eslint . --fix --cache ", " format ": " prettier --write --experimental-cli src/ " } , " devDependencies ": { " eslint ": " ^10.1.0 ", " eslint-plugin-vue ": " ~10.8.0 ", " eslint-plugin-oxlint ": " ~1.57.0 ", " eslint-config-prettier ": " ^10.1.8 ", " oxlint ": " ~1.57.0 ", " prettier ": " 3.8.1 ", " vite ": " ^8.0.3 ", " vitest ": " ^4.1.2 " } } .oxlintrc.json: { " plugins ": [ " eslint ", " typescript ", " unicorn ", " oxc ", " vue ", " vitest " ] , " env ": { " browser ": true } , " categories ": { " correctness ": " error " } } .prettierrc.json: { " $schema ": " https://json.schemastore.org/prettierrc ", " semi ": false , " singleQuote ": true , " printWidth ": 100 } 共通: eslint.config.ts 前述の通り vp create vue は内部で create-vue を実行しているため、eslint.config.ts は両プロジェクトで同一です。 import { defineConfigWithVueTs, vueTsConfigs, } from "@vue/eslint-config-typescript" import pluginVue from "eslint-plugin-vue" import pluginVitest from "@vitest/eslint-plugin" import pluginOxlint from "eslint-plugin-oxlint" import skipFormatting from "eslint-config-prettier/flat" export default defineConfigWithVueTs( { name : "app/files-to-lint" , files : [ "**/*.{vue,ts,mts,tsx}" ] } , ...pluginVue.configs[ "flat/essential" ], vueTsConfigs.recommended, { ...pluginVitest.configs.recommended, files : [ "src/**/__tests__/*" ] } , ...pluginOxlint.buildFromOxlintConfigFile( ".oxlintrc.json" ), skipFormatting ) ただし、Vite+ プロジェクトではこの eslint.config.ts に落とし穴があります。 Vite+ の公式ガイド では .oxlintrc.json の使用は推奨されておらず、 vite.config.ts の lint ブロックへ設定を集約する方針です。実際、 vp create vue で .oxlintrc.json は vite.config.ts へマージされた後に削除されています。 しかし eslint.config.ts の buildFromOxlintConfigFile(".oxlintrc.json") はそのまま残っています。存在しないファイルを参照すると eslint-plugin-oxlint: could not find oxlint config file: .oxlintrc.json と警告が出て空配列を返すため、ルール重複の無効化が効きません。 つまり、Oxlint と ESLint で同じ違反が重複報告される状態になります。 回避策は2つあります。 1つ目は vite.config.ts から lint ブロックを直接インポートする方法です。 eslint.config.ts は TypeScript ですから、 vite.config.ts の default export から .lint を取り出して buildFromOxlintConfig に渡せます。 // eslint.config.ts import viteConfig from './vite.config' // 変更前: ファイルが存在しないため機能しない ...pluginOxlint.buildFromOxlintConfigFile( ".oxlintrc.json" ), // 変更後: vite.config.ts の lint ブロックをそのまま渡す ...pluginOxlint.buildFromOxlintConfig(viteConfig.lint), 2つ目は vite.config.ts の lint ブロックを削除し、 .oxlintrc.json に設定を一本化する方法です。 vite-plus の issue によると、現状の実装では .oxlintrc.json 等の専用設定ファイルが優先され、 vite.config.ts はフォールバックとして使われます。 .oxlintrc.json があればそちらが読み込まれます。 { " plugins ": [ " eslint ", " typescript ", " unicorn ", " oxc ", " vue ", " vitest " ] , " env ": { " browser ": true } , " categories ": { " correctness ": " error " } , " options ": { " typeAware ": true , " typeCheck ": true } } eslint.config.ts の修正が不要で済みますが、Vite+ の「 vite.config.ts に集約する」方針とは外れます。 共通: Lint 実行順 2026年4月時点で、create-vue は Oxlint をデフォルトで同梱しています。前述の package.json にある通り、 pnpm lint ( run-s lint:* )で lint:oxlint → lint:eslint の順に直列実行されます。 create-vue 側では eslint-plugin-oxlint が .oxlintrc.json を読み取り、Oxlint と重複する ESLint ルールを自動で無効化してくれます。 # Vite+ vp run lint # → vp lint . --fix ... Oxlint(vp経由) # → eslint . --fix --cache ... ESLint(直接呼び出し) # create-vue pnpm lint # → oxlint . --fix ... Oxlint(直接呼び出し) # → eslint . --fix --cache ... ESLint(直接呼び出し) vp lint は Oxlint だけを実行する組み込みコマンドで、ESLint は Vite+ に統合されていません。 そのため、テンプレートでは ESLint を eslint コマンドで直接呼ぶ構成になっています。 Vite+ のタスクランナーを活用したい場合は、 vite.config.ts の run.tasks に定義を移行すると良さそうです。 run.tasks で定義したタスクはデフォルトでキャッシュが有効なため、入力ファイルに変更がなければ再実行がスキップされます。 なお、 run.tasks のタスク名は package.json の scripts と重複できないため、移行する場合は package.json 側の lint スクリプトを削除します。 // package.json の lint 関連スクリプトを run.tasks に移行する例 run: { tasks: { lint: { command: 'vp lint . --fix && eslint . --fix --cache' , input: [{ auto : true } , '!.eslintcache' ] , } , } , } , eslint の --cache を使うと .eslintcache が書き出され、 vp run がそれを入力の変更と見なしてタスクキャッシュがヒットしません。 input で '!.eslintcache' を指定し、キャッシュファイルを変更検知の対象外にすることで併用できます。 キャッシュ機能については ESLint ではなくタスクランナー側のもので十分かもしれませんが、 --cache の有無による差異は今回未検証です。 Linter 比較: Oxlint vs ESLint 検証用コンポーネント 検証用に、意図的に Lint 違反を仕込んだ Vue コンポーネントを用意しました。 < script setup lang = "ts" > import { ref } from "vue" // unused expression (correctness) const x = 1 x // prefer-as-const (typescript) let y = "hello" as "hello" const items = ref ([ { id : 1 , name : "Apple" } , { id : 2 , name : "Banana" } , ]) </ script > < template > <!-- v-for without :key --> < li v- for = "item in items" > {{ item.name }} </ li > <!-- v-if and v-for on same element --> < div v- for = "item in items" v-if= "item.id > 0" :key= "item.id" > {{ item.name }} </ div > </ template > < style scoped> .unused-class { color : redd; } </ style > Oxlint の結果 # Vite+ vp lint src/components/LintTest.vue x eslint(no-unused-expressions): Expected expression to be used , - [src/components/LintTest.vue: 6 : 1 ] 5 | const x = 1 6 | x : ^ `---- x typescript-eslint(prefer-as-const): Expected a ` const ` assertion instead of a literal type annotation. , - [src/components/LintTest.vue: 9 : 20 ] 8 | // prefer-as-const (typescript) 9 | let y = 'hello' as 'hello' : ^^^^^^^ ` ---- Found 0 warnings and 2 errors. Finished in 369ms on 1 file with 132 rules using 10 threads. # create-vue pnpm exec oxlint -c .oxlintrc.json src/components/LintTest.vue ...(同一の 2 件) Found 0 warnings and 2 errors. Finished in 24ms on 1 file with 116 rules using 10 threads. Oxlint は <script> 内の違反を検出しましたが、 <template> / <style> の問題はスルーされています。Oxlint は .vue ファイルの <script> ブロックしか Lint しないためです。 oxc の互換性ページ にもある通り、Vue/Svelte/Astro 等のフレームワークでは script ブロックのみが対象です。 vue プラグインを有効にしても、script ブロック内の Vue 関連ルール(ref の使い方など)しか動きません。 SFC テンプレートの Lint 対応は oxc#15761 で追跡されていますが、まだ実装されていません。 また、Oxlint には ESLint の JS プラグインを読み込む JS plugins 機能もありますが、eslint-plugin-vue は動きません。 JS plugins の 制限事項 に「Custom file formats and parsers (e.g. Svelte, Vue, Angular)」は未対応と明記されています。 eslint-plugin-vue はカスタムパーサー( vue-eslint-parser )で .vue ファイル全体をパースしてテンプレートの AST をルールに渡す仕組みのため、この制限に該当しています。 ルール数の差(132 vs 116)は、 typeAware: true で型情報を使ったチェック(floating promise の検出等)が追加されるためです。 なお、Vite+ テンプレートの typeCheck: true は Vue プロジェクトでは実質的に使えないようです。 vp lint src/ のようにディレクトリを指定すると .ts ファイルもチェック対象になります。 しかし、 .ts から .vue をインポートしている箇所で tsgo がモジュール解決に失敗し TS2307: Cannot find module エラーが出ます。 上の検証でファイルを直接指定しているのはこの問題を回避するためです。 ESLint の結果 # Vite+(eslint-plugin-oxlint が機能していない) vp exec eslint src/components/LintTest.vue src/components/LintTest.vue 6 : 1 error Expected an assignment or function call and instead saw an expression @typescript-eslint/no-unused-expressions 9 : 5 error 'y' is never reassigned. Use 'const' instead prefer-const 9 : 5 error 'y' is assigned a value but never used @typescript-eslint/no-unused-vars 9 : 20 error Expected a `const` instead of a literal type assertion @typescript-eslint/prefer-as-const 19 : 3 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key 22 : 30 error The 'items' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for ✖ 6 problems ( 6 errors, 0 warnings) # create-vue(eslint-plugin-oxlint が正常動作) pnpm exec eslint src/components/LintTest.vue src/components/LintTest.vue 9 : 5 error 'y' is never reassigned. Use 'const' instead prefer-const 9 : 5 error 'y' is assigned a value but never used @typescript-eslint/no-unused-vars 19 : 3 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key 22 : 30 error The 'items' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if' vue/no-use-v-if-with-v-for ✖ 4 problems ( 4 errors, 0 warnings) Vite+ 側は Oxlint で検出されている no-unused-expressions と prefer-as-const も ESLint から報告されて6件。前述の通りルール重複の無効化が効いていません。 create-vue 側は eslint-plugin-oxlint が正常動作し、Oxlint との重複ルールが ESLint 側で無効化されるため4件。 どちらも、 eslint-plugin-vue により <template> 内の Vue 固有の問題も検出されています。 CSS/Style Lint について 検証用コンポーネントの <style> に color: redd; というタイポを仕込みましたが、Oxlint でも ESLint でも引っかかりませんでした。 どちらのテンプレートも CSS の Lint は対象外のようです。 Vue 公式のツーリングガイドの Linting セクション でも案内されているのは eslint-plugin-vue による JavaScript/テンプレートの Lint だけで、CSS/Style の Lint には触れていません。 CSS の Lint が必要なら、これまで同様 Stylelint と Vue プラグインを別途入れることになりそうです。 型チェックの違い pnpm type-check はどちらも vue-tsc --build で同じです。 Vite+ 側は vite.config.ts に typeAware: true (型認識 Lint ルールの有効化)と typeCheck: true (tsgo 経由の型チェック同時実行)の設定があります。 ただし前述の通り tsgo は .vue を読めないため、 .vue の型チェックには引き続き vue-tsc が必要です。 テスト どちらも Vitest です。 Vite+ ではインポートパスが 'vitest' から 'vite-plus/test' に、コマンドが vitest から vp test に変わりますが、設定内容やテストの書き方は同じです。 Formatter 比較: Oxfmt vs Prettier Oxfmt は Prettier との出力互換を謳っており、JavaScript/TypeScript の conformance test を 100% パスしています。 Vue SFC のフォーマット対応 <template> と <style> もフォーマットできるのか気になったため、わざと崩した Vue ファイルで試しました。 <!-- フォーマット前 --> < template >< div class = "foo" >< p v-if= "true" > hello </ p ></ div ></ template > < style scoped>.foo{ color : red ; font-size : 16px ; display : flex ; justify-content : center }</ style > <!-- フォーマット後(Oxfmt / Prettier どちらも同一の結果) --> < template > < div class = "foo" >< p v-if= "true" > hello </ p ></ div > </ template > < style scoped> .foo { color : red ; font-size : 16px ; display : flex ; justify-content : center ; } </ style > Oxfmt でも Prettier でも <template> と <style> をフォーマットでき、出力結果は同一でした。乗り換えて問題なさそうです。 公式ドキュメント によると Prettier の約30倍の速度とのこと。小規模プロジェクトだと体感差はありませんが、大規模プロジェクトでは差が出そうです。 全体比較表 項目 Vite+ ( vp create vue ) create-vue ( pnpm create vue@latest ) Linter Oxlint ( vp lint ) + ESLint Oxlint + ESLint ESLint 設定 同一 (ただし eslint-plugin-oxlint の修正が必要) 同一 Vue template lint ESLint 経由で対応 ESLint 経由で対応 CSS lint なし なし typeCheck tsgo が .vue を読めず実質使えない なし テスト Vitest ( vp test ) Vitest Formatter Oxfmt ( vp fmt ) Prettier (Oxfmtも案内される) ビルド Vite ( vp build ) Vite ( vite build ) 設定の統合 vite.config.ts に集約 個別ファイル ( .oxlintrc.json , .prettierrc.json ) Pre-commit フック .vite-hooks/pre-commit → vp staged なし (要別途設定) まとめ vp create vue と pnpm create vue@latest で生成されるプロジェクトを比較した結果をまとめます。 Linter Oxlint は .vue の <script> ブロックしか Lint できない <template> や <style> は対象外 Vue template の Lint には引き続き ESLint(eslint-plugin-vue)が必要 これは Vite+ でも create-vue でも同じ create-vue は Oxlint をデフォルトで同梱している ESLint 連携の落とし穴 Vite+ テンプレートでは .oxlintrc.json がマージ後に削除されることへの対応が無く、ルール重複の無効化が壊れる vite.config.ts の lint ブロックを import するか、 .oxlintrc.json に一本化することで対処できる Lint 実行 どちらも Oxlint → ESLint の直列実行 型チェック typeCheck: true は Vue プロジェクトでは実質使えない tsgo が .vue をインポートした .ts ファイルで TS2307 エラーを出すため .vue の型チェックには引き続き vue-tsc が必要 Formatter Oxfmt は Prettier と同一の出力 <template> / <style> もフォーマットできる Prettier からの乗り換えで困ることはなさそう CSS Lint どちらのテンプレートも対象外 必要なら Stylelint を別途入れることになる Vite+ を選ぶメリットは Linter/Formatter 単体の差分よりも、 vite.config.ts への設定一元化と vp コマンドによる統合にありそうでした。 Vue 固有の Lint や型チェックについてはまだ ESLint + vue-tsc 頼りなため、Oxlint の Vue テンプレート対応や tsgo の .vue サポートが整えば、設定が楽になりそうです。 今回はテンプレートの設定比較だけでしたが、気になるのはやはり実際のプロジェクトでの速度差です。 次回は Vue ファイルが約2000個存在する駅メモ!のフロントエンドで、Lint/Format/ビルドがどれくらい速くなるか実測してみます。お楽しみに!
はじめに # ビジネスソリューション事業部の塚野です。 本記事は「Vitestと統合可能!StorybookでNext.js v16のコンポーネントテストを行う」の後編です。 前編では Storybook の導入や基本的な使い方についてご紹介しました。本記事では Next.js 固有の設定やモジュールモックなどについてまとめていきます。 next/router、next/navigationのモック # Next.js でページ遷移や URL の参照・更新に関わるパッケージとして next/router 、 next/navigation パッケージがあります。 next/router は主に Page Router で、 next/navigation は App Router で使用されます。Storybook(@storybook/nextjs-vite)では next/router パッケージはデフォルトでスタブされ、ルーターオブジェクトはActions タブにイベントを出力するモックに置き換えられます。 next/navigation も自動的にスタブされるため、 Story 上でも usePathname、 useSearchParams、 useRouter などを呼び出せます。 ただし、App Routerを使用する場合 Storybook 側に「App Router を使う」ことを明示する必要があります。Story 単位で設定できますが、プロジェクト全体が App Router 前提であれば .storybook/preview.ts に書いて全 Story に適用するのが手軽です。 .storybook/preview.ts import type { Preview } from '@storybook/nextjs-vite'; const preview: Preview = { ... parameters: { ... nextjs: { appDirectory: true, // ← App Router を利用する場合 true とする }, }, }; export default preview; ここで、 next/navigation パッケージを使用したコンポーネントとその Story を作成してみます。 コンポーネントのコードは読み飛ばしてかまいません。このコンポーネントでは input に入力した値を searchParams として現在の URL を書き換えます。 コンポーネント内では next/navigation パッケージの useRouter、 useSearchParams を利用しています。 NavigationDemo.tsx 'use client'; import Link from 'next/link'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useState } from 'react'; export function NavigationDemo() { const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); const [query, setQuery] = useState(searchParams.get('query') ?? ''); const [currentQuery, setCurrentQuery] = useState(searchParams.get('query') ?? ''); const apply = () => { const next = new URLSearchParams(searchParams.toString()); query ? next.set('query', query) : next.delete('query'); const queryString = next.toString(); router.replace(queryString ? `?${queryString}` : '?'); setCurrentQuery(query); }; return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} className="p-2 border border-black" /> <button onClick={apply} className="p-2 border border-black">Apply</button> <Link href={`${pathname}/link?query=${query}`} className="ml-2 underline"> go to Link </Link> <div>current path: {pathname}</div> <div>current query: {currentQuery || '(empty)'}</div> </div> ); }; このコンポーネントの Story は以下のように作成しました。 NavigationDemo.stories.tsx import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { getRouter } from '@storybook/nextjs-vite/navigation.mock'; //useRouter()のMock import { expect, userEvent, within } from 'storybook/test'; import { NavigationDemo } from './NavigationDemo'; const meta = { component: NavigationDemo, parameters: { nextjs: { appDirectory: true, navigation: { pathname: '/demo/navigation', //Story上でURL Pathの初期値を設定可能 query: { query: 'initial' }, //Story上でクエリパラメータの初期値を設定可能 }, }, }, } satisfies Meta<typeof NavigationDemo>; export default meta; type Story = StoryObj<typeof meta>; export const ReplaceIsCalled: Story = { async play({ canvasElement }) { const c = within(canvasElement); getRouter().replace.mockClear(); await userEvent.clear(await c.findByRole('textbox')); await userEvent.type(await c.findByRole('textbox'), 'hello'); await expect(c.getByRole('link', { name: 'go to Link' })).toHaveAttribute( 'href', '/demo/navigation/link?query=hello', ); await userEvent.click(await c.findByRole('button', { name: 'Apply' })); //useRouter().replace呼び出しのアサートに相当 await expect(getRouter().replace).toHaveBeenCalledWith('?query=hello'); }, }; ここで、Story ごとに pathname や query などを変えたい場合は、meta オブジェクトの parameters.nextjs.navigation を上書きします。これにより、URL に依存するコンポーネント(アクティブ状態、検索条件の表示など)を Story 単位で再現できます。 parameters.nextjs.navigation は初期状態の再現に便利ですが、「クリックで router.push() が呼ばれた」など、呼び出しの検証をしたいケースでは不足します。 そこで使うのが @storybook/nextjs-vite/navigation.mock です。これは next/navigation のモック実装に加えて、 useRouter() 相当のルーターオブジェクトを getRouter() で取り出せるため、push、 replace、 back などの呼び出しを テストとして assert できます。 このコンポーネントの Story 上で Apply ボタンを押下すると、Actions タブに入力したクエリパラメータが出力され、ルーターオブジェクトがモックできていることが分かります。 @storybook/nextjs-vite/navigation.mock 以外のビルトインモックに関してはこちらを参照してください。( Built-in mocked modules | Storybook docs ) --> Information ページ遷移に関わるパッケージとして他に next/link パッケージがあります。このパッケージに含まれる Link コンポーネントは pre-fetch 機能を備えた <a> タグを拡張したコンポーネントとしてよく使われます。この Link は内部で next/navigation 、 next/router のルーターオブジェクトを使用しているため、これらパッケージのモックと同時に Link コンポーネントもモックされるはずです。 しかし、Next.js(15以降〜)+ App Router 設定の Storybook では、Link コンポーネントをクリックしたときに Storybook の iframe が存在しないページへ遷移しようとするケースが報告されています。( storybookjs/storybook | GitHub ) 実際、NavigationDemo 内の「go to Link」ボタンクリックでページ遷移が発生してしまいます(Storybook v10.2.7 執筆時点)。 修正されるまで、Link コンポーネントは後述するモジュールモックを用いて Storybook 上では <a> タグにモックするなどの対策が必要でしょう。 React Server Componentの利用とServer functionsのモック # App Router では、 use client ディレクティブを付与して明示的に Client Component としない限り、デフォルトとして React Server Components(RSC)としてコンポーネントは扱われます。 特に、async function としている RSC については そのままでは Storybook で使用できません 。 Storybook v10.2.7(@storybook/nextjs-vite)現在、RSC 対応は Experimental 扱いのため、RSC を Storybook 上でレンダリングする場合は明示的に機能を有効化する設定が必要です。 具体的には .storybook/main.ts で features.experimentalRSC: true を指定します。 main.ts import type { StorybookConfig } from '@storybook/nextjs-vite'; const config: StorybookConfig = { framework: '@storybook/nextjs-vite', features: { experimentalRSC: true, //RSCを利用するにはexperimentalRSC: trueとする }, }; export default config; この設定で RSC を Storybook で動作させることはできます。ただしコンポーネント内で "server actions" ディレクティブを付けた、 DB 接続やファイルアクセスなどのサーバー関数を呼び出す場合これも Storybook 上では実行ができません。 Next.js でのベストプラクティスとして、 RSC 側ではデータフェッチ関数を直接記述するのではなく、呼び出すサーバー関数を別モジュールに切り出すことが知られています。 Storybook ではコンポーネント内でimportするモジュールをモックできます( Mocking modules | Storybook docs )。そこでサーバー関数を利用する場合、Storybook ではモジュールごとモックをしてしまい UI 確認用の戻り値に差し替える、という形で運用します。 また、Storybook では、コンポーネント単体の表示確認や振る舞いの検証が目的であるため、実際のサーバー依存処理は実行しないようにモック化した方がよいです。 Storybook v10.2 では、Vite/webpack 環境での推奨手段として sb.mock() による モジュールモックが用意されています。 モジュールモックの例として、以下のようなサーバー関数 getGreeting.ts を用意しました。 actions/getGreeting.ts "server actions" export async function getGreeting(name: string) { // 実環境ではDBやAPIなどにアクセスする想定 return `Hello, ${name}!`; } この関数をモックする場合、 .storybook/preview.ts にモックを登録します。各 Story 内ではモックの登録はできません。 これにより、Story 実行前に対象モジュールが置き換えられ、Story 単位で戻り値だけを制御できます。 .storybook/preview.ts import type { Preview } from '@storybook/nextjs-vite'; import { sb } from 'storybook/test'; // モック登録は preview.ts で行う sb.mock(import('../src/server/getGreeting.ts')); const preview: Preview = { parameters: { nextjs: { appDirectory: true }, }, }; export default preview; モック登録の注意点として以下があります。 Typescript を使用する場合(モックする関数が .ts の場合)、 sb.mock() 内で import() を用いて記述すること @ のような alias の使用は不可。必ず preview.ts からの相対パスで記述すること 拡張子まで含めてパスは記述すること この設定で getGreeting.ts は Storybook 上でモック化ができます。 ただしこの場合、Storybook 上では getGreeting.ts の機能は完全に失われます。もし、機能はそのままにスパイ関数化をしたい場合は sb.mock() の第2引数に { spy: true } を含めます。 sb.mock(import('../src/server/getGreeting.ts'), { spy: true }); それではこの関数を利用するコンポーネントと、その Story ファイルを作成し、Storybook 上でこのモック化した関数をどのように使用するのか見ていきます。 components/GreetingPanel.tsx import { getGreeting } from '@/actions/getGreeting'; type Props = { name: string }; export async function GreetingPanel({ name }: Props) { const message = await getGreeting(name); return ( <div> <h3>Greeting</h3> <p>{message}</p> </div> ); } 簡単な、 getGreeting でメッセージを取得しそれを表示するだけのコンポーネントです。 components/GreetingPanel.stories.tsx import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { expect, mocked } from 'storybook/test'; import { within } from 'storybook/test'; import { GreetingPanel } from './GreetingPanel'; import { getGreeting } from '../server/getGreeting'; const meta = { component: GreetingPanel, args: { name: 'Taro' }, } satisfies Meta<typeof GreetingPanel>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { // beforeEach()でモック化した関数の戻り値などの設定を行う async beforeEach() { mocked(getGreeting).mockResolvedValue('Hello from mocked function!'); }, async play({ canvasElement }) { const canvas = within(canvasElement); await expect(getGreeting).toHaveBeenCalledWith('Taro'); await expect(canvas.getByText('Hello from mocked function!')).toBeTruthy(); }, }; GreetingPanel の Story を作成しました。 Story 内でモック化した関数を利用する場合、 beforeEach() 内でモック化関数の戻り値などの設定を行います。 beforeEach() は各 Story で実行してもよいですし、 meta 内 beforeEach 要素に記述することですべての Story に適用が可能です。 mocked() の引数に preview.ts で登録したモックしたい関数を渡し、その戻り値に対して、モックした関数が非同期関数である場合は mockResolvedValue() で戻り値を設定します。 モックした関数が同期関数である場合は mockReturnValue(value) 、モック関数に対して任意の実装を行いたい場合は mockImplementation(fn) を利用してください。 まとめ # ここまで Vitest アドオンを利用したコンポーネントテストやモジュールモックなどを利用した Next.js コンポーネントのテストをご紹介しました。 Storybook ではさらにアドオンを使うことで Visual Regression Test(VRT)やアクセシビリティのテストなども実行可能です。 学習コストは若干感じるものの、CI パイプラインへの統合が可能なことや、デプロイすることでデザイナーとイメージアップに利用できるため、使いこなせればフロントエンド開発において欠かせないツールになると感じました。 Storybook は Next.js だけでなく Vue.js や Angular など幅広いフレームワークに対応しています。ご興味持たれた方は是非導入検討してみてはいかがでしょうか。












