← 記事一覧に戻る

Vitest で @/ パスエイリアスが解決できない・Cannot find module エラーの対処法【Vue / React / TypeScript】

tsconfig.json@/のパスエイリアスを設定してVite + Vue(またはReact)でアプリを作っていると、開発ビルドは問題なく動くのに、Vitestでテストを走らせると以下のエラーが出る。

Cannot find module '@/components/Button.vue' from 'src/__tests__/App.test.ts'

または

Error: Failed to resolve import "@/utils/helpers" from "src/components/App.vue"

これは非常によくある問題で、原因と対策のパターンが複数ある。順番に確認していこう。


なぜ起きるのか:Viteのpathエイリアスは「ビルドツール専用」

Viteのresolve.alias設定はViteのバンドル処理でのみ有効で、Node.js(Vitestの実行環境)には自動では引き継がれない。

開発サーバー/ビルド:  Vite が @/ → src/ に解決  ✅
テスト(Vitest):      Node.js が @/ を解決しようとする → 失敗  ❌

TypeScriptのtsconfig.jsonpaths設定も同様で、型チェックには使われるがランタイムのモジュール解決には影響しない。


対策1: vitest.config.ts に alias を追加する(最も確実)

vite.config.tsvitest.config.tsが分離している場合、Vitestの設定にもaliasを追加する必要がある。

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  test: {
    environment: 'jsdom',
    globals: true,
  },
})

vitest.config.tsが存在しない場合(vite.config.tsでVitestを設定している場合)は設定が引き継がれているはずなのに動かない場合がある。その場合は対策2へ。


対策2: vite.config.ts で Vitest を設定している場合の確認ポイント

vite.config.tstestフィールドでVitestを設定している場合、resolve.aliasは共有される。しかしありがちなミスが2つある。

ミス1: defineConfig のimport元が間違っている

// NG: vitestの型定義が含まれない → test フィールドが型エラーになる
import { defineConfig } from 'vite'

// OK: Vitestのテスト設定を型付きで書ける
import { defineConfig } from 'vitest/config'

viteからimportしたままだと、testフィールドの型定義が存在せず、Vitestが設定ファイルとして認識しないことがある。

ミス2: path.resolve の代わりに文字列を使っている

// NG: 相対パスのまま指定(作業ディレクトリによって解決先が変わる)
resolve: {
  alias: {
    '@': './src',  // これは動かないことがある
  },
},

// OK: 絶対パスで指定する
import path from 'path'

resolve: {
  alias: {
    '@': path.resolve(__dirname, './src'),
  },
},

対策3: ESModule プロジェクト("type": "module")での書き方

package.json"type": "module"が設定されている場合、__dirnameは使えない。代わりにimport.meta.urlを使う。

// vitest.config.ts(ESModule対応)
import { defineConfig } from 'vitest/config'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  test: {
    environment: 'jsdom',
  },
})

create-vueで生成したプロジェクトはデフォルトでこの形式が使われている。


対策4: create-vue / Vite テンプレートの標準設定を確認する

create-vuenpm create vite@latestで生成したプロジェクトは既にalias設定が入っているはずだが、vitest.config.tsを手で別途作成してしまうと既存の設定が上書きされることがある。

create-vueが生成するvite.config.tsの標準形:

// vite.config.ts(create-vue の生成例)
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitest/config'  // ← vitest/config からimport
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  test: {
    environment: 'jsdom',
    globals: true,
    include: ['src/**/*.{test,spec}.{js,ts}'],
  },
})

この設定ファイルが存在する状態で別途vitest.config.tsを作ると、resolve.aliasが引き継がれないので注意。


対策5: Jest を使っている場合(moduleNameMapper

VitestではなくJestを使っている場合、jest.config.tsmoduleNameMapperを追加する。

// jest.config.ts
import type { Config } from 'jest'

const config: Config = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    // 複数エイリアスがある場合も列挙できる
    '^~/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '^.+\.(ts|tsx)$': ['ts-jest', {
      tsconfig: './tsconfig.json',
    }],
  },
}

export default config

tsconfig.jsonpaths設定と対応させる:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "~/*": ["src/*"]
    }
  }
}

moduleNameMapperのキーは正規表現で、^@/(.*)$は「@/で始まる全パス」にマッチする。<rootDir>はJestが自動でプロジェクトルートに展開してくれる。


対策6: ts-node / tsx で直接実行している場合

テストではなくts-nodetsxでTypeScriptファイルを直接実行している場合も同様のエラーが出る。この場合はtsconfig-pathsを使う。

npm install -D tsconfig-paths
# ts-node の場合
ts-node -r tsconfig-paths/register src/script.ts

# tsx の場合(オプション不要、tsconfig.json の paths を自動認識)
tsx src/script.ts

package.jsonのscriptsに組み込む場合:

{
  "scripts": {
    "run:script": "ts-node -r tsconfig-paths/register src/script.ts"
  }
}

よくある追加の落とし穴

Vue の SFC(.vue ファイル)が解決できない

Vue のSFCをテストするには@vue/vue3-jest(Jest)または@vitejs/plugin-vue(Vitest)が必要。Vitestの場合:

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],  // ← VueのSFCをテストするために必要
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  test: {
    environment: 'jsdom',
  },
})

モノレポでサブパッケージのテストが動かない

モノレポ構成でサブパッケージにvitest.config.tsを置いている場合、__dirnameimport.meta.urlが各パッケージのディレクトリを基準にするため、ルートのsrc/を誤って指してしまうことがある。

// packages/ui/vitest.config.ts
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  resolve: {
    alias: {
      // NG: プロジェクトルートの src を指してしまう場合がある
      '@': fileURLToPath(new URL('../../src', import.meta.url)),

      // OK: このパッケージの src を正しく指す
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

デバッグ方法

解決されているパスを確認する

// vitest.config.ts に一時的に追加
import path from 'path'
const aliasPath = path.resolve(__dirname, './src')
console.log('[vitest.config] alias @ →', aliasPath)
// 出力例: [vitest.config] alias @ → /home/user/myapp/src

Vitest の verbose ログで確認

npx vitest --reporter=verbose 2>&1 | head -50

まとめ

状況 対策
vitest.config.tsが独立して存在する resolve.aliasを追加する
vite.config.tsでVitestを設定している defineConfigvitest/configからimport
'./src'のような相対パスを指定している path.resolve(__dirname, './src')に変更
ESModuleプロジェクト("type":"module" fileURLToPath(new URL('./src', import.meta.url))を使う
Jestを使っている moduleNameMapper'^@/(.*)$': '<rootDir>/src/$1'を追加
ts-nodeで直接実行している tsconfig-pathsを使う

最も多いのは「vitest.config.tsを新規作成したがvite.config.tsのalias設定が引き継がれていない」パターンだ。設定ファイルをvite.config.tsvitest.config.tsに分けるなら、どちらにもresolve.aliasを書く。一方に統一するならvite.config.tstestフィールドを追加してdefineConfigvitest/configからimportするのがシンプルでトラブルが少ない。