多くの場合、プロジェクトは複雑なネストされたディレクトリ構造に発展します。その結果、インポート パスが長くなり、より混乱する可能性があり、コードの外観に悪影響を及ぼし、インポートされたコードがどこから来たのかを理解するのがより困難になる可能性があります。
パスエイリアスを使用すると、事前定義されたディレクトリに関連するインポートの定義を許可することで問題を解決できます。このアプローチは、インポート パスの理解に関する問題を解決するだけでなく、リファクタリング中のコード移動のプロセスを簡素化します。
// Without Aliases import { apiClient } from '../../../../shared/api'; import { ProductView } from '../../../../entities/product/components/ProductView'; import { addProductToCart } from '../../../add-to-cart/actions'; // With Aliases import { apiClient } from '#shared/api'; import { ProductView } from '#entities/product/components/ProductView'; import { addProductToCart } from '#features/add-to-cart/actions';
alias-hqやtsconfig-pathsなど、Node.js でパス エイリアスを構成するために使用できる複数のライブラリがあります。しかし、Node.js のドキュメントを調べているうちに、サードパーティのライブラリに依存せずにパス エイリアスを構成する方法を発見しました。
さらに、このアプローチにより、ビルド手順を必要とせずにエイリアスを使用できます。
この記事では、 Node.js サブパス インポートと、それを使用してパス エイリアスを構成する方法について説明します。また、フロントエンド エコシステムでのサポートについても検討します。
Node.js v12.19.0 以降、開発者はサブパス インポートを使用して、npm パッケージ内でパス エイリアスを宣言できます。これは、 package.json
ファイルのimports
フィールドを使用して実行できます。 npm でパッケージを公開する必要はありません。
任意のディレクトリにpackage.json
ファイルを作成するだけで十分です。したがって、この方法はプライベートプロジェクトにも適しています。
ここに興味深い事実があります: Node.js は、「 node.js の Bare Module Specifier Resolution 」と呼ばれる RFC を通じて、2020 年にimports
フィールドのサポートを導入しました。この RFC は、npm パッケージのエントリ ポイントの宣言を可能にするexports
フィールドで主に認識されていますが、 exports
とimports
フィールドは、似たような名前と構文を持っていても、まったく異なるタスクに対応しています。
パス エイリアスのネイティブ サポートには、理論的には次の利点があります。
私は自分のプロジェクトでパス エイリアスを設定しようとし、それらのステートメントを実際にテストしました。
例として、次のディレクトリ構造を持つプロジェクトを考えてみましょう。
my-awesome-project ├── src/ │ ├── entities/ │ │ └── product/ │ │ └── components/ │ │ └── ProductView.js │ ├── features/ │ │ └── add-to-cart/ │ │ └── actions/ │ │ └── index.js │ └── shared/ │ └── api/ │ └── index.js └── package.json
パス エイリアスを構成するには、ドキュメントで説明されているように、 package.json
に数行を追加します。たとえば、 src
ディレクトリに相対的なインポートを許可する場合は、次のimports
フィールドをpackage.json
に追加します。
{ "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
構成されたエイリアスを使用するには、インポートを次のように記述できます。
import { apiClient } from '#shared/api'; import { ProductView } from '#entities/product/components/ProductView'; import { addProductToCart } from '#features/add-to-cart/actions';
セットアップ フェーズから始めて、最初の制限に直面します。 imports
フィールドのエントリは#
記号で開始する必要があります。これにより、 @
などのパッケージ指定子と確実に区別されます。
この制限は、パス エイリアスがインポートでいつ使用されるか、およびエイリアス構成がどこにあるかを開発者がすばやく判断できるため、便利だと思います。
一般的に使用されるモジュールのパス エイリアスを追加するには、 imports
フィールドを次のように変更します。
{ "name": "my-awesome-project", "imports": { "#modules/*": "./path/to/modules/*", "#logger": "./src/shared/lib/logger.js", "#*": "./src/*" } }
「他のすべてはすぐに使える」というフレーズで記事を締めくくるのが理想的です。しかし、実際には、 imports
フィールドを使用する場合、いくつかの問題に直面する可能性があります。
CommonJS モジュールでパス エイリアスを使用する予定がある場合は、悪いニュースがあります。次のコードは機能しません。
const { apiClient } = require('#shared/api'); const { ProductView } = require('#entities/product/components/ProductView'); const { addProductToCart } = require('#features/add-to-cart/actions');
Node.js でパス エイリアスを使用する場合、ESM ワールドのモジュール解決ルールに従う必要があります。これは ES モジュールと CommonJS モジュールの両方に適用され、満たす必要がある 2 つの新しい要件が生じます。
ファイル拡張子を含むファイルへのフル パスを指定する必要があります。
ディレクトリへのパスを指定してindex.js
ファイルをインポートすることは許可されていません。代わりに、 index.js
ファイルへのフル パスを指定する必要があります。
Node.js がモジュールを正しく解決できるようにするには、インポートを次のように修正する必要があります。
const { apiClient } = require('#shared/api/index.js'); const { ProductView } = require('#entities/product/components/ProductView.js'); const { addProductToCart } = require('#features/add-to-cart/actions/index.js');
これらの制限により、多数の CommonJS モジュールを含むプロジェクトでimports
フィールドを構成するときに問題が発生する可能性があります。ただし、すでに ES モジュールを使用している場合、コードはすべての要件を満たしています。
さらに、バンドラーを使用してコードをビルドしている場合は、これらの制限を回避できます。これを行う方法については、以下で説明します。
型チェックのためにインポートされたモジュールを適切に解決するには、TypeScript がimports
フィールドをサポートする必要があります。この機能はバージョン 4.8.1 以降でサポートされていますが、上記の Node.js の制限が満たされている場合に限ります。
モジュールの解決にimports
フィールドを使用するには、 tsconfig.json
ファイルでいくつかのオプションを構成する必要があります。
{ "compilerOptions": { /* Specify what module code is generated. */ "module": "esnext", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "nodenext" } }
この構成により、 imports
フィールドは Node.js と同じように機能します。これは、モジュールのインポートにファイル拡張子を含めるのを忘れた場合、TypeScript はそれについて警告するエラーを生成することを意味します。
// OK import { apiClient } from '#shared/api/index.js'; // Error: Cannot find module '#src/shared/api/index' or its corresponding type declarations. import { apiClient } from '#shared/api/index'; // Error: Cannot find module '#src/shared/api' or its corresponding type declarations. import { apiClient } from '#shared/api'; // Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'? import { foo } from './relative';
私のプロジェクトのほとんどはバンドラーを使用してコードをビルドし、モジュールをインポートするときにファイル拡張子を追加することは決してないため、すべてのインポートを書き直したくありませんでした。この制限を回避するために、次のようにプロジェクトを構成する方法を見つけました。
{ "name": "my-awesome-project", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
この構成により、拡張子を指定する必要なく、モジュールをインポートする通常の方法が可能になります。これは、インポート パスがディレクトリを指している場合でも機能します。
// OK import { apiClient } from '#shared/api/index.js'; // OK import { apiClient } from '#shared/api/index'; // OK import { apiClient } from '#shared/api'; // Error: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './relative.js'? import { foo } from './relative';
相対パスを使用したインポートに関する問題が 1 つあります。この問題は、パス エイリアスとは関係ありません。 nodenext
モードを使用するようにモジュール解決を構成したため、TypeScript はエラーをスローします。
幸いなことに、最近のTypeScript 5.0 リリースで新しいモジュール解決モードが追加され、インポート内でフル パスを指定する必要がなくなりました。このモードを有効にするには、 tsconfig.json
ファイルでいくつかのオプションを構成する必要があります。
{ "compilerOptions": { /* Specify what module code is generated. */ "module": "esnext", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "bundler" } }
セットアップが完了すると、相対パスのインポートは通常どおり機能します。
// OK import { apiClient } from '#shared/api/index.js'; // OK import { apiClient } from '#shared/api/index'; // OK import { apiClient } from '#shared/api'; // OK import { foo } from './relative';
これで、インポート パスの記述方法に関する追加の制限なしに、 imports
フィールドを介してパス エイリアスを完全に利用できるようになりました。
tsc
コンパイラを使用してソース コードをビルドする場合、追加の構成が必要になる場合があります。 TypeScript の 1 つの制限は、 imports
フィールドを使用する場合、コードを CommonJS モジュール形式にビルドできないことです。
したがって、Node.js でコンパイルされたコードを実行するには、コードを ESM 形式でコンパイルし、 type
フィールドをpackage.json
に追加する必要があります。
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": "./src/*" } }
コードがbuild/
などの別のディレクトリにコンパイルされている場合、パス エイリアスがsrc/
などの元の場所を指しているため、モジュールが Node.js によって検出されない可能性があります。この問題を解決するために、 package.json
ファイルで条件付きインポート パスを使用できます。
これにより、ビルド済みのコードをsrc/
ディレクトリではなくbuild/
ディレクトリからインポートできます。
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
特定のインポート条件を使用するには、 --conditions
フラグを指定して Node.js を起動する必要があります。
node --conditions=production build/index.js
通常、コード バンドラーは、Node.js に組み込まれているものではなく、独自のモジュール解決の実装を使用します。したがって、 imports
フィールドのサポートを実装することが重要です。
プロジェクトで Webpack、Rollup、および Vite を使用してパス エイリアスをテストしましたが、その結果を共有する準備ができています。
以下は、バンドラーのテストに使用したパス エイリアスの構成です。インポート内のファイルへのフル パスを指定する必要がないように、TypeScript と同じトリックを使用しました。
{ "name": "my-awesome-project", "type": "module", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
Webpack は v5.0 からimports
フィールドをサポートします。パス エイリアスは、追加の構成なしで機能します。 TypeScript でテスト プロジェクトをビルドするために使用した Webpack 構成は次のとおりです。
const config = { mode: 'development', devtool: false, entry: './src/index.ts', module: { rules: [ { test: /\.tsx?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-typescript'], }, }, }, ], }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, }; export default config;
imports
フィールドのサポートは、Vite バージョン 4.2.0 で追加されました。ただし、バージョン 4.3.3 で重要なバグが修正されているため、少なくともこのバージョンを使用することをお勧めします。 Vite では、パス エイリアスは、 dev
とbuild
モードの両方で追加の構成を必要とせずに機能します。
したがって、完全に空の構成でテスト プロジェクトをビルドしました。
ロールアップは Vite 内で使用されますが、 imports
フィールドはそのままでは機能しません。有効にするには、 @rollup/plugin-node-resolve
プラグイン バージョン 11.1.0 以降をインストールする必要があります。構成例を次に示します。
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { babel } from '@rollup/plugin-babel'; export default [ { input: 'src/index.ts', output: { name: 'mylib', file: 'build.js', format: 'es', }, plugins: [ nodeResolve({ extensions: ['.ts', '.tsx', '.js', '.jsx'], }), babel({ presets: ['@babel/preset-typescript'], extensions: ['.ts', '.tsx', '.js', '.jsx'], }), ], }, ];
残念ながら、この構成では、パス エイリアスは Node.js の制限内でしか機能しません。つまり、拡張子を含む完全なファイル パスを指定する必要があります。ロールアップは配列の最初のパスのみを使用するため、 imports
フィールド内に配列を指定しても、この制限は回避されません。
Rollup プラグインを使用してこの問題を解決できると信じていますが、主に小さなライブラリに Rollup を使用しているため、試したことはありません。私の場合、プロジェクト全体でインポート パスを書き換える方が簡単でした。
テスト ランナーは、モジュール解決メカニズムに大きく依存する開発ツールの別のグループです。多くの場合、コード バンドラーと同様に、独自のモジュール解決の実装を使用します。その結果、 imports
フィールドが期待どおりに機能しない可能性があります。
幸いなことに、私がテストしたツールはうまく機能します。 Jest v29.5.0 と Vite v0.30.1 でパス エイリアスをテストしました。どちらの場合も、パス エイリアスは追加の設定や制限なしでシームレスに機能しました。バージョン v29.4.0 以降、Jest はimports
フィールドをサポートしています。
Vitest のサポート レベルは、Vite のバージョンのみに依存しており、少なくとも v4.2.0 である必要があります。
一般的なライブラリのimports
フィールドは、現在十分にサポートされています。しかし、コード エディターはどうでしょうか。パス エイリアスを使用するプロジェクトで、コード ナビゲーション、特に「定義へ移動」機能をテストしました。コード エディターでのこの機能のサポートには、いくつかの問題があることが判明しました。
VS Code に関しては、TypeScript のバージョンが重要です。 TypeScript 言語サーバーは、JavaScript および TypeScript コードの分析とナビゲートを担当します。
設定に応じて、VS Code は TypeScript の組み込みバージョンまたはプロジェクトにインストールされているバージョンのいずれかを使用します。
TypeScript v5.0.4 と組み合わせて、VS Code v1.77.3 でimports
フィールドのサポートをテストしました。
VS Code には、パス エイリアスに関する次の問題があります。
モジュール解決設定がnodenext
またはbundler
に設定されるまで、TypeScript はimports
フィールドを使用しません。したがって、VS Code で使用するには、プロジェクトでモジュールの解像度を指定する必要があります。
IntelliSense は現在、 imports
フィールドを使用したインポート パスの提案をサポートしていません。この問題には未解決の問題があります。
両方の問題を回避するために、 tsconfig.json
ファイルでパス エイリアス構成を複製できます。 TypeScript を使用していない場合は、 jsconfig.json
で同じことができます。
// tsconfig.json OR jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
バージョン 2021.3 (私は 2022.3.4 でテストしました) 以降、WebStorm はimports
フィールドをサポートしています。 WebStorm は独自のコード アナライザーを使用するため、この機能は TypeScript バージョンとは無関係に機能します。ただし、PhpStorm には、パス エイリアスのサポートに関して別の問題があります。
エディターは、パス エイリアスの使用に関して Node.js によって課される制限に厳密に従います。ファイル拡張子が明示的に指定されていない場合、コード ナビゲーションは機能しません。同じことが、 index.js
ファイルを使用してディレクトリをインポートする場合にも当てはまります。
WebStorm には、 imports
フィールド内でパスの配列を使用できないというバグがあります。この場合、コード ナビゲーションは完全に機能しなくなります。
{ "name": "my-awesome-project", // OK "imports": { "#*": "./src/*" }, // This breaks code navigation "imports": { "#*": ["./src/*", "./src/*.ts", "./src/*.tsx"] } }
幸いなことに、VS Code のすべての問題を解決する同じトリックを使用できます。具体的には、 tsconfig.json
またはjsconfig.json
ファイルでパス エイリアス構成を複製できます。これにより、制限なしでパス エイリアスを使用できます。
さまざまなプロジェクトでimports
フィールドを使用した実験と経験に基づいて、さまざまな種類のプロジェクトに最適なパス エイリアス構成を特定しました。
この構成は、追加のビルド手順を必要とせずにソース コードが Node.js で実行されるプロジェクトを対象としています。使用するには、次の手順に従います。
package.json
ファイルでimports
フィールドを構成します。この場合、非常に基本的な構成で十分です。
コード エディターでコード ナビゲーションを機能させるには、 jsconfig.json
ファイルでパス エイリアスを構成する必要があります。
// jsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": "./src/*" } }
この構成は、ソース コードが TypeScript で記述され、 tsc
コンパイラを使用してビルドされるプロジェクトに使用する必要があります。この構成では、以下を構成することが重要です。
package.json
ファイルのimports
フィールド。この場合、条件付きパス エイリアスを追加して、Node.js がコンパイル済みコードを正しく解決できるようにする必要があります。
imports
フィールドを使用する場合、TypeScript は ESM 形式のコードしかコンパイルできないため、 package.json
ファイルで ESM パッケージ形式を有効にする必要があります。
tsconfig.json
ファイルで、ESM モジュールの形式とmoduleResolution
を設定します。これにより、TypeScript はインポートで忘れられたファイル拡張子を提案できるようになります。ファイル拡張子が指定されていない場合、コードはコンパイル後に Node.js で実行されません。
コード エディターでコード ナビゲーションを修正するには、パス エイリアスをtsconfig.json
ファイルで繰り返す必要があります。
// tsconfig.json { "compilerOptions": { "module": "esnext", "moduleResolution": "nodenext", "baseUrl": "./", "paths": { "#*": ["./src/*"] }, "outDir": "./build" } } // package.json { "name": "my-awesome-project", "type": "module", "imports": { "#*": { "default": "./src/*", "production": "./build/*" } } }
この構成は、ソース コードがバンドルされているプロジェクトを対象としています。この場合、TypeScript は必要ありません。存在しない場合は、すべての設定をjsconfig.json
ファイルで設定できます。
この構成の主な機能は、インポートでのファイル拡張子の指定に関する Node.js の制限を回避できることです。
以下を構成することが重要です。
package.json
ファイルでimports
フィールドを構成します。この場合、各エイリアスにパスの配列を追加する必要があります。これにより、バンドラーはファイル拡張子を指定しなくても、インポートされたモジュールを見つけることができます。
コード エディターでコード ナビゲーションを修正するには、 tsconfig.json
またはjsconfig.json
ファイルでパス エイリアスを繰り返す必要があります。
// tsconfig.json { "compilerOptions": { "baseUrl": "./", "paths": { "#*": ["./src/*"] } } } // package.json { "name": "my-awesome-project", "imports": { "#*": [ "./src/*", "./src/*.ts", "./src/*.tsx", "./src/*.js", "./src/*.jsx", "./src/*/index.ts", "./src/*/index.tsx", "./src/*/index.js", "./src/*/index.jsx" ] } }
imports
フィールドを介してパス エイリアスを構成することには、サードパーティのライブラリを介して構成する場合と比較して、長所と短所の両方があります。このアプローチは一般的な開発ツールでサポートされていますが (2023 年 4 月現在)、制限もあります。
この方法には、次の利点があります。
package.json
ファイル) でパス エイリアスを構成することが促進されます。
ただし、開発ツールが進化するにつれて解消される一時的な欠点があります。
imports
フィールドのサポートに問題があります。これらの問題を回避するには、 jsconfig.json
ファイルを使用できます。ただし、これにより、2 つのファイルでパス エイリアス構成が重複することになります。
imports
フィールドが機能しない場合があります。たとえば、ロールアップには追加のプラグインのインストールが必要です。
imports
フィールドを使用すると、インポート パスに新しい制約が追加されます。これらの制約は ES モジュールの制約と同じですが、 imports
フィールドの使用を開始するのがより難しくなる可能性があります。
では、 imports
フィールドを使用してパス エイリアスを設定する価値はありますか?新しいプロジェクトでは、サードパーティ ライブラリの代わりにこの方法を使用する価値があると思います。
imports
フィールドは、従来の構成方法と比較して大きな利点を提供するため、今後数年間で多くの開発者にとってパス エイリアスを構成する標準的な方法になる可能性が十分にあります。
ただし、パス エイリアスが設定されたプロジェクトが既にある場合は、 imports
フィールドに切り替えても大きなメリットはありません。
この記事から何か新しいことを学んでいただければ幸いです。読んでくれてありがとう!
こちらにも掲載