ブログ

フロントエンドモジュール解決入門:importの背後にある魔法を理解する

モジュールパス解決メカニズムを深く理解し、OXC Resolverを使ってパスエイリアスやTypeScriptパスマッピングを処理し、独自のモジュール解決ツールを構築する方法を学びます。

LibDoc Team 2026年3月12日 OXC 連載 67 分で読める
#oxc #resolver #モジュール解決 #path alias #rust

フロントエンドモジュール解決入門:importの背後にある魔法を理解する

import { Button } from '@/components/Button' と書いたとき、ブラウザやバンドルツールがどうやって @/ がどこを指しているのかを知っているか考えたことはありますか?

これが**モジュール解決(Module Resolution)**の仕事です。今日は、OXC Resolver を学びます — 高性能なモジュールパス解決ツールです。

モジュール解決とは?

シンプルな質問

コードで次のように書いたとき:

import { something } from './utils/helper';

JavaScriptランタイムまたはバンドルツールはいくつかの質問に答える必要があります:

  1. ./utils/helper はファイルかディレクトリか?
  2. ディレクトリの場合、エントリーファイルは index.jsindex.ts か?
  3. ファイル拡張子は .js.ts.mjs のどれか?
  4. このパスに対応する実際のファイルはどこにあるか?

モジュールリゾルバーの仕事は、これらの質問に答えることです。

Node.js の解決ルール

Node.js には標準的なモジュール解決ルールがあります:

1. 相対パス

import './utils';      // 現在のディレクトリの utils
import '../lib/helper'; // 親ディレクトリの lib/helper

2. 絶対パス

import '/home/user/project/utils';  // ルートディレクトリから開始

3. モジュールパス(node_modules)

import 'lodash';       // node_modules/lodash を探す
import 'react/dom';    // node_modules/react/dom を探す

パスエイリアス

モダンなフロントエンドプロジェクトでは、インポートを簡素化するためにパスエイリアスがよく使われます:

// エイリアスなし
import { Button } from '../../../components/Button';
import { formatDate } from '../../../utils/date';

// エイリアス使用
import { Button } from '@/components/Button';
import { formatDate } from '@utils/date';

パスエイリアスはコードをより明確にしますが、リゾルバーのサポートも必要です。

なぜ解決速度が重要なのか?

大規模プロジェクトでは、モジュール解決は頻繁な操作です:

  • 開発サーバーの起動:すべてのエントリーファイルの依存関係を解決
  • ホットリロード:変更されたファイルの依存関係を再解決
  • 型チェック:TypeScriptはすべての型参照を解決する必要がある

プロジェクトに数千のモジュールがある場合、リゾルバーのパフォーマンスは開発体験に大きく影響します。

Resolverの役割

OXC Resolverは以下の機能を提供します:

  • 高速解決:Node.jsネイティブ解決より数倍高速
  • パスエイリアスサポート@/~ などのエイリアスを処理
  • TypeScriptパスマッピングtsconfig.jsonpaths 設定をサポート
  • 複数拡張子サポート.js.ts.jsx などを自動試行

クイックスタート

インストール

npm install @oxc-resolver

基本的な使用方法

import { Resolver } from "@oxc-resolver";

// リゾルバーインスタンスを作成
const resolver = new Resolver({
  // プロジェクトルートディレクトリ
  projectRoot: process.cwd(),
});

// 同期解決
const result = resolver.sync("./src", "./utils/helper");

console.log(result);
// {
//   path: "/home/user/project/src/utils/helper.js",
//   error: null
// }

// 非同期解決
const asyncResult = await resolver.async("./src", "./utils/helper");

解決結果

// 解決成功
{
  path: "/absolute/path/to/resolved/file.js",
  error: null
}

// 解決失敗
{
  path: null,
  error: "Cannot find module './nonexistent'"
}

実践:モジュールパスを解決

実例でResolverの使い方を学びましょう。

サンプルプロジェクト構造

my-project/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── index.ts
│   ├── utils/
│   │   ├── date.ts
│   │   └── string.ts
│   └── index.ts
├── tsconfig.json
└── package.json

基本的な解決

import { Resolver } from "@oxc-resolver";

const resolver = new Resolver({
  projectRoot: "/path/to/my-project",
});

// 相対パスを解決
resolver.sync("/path/to/my-project/src", "./components/Button");
// → /path/to/my-project/src/components/Button.tsx

// ディレクトリを解決(自動的にindexを探す)
resolver.sync("/path/to/my-project/src", "./components");
// → /path/to/my-project/src/components/index.ts

パスエイリアスの設定

const resolver = new Resolver({
  projectRoot: "/path/to/my-project",

  // エイリアスを設定
  alias: {
    "@": "./src",
    "@components": "./src/components",
    "@utils": "./src/utils",
  },
});

// エイリアスを使って解決
resolver.sync("/path/to/my-project", "@/components/Button");
// → /path/to/my-project/src/components/Button.tsx

resolver.sync("/path/to/my-project", "@utils/date");
// → /path/to/my-project/src/utils/date.ts

TypeScriptパスマッピングのサポート

tsconfig.json の設定を仮定:

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

Resolverは自動的に読み取れます:

const resolver = new Resolver({
  projectRoot: "/path/to/my-project",
  tsconfig: "./tsconfig.json",  // tsconfigのパスを指定
});

// tsconfigのpaths設定が自動的に適用される
resolver.sync("/path/to/my-project", "@components/Button");
// → /path/to/my-project/src/components/Button.tsx

設定オプション詳細

基本設定

const resolver = new Resolver({
  // プロジェクトルートディレクトリ
  projectRoot: process.cwd(),

  // モジュールディレクトリ(デフォルト node_modules)
  modules: ["node_modules"],

  // 自動試行する拡張子
  extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],

  // ディレクトリのデフォルトファイル
  mainFiles: ["index"],

  // エイリアス設定
  alias: {
    "@": "./src",
  },
});

エイリアス設定詳細

const resolver = new Resolver({
  alias: {
    // シンプルなマッピング
    "@": "./src",

    // 完全一致
    "lodash": "./vendor/lodash.js",

    // プレフィックス一致
    "react-native$": "./custom-react-native.js",

    // 配列形式(複数の候補)
    "@components": ["./src/components", "./src/shared/components"],
  },
});

条件付きエクスポートサポート

Node.js の exports フィールド:

// package.json
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.js"
    },
    "./utils": "./utils/index.js"
  }
}

Resolver設定:

const resolver = new Resolver({
  // 解決条件を指定
  conditionNames: ["import", "require", "default"],
});

モジュール解決戦略

const resolver = new Resolver({
  // Node.js スタイル(デフォルト)
  resolutionStrategy: "node",

  // Classic スタイル(TypeScript互換)
  resolutionStrategy: "classic",
});

実践プロジェクト:インポート分析ツールを構築

Resolverを使って実用的なツールを構築しましょう — プロジェクトのインポート依存関係を分析します。

分析スクリプトの作成

// analyze-imports.js
import { Resolver } from "@oxc-resolver";
import { parseSync } from "@oxc-parser";
import fs from "fs";
import path from "path";

// 設定
const config = {
  projectRoot: process.cwd(),
  entry: "./src/index.ts",
  alias: {
    "@": "./src",
  },
};

// リゾルバーを作成
const resolver = new Resolver({
  projectRoot: config.projectRoot,
  alias: config.alias,
  extensions: [".ts", ".tsx", ".js", ".jsx"],
});

// 解決済みファイルを保存(循環を避ける)
const visited = new Set();
const dependencies = new Map();

// 単一ファイルを分析
function analyzeFile(filePath) {
  if (visited.has(filePath)) return;
  visited.add(filePath);

  console.log(`📄 分析中: ${path.relative(config.projectRoot, filePath)}`);

  // ファイルを読み込んで解析
  const code = fs.readFileSync(filePath, "utf-8");
  const ast = parseSync(code, {
    lang: filePath.endsWith(".tsx") ? "tsx" : "ts",
  });

  const imports = [];

  // ASTを走査してインポート文を探す
  function traverse(node) {
    if (!node || typeof node !== "object") return;

    if (node.type === "ImportDeclaration") {
      imports.push(node.source.value);
    }

    if (node.type === "ExportNamedDeclaration" && node.source) {
      imports.push(node.source.value);
    }

    if (node.type === "ExportAllDeclaration" && node.source) {
      imports.push(node.source.value);
    }

    // 動的インポートを処理
    if (
      node.type === "CallExpression" &&
      node.callee.type === "Import"
    ) {
      if (node.arguments[0]?.type === "Literal") {
        imports.push(node.arguments[0].value);
      }
    }

    for (const key of Object.keys(node)) {
      const child = node[key];
      if (Array.isArray(child)) {
        child.forEach(traverse);
      } else if (child && typeof child === "object") {
        traverse(child);
      }
    }
  }

  traverse(ast);

  // 各インポートを解決
  const resolvedImports = [];
  for (const importPath of imports) {
    const dir = path.dirname(filePath);
    const result = resolver.sync(dir, importPath);

    resolvedImports.push({
      raw: importPath,
      resolved: result.path,
      error: result.error,
    });

    // ローカルファイルの場合、分析を続行
    if (
      result.path &&
      !result.path.includes("node_modules") &&
      !visited.has(result.path)
    ) {
      analyzeFile(result.path);
    }
  }

  dependencies.set(filePath, resolvedImports);
}

// 分析を開始
console.log("🚀 インポート分析を開始...\n");

const entryPath = path.resolve(config.projectRoot, config.entry);
analyzeFile(entryPath);

// 結果を出力
console.log("\n📊 分析結果:\n");

console.log("分析ファイル数:", visited.size);
console.log("\n依存グラフ:");

for (const [file, imports] of dependencies) {
  const relativeFile = path.relative(config.projectRoot, file);
  console.log(`\n  ${relativeFile}`);
  for (const imp of imports) {
    if (imp.error) {
      console.log(`    ❌ ${imp.raw} (${imp.error})`);
    } else if (imp.resolved?.includes("node_modules")) {
      console.log(`    📦 ${imp.raw} → node_modules`);
    } else {
      const resolvedRelative = path.relative(config.projectRoot, imp.resolved);
      console.log(`    ✅ ${imp.raw} → ${resolvedRelative}`);
    }
  }
}

// 統計
let totalImports = 0;
let externalImports = 0;
let localImports = 0;
let errors = 0;

for (const [, imports] of dependencies) {
  for (const imp of imports) {
    totalImports++;
    if (imp.error) errors++;
    else if (imp.resolved?.includes("node_modules")) externalImports++;
    else localImports++;
  }
}

console.log("\n📈 統計:");
console.log(`  総インポート数: ${totalImports}`);
console.log(`  ローカルインポート: ${localImports}`);
console.log(`  外部インポート (node_modules): ${externalImports}`);
console.log(`  エラー: ${errors}`);

分析の実行

node analyze-imports.js

出力例:

🚀 インポート分析を開始...

📄 分析中: src/index.ts
📄 分析中: src/components/Button.tsx
📄 分析中: src/utils/date.ts
📄 分析中: src/utils/string.ts

📊 分析結果:

分析ファイル数: 4

依存グラフ:

  src/index.ts
    ✅ ./components/Button → src/components/Button.tsx
    ✅ @/utils/date → src/utils/date.ts
    📦 react → node_modules

  src/components/Button.tsx
    📦 react → node_modules
    ✅ @/utils/string → src/utils/string.ts

  src/utils/date.ts
    📦 dayjs → node_modules

  src/utils/string.ts
    📦 lodash → node_modules

📈 統計:
  総インポート数: 7
  ローカルインポート: 3
  外部インポート (node_modules): 4
  エラー: 0

応用シーン

1. カスタムビルドツール

// シンプルなバンドラーのコアロジック
async function bundle(entryFile) {
  const resolver = new Resolver({ projectRoot: process.cwd() });
  const modules = new Map();

  async function loadModule(filePath, importer) {
    if (modules.has(filePath)) return;

    const result = resolver.sync(path.dirname(importer), filePath);
    if (result.error) throw new Error(result.error);

    const code = fs.readFileSync(result.path, "utf-8");
    modules.set(result.path, code);

    // インポートを解析し、再帰的に読み込む...
  }

  await loadModule(entryFile, process.cwd());
  return modules;
}

2. Monorepoプロジェクト

// monorepoのパッケージ参照を処理
const resolver = new Resolver({
  projectRoot: "/path/to/monorepo",
  alias: {
    "@myorg/core": "./packages/core/src",
    "@myorg/utils": "./packages/utils/src",
    "@myorg/ui": "./packages/ui/src",
  },
});

3. 循環依存の検出

function detectCircularDeps(entryPath) {
  const resolver = new Resolver({ projectRoot: process.cwd() });
  const visiting = new Set();
  const circular = [];

  function check(filePath, path) {
    if (visiting.has(filePath)) {
      circular.push([...path, filePath]);
      return;
    }
    visiting.add(filePath);

    // インポートを解決し、再帰的にチェック...

    visiting.delete(filePath);
  }

  check(entryPath, []);
  return circular;
}

パフォーマンスの優位性

比較テスト

// benchmark.js
import { Resolver as OxcResolver } from "@oxc-resolver";
import { createSyncFn } from "enhanced-resolve";

const code = fs.readFileSync("./large-project-imports.json", "utf-8");
const imports = JSON.parse(code); // 1000個のインポートパス

// OXC Resolver
const oxcResolver = new OxcResolver({ projectRoot: process.cwd() });
console.time("OXC Resolver");
for (const imp of imports) {
  oxcResolver.sync(process.cwd(), imp);
}
console.timeEnd("OXC Resolver");

// enhanced-resolve
const enhancedResolver = createSyncFn({
  /* config */
});
console.time("enhanced-resolve");
for (const imp of imports) {
  enhancedResolver(process.cwd(), imp);
}
console.timeEnd("enhanced-resolve");

テスト結果

リゾルバー1000回解決時間
enhanced-resolve450ms
OXC Resolver85ms

OXC Resolverは約5倍速い!

よくある質問

node_modulesのパッケージが解決できない?

設定が正しいか確認:

const resolver = new Resolver({
  projectRoot: process.cwd(),
  modules: ["node_modules"],  // modules設定が正しいか確認
});

TypeScriptパスマッピングが効かない?

tsconfig.json の設定を確認:

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

そしてResolverで指定:

const resolver = new Resolver({
  projectRoot: process.cwd(),
  tsconfig: "./tsconfig.json",
});

異なる拡張子の優先順位は?

const resolver = new Resolver({
  extensions: [".ts", ".tsx", ".js", ".jsx"],  // 順番に試行
});

パッケージのブラウザ/Node版をどう処理?

const resolver = new Resolver({
  conditionNames: ["browser", "import", "require", "default"],
  // ブラウザ優先、次にES Module、最後にCommonJS
});

まとめ

この記事ではOXC Resolverの核心的な使い方を紹介しました:

内容説明
モジュール解決インポートパスを実際のファイルパスに変換
パスエイリアス@/ などのエイリアスでインポートを簡素化
TypeScript統合tsconfig.json のpaths設定を読み取る
パフォーマンスの優位性従来のリゾルバーより5倍以上速い

Resolverの核心価値:モジュール解決を高速かつ正確に。

次のステップ

ResolverのAPIをさらに知りたいですか?**OXC日本語ドキュメント - Resolverセクション**をご覧ください。

次回の記事では、すべてのモジュールを組み合わせて、完全な高速フロントエンドワークフローを構築します!


💡 関連記事