import.metaはJavaScriptに追加予定の新機能です。すでにStage 3プロポーザル(内容がほぼ確定している状態)となってから2年近く経っている仕様で、Google Chrome, Firefox, Safariで実装済みです。また、node.jsではexperimentalな扱いで実装されています(import.metaが前提とするESモジュールがそもそもまだnode.jsではexperimentalという事情がありますが)。
Stage 3になってから長いこともありすでに広く知られているこのプロポーザルですが、まとまった日本語資料が見当たらなかったのでこの記事を用意してみました。では、さっそくimport.metaとは何かから解説していきます。ちなみに、import.metaのプロポーザルはこちらです。
import.metaとは何か
一言で言えば、import.metaはモジュールに固有の情報を持つメタプロパティです。
メタプロパティ
まずメタプロパティから解説します。これは、プロパティアクセスっぽく見えるけどそうではない構文のことを指します。実際、import.metaという構文はimportというオブジェクトのmetaというプロパティに見えますが、そうではありません。なぜなら、importは予約語であり変数名ではないからです。これはあくまでimport.metaというひとつの構文であり、これに外れる形(例えばimport.foobarなど)は構文エラーとなります。
import.metaは現在提案中のメタプロパティですが、実はすでに現在JavaScriptに採用されているメタプロパティが1つだけあります。それはnew.targetです。new.targetもあまり日本語記事がありませんが、こちらの記事が詳しいので読んでみるとよいでしょう。また、現在提案中(Stage 2)のfunction.sentというメタプロパティもあります。いずれも、予約語.プロパティ名という構文になっているのがメタプロパティの特徴です。
メタプロパティは、特定のコンテキストに固有の情報を表すのに使用される傾向があります。これは、仕様がグローバルに提供する何らかのAPIを使用するのに比べてコンテキスト依存であることが分かりやすいからだと思われます。そのため、メタプロパティは特定の状況でしか使用できないことがほとんどです。例えばnew.targetは関数がコンストラクタとして呼ばれたときに意味があるものですから、関数の中でしか使用できません。また、アロー関数はコンストラクタとして使用できないため、アロー関数の中でも使用できません。
モジュール
ではimport.metaが使用できるのはどこかというと、モジュールの中です。
モジュールについての詳細な説明は省きますが、これはJavaScriptプログラムを複数のファイルに分割するための仕組みです。この分割された各ファイルが一つ一つのモジュールとなります。モジュールexport文を用いて自身から何らかの値を他のモジュールに対して提供したり、import文を用いて他のモジュールが提供する値を読みこんだりできます。モジュールでない従来のJavaScriptプログラムはスクリプトと呼んで区別されます。import.metaはモジュールの中では使えますがスクリプトの中では使えないのです。
HTMLでは、script要素を用いてモジュールを読み込む場合はtype="module"という属性を付ける必要があります。例えば以下のようにすることで、module.jsがモジュール扱いで読みこまれます。
<script type="module" src="./module.js"></script>
一方、type="module"を付け忘れるとmodule.jsはスクリプト扱いで読みこまれます。スクリプト扱いの場合はmodule.jsの中でimport文やexport文、そしてimport.metaは使えません。
モジュールの動作例はこんな感じです。
/// module.js
import { fooFunction } from './foo.js';
fooFunction(123);
/// foo.js
export function fooFunction(arg) {
console.log(arg * 10);
}
上記のHTMLでmodule.jsが読みこまれると、module.jsがimport文でさらにfoo.jsを読み込んで、結果としてコンソールに1230が表示されることになります。
以上がモジュールの簡単な復習でした。
import.metaの中身
では、いよいよ本題に入りましょう。import.metaが持つモジュールに固有の情報とは何でしょうか。実を言えば、JavaScriptの仕様ではimport.metaの中身は決まっていません(オブジェクトであるということは決まっています)。
そもそも、モジュールという概念自体、定義しているのはJavaScriptの仕様であるもののその具体的な仕組みは実行環境に委ねられています。例えばnode.jsの場合は、import './foo.js'と書いた場合はファイルシステムからfoo.jsが読みこまれるでしょう。一方Webの場合は、import './foo.js'はfoo.jsへのHTTPリクエストを発生させるでしょう。このように、import文で指定された読み込み先モジュールが具体的にどのように解決されるかということは環境に依存します。
ですから、「モジュール」というものの実体が環境によって異なるために、import.metaが提供すべき「モジュールに固有の情報」というのも環境によって異なるのです。
ここまで抽象的な話が続いてきたので、ここからは具体的にimport.metaを使ってどんな情報が得られるのかを見ていきます。
Webにおけるimport.meta
先ほども述べたように、import.metaによって提供される情報というのは環境ごとに異なります。まずは、Web(すなわちブラウザ上)におけるimport.metaについて解説します。
Webにおけるimport.metaの挙動はHTML仕様書において定められています。それによれば、import.meta(によって得られるオブジェクト)はurlというプロパティをひとつだけ持っています。これは、そのモジュールのURLです。次の例で考えてみます。
console.log(import.meta.url);
<script type="module" src="./module.mjs"></script>
このHTMLファイルがhttp://localhost:8001/index.htmlでアクセスできるとしましょう。ここからmodule.mjsを読み込んだ場合、コンソールに表示されるのは"http://localhost:8001/module.mjs"です。このように、モジュール内でimport.meta.urlを取得した場合はそのモジュールのURLが取得できるのです。これは特に、モジュールからの相対パスを絶対URLに変換したい場合に有用です。また、Webでは必然的にひとつのURLがひとつのモジュールに対応しているという事実は覚えておく価値があるかもしれません。
Webにおけるimport.metaの拡張案
現在のところ仕様で定められているimport.metaのプロパティはurlだけですが、もうひとつscriptElementというプロパティを入れようという議論がされています。ところが、これはさまざまな理由から難航しています。かなりうまく作らないと使い物にならなそうだということが分かってきたからです。
import.meta.scriptElementというのは、そのモジュールが読みこまれるきっかけとなったscript要素を得ることができるプロパティです(名前は変わるかもしれません)。上の例を少しいじってみましょう。
console.log(import.meta.scriptElement.id); // "abc"が表示される
<script id="abc" type="module" src="./module.mjs"></script>
index.htmlはscript要素を用いてmodule.mjsを読み込みます。そうして実行されたmodule.mjsがimport.meta.scriptElementを見ると、index.html内のscript要素が得られるというわけです。
なんとなく便利そうに見えますが、すでに問題が見えています。index.htmlを次のようにすると何が表示されるでしょうか。
<script id="abc" type="module" src="./module.mjs"></script>
<script id="def" type="module" src="./module.mjs"></script>
実は、この場合は"abc"だけが表示されます。同じモジュールを2回読み込んでも2回実行されることはないからです。
これだけで使いやすさが半減していますね。モジュールを複数回実行できないという根本的な制限に加えて、わざわざimport.meta.scriptElementを使うということは「自分の読み込み元」を詳細に(単にdocumentにアクセスするのでは不足するくらいに)知りたい場面のはずなのに、読み込み先をひとつしか取れないというのは問題です。
他にも、script要素がshadowRootの中にある場合などの問題が議論されていたようですが、とにかく難航しており標準化には至っていません。
既に仕様化されているurl、そして議論されているscriptElementくらいしか現状では見当たりません。もし他にこんな情報もimport.metaに必要だろうと思うものがあれば提案してみるともしかしたら採用されるかもしれませんね。
node.jsにおけるimport.meta
Webとは別のJavaScript実行環境として有力なのがnode.jsです。node.jsも既にimport.metaを実装しています。node.jsのimport.metaにどんなプロパティがあるのかについては特に仕様化されていません。node.jsが自分で欲しい情報を好き勝手に実装すればいいだけです。議論したい場合、場所はGitHubのissueになります。
結論から言ってしまえば、node.jsに現在実装されているのはimport.meta.urlだけです。Webと同じですね。
node.jsにおいてはES Modules以前からCommonjsによるモジュールシステム(requireとかのやつですね)が広く利用されており、自身のファイル名の取得に関しては__dirnameや__filenameが(モジュールに対するローカル)変数として提供されるという独自仕様により行ってきました。node.jsにおいてはこれが結構便利で利用される場面も多かったのですが、JavaScriptの標準に準拠したモジュールを実装するにあたってこれらの独自仕様は排除される(スクリプトの中では依然使用可能だがモジュールの中では使用不可になる)こととなり、その代替としてimport.metaが必要となりました。
では、import.meta.urlの例を見ましょう。
console.log(import.meta.url);
node.jsで動作を確認するには次のようにします。
node --experimental-modules module.mjs
結果はこんな感じになります。
file:///tmp/test-module/module.mjs
ポイントは、結果が単なるパスではなくfile:スキームを持つURLとして得られることです。なんとなくWebの場合と一貫性があっていいですね。ちなみに、import文でもこのようなfile:スキームがサポートされるようです。
node.jsにおいてもimport.meta.url以外のプロパティの議論はあまり活発ではないようです(全く無いわけではないようですが)。そもそもまだnode.jsにおけるモジュール自体がexperimentalということもあり、先にそちらに注力するのでしょう。
まとめ
この記事ではimport.metaというプロポーザルを紹介しました。将来的な拡張は考えられますが現時点ではimport.meta.urlのみしかWeb・node.jsともに存在しないようです。
今後モジュールの利用が本格化するにつれてimport.metaの利用は避けて通れなくなるでしょう。今後起こるかもしれないimport.metaへの拡張に目を光らせておくのも悪くはありません。
尤も、生のモジュールなど使わずにwebpackでモジュールたちを1つのスクリプトにまとめる人ばかりの現状では実用化はしばらく先となりそうですが。
関連リンク
import.metaへの言及を含む日本語記事を探したところ以下の3つが見つかりました。