モジュール時代のJavaScriptにおける必需品「import.meta」の総復習

`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のプロポーザルはこちらです](https://github.com/tc39/proposal-import-meta)。

## `import.meta`とは何か

一言で言えば、`import.meta`は**モジュールに固有の情報を持つメタプロパティ**です。

### メタプロパティ

まず**メタプロパティ**から解説します。これは、**プロパティアクセスっぽく見えるけどそうではない構文**のことを指します。実際、`import.meta`という構文は`import`というオブジェクトの`meta`というプロパティに見えますが、そうではありません。なぜなら、`import`は予約語であり変数名ではないからです。これはあくまで`import.meta`というひとつの構文であり、これに外れる形（例えば`import.foobar`など）は構文エラーとなります。

`import.meta`は現在提案中のメタプロパティですが、実はすでに現在JavaScriptに採用されているメタプロパティが1つだけあります。それは`new.target`です。`new.target`もあまり日本語記事がありませんが、[こちらの記事](http://js-next.hatenablog.com/entry/2015/03/24/190047)が詳しいので読んでみるとよいでしょう。また、現在提案中（Stage 2）の`function.sent`というメタプロパティもあります。いずれも、`予約語.プロパティ名`という構文になっているのがメタプロパティの特徴です。

メタプロパティは、**特定のコンテキストに固有の情報**を表すのに使用される傾向があります。これは、仕様がグローバルに提供する何らかのAPIを使用するのに比べてコンテキスト依存であることが分かりやすいからだと思われます。そのため、メタプロパティは特定の状況でしか使用できないことがほとんどです。例えば`new.target`は関数がコンストラクタとして呼ばれたときに意味があるものですから、関数の中でしか使用できません。また、アロー関数はコンストラクタとして使用できないため、アロー関数の中でも使用できません。

### モジュール

では`import.meta`が使用できるのはどこかというと、**モジュールの中**です。

モジュールについての詳細な説明は省きますが、これはJavaScriptプログラムを複数のファイルに分割するための仕組みです。この分割された各ファイルが一つ一つの**モジュール**となります。モジュール`export`文を用いて自身から何らかの値を他のモジュールに対して提供したり、`import`文を用いて他のモジュールが提供する値を読みこんだりできます。モジュールでない従来のJavaScriptプログラムは**スクリプト**と呼んで区別されます。`import.meta`はモジュールの中では使えますがスクリプトの中では使えないのです。

HTMLでは、`script`要素を用いてモジュールを読み込む場合は`type="module"`という属性を付ける必要があります。例えば以下のようにすることで、`module.js`がモジュール扱いで読みこまれます。

```html
<script type="module" src="./module.js"></script>
```

一方、`type="module"`を付け忘れると`module.js`はスクリプト扱いで読みこまれます。スクリプト扱いの場合は`module.js`の中で`import`文や`export`文、そして`import.meta`は使えません。

モジュールの動作例はこんな感じです。

```js
/// 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仕様書において定められています](https://html.spec.whatwg.org/multipage/webappapis.html#hostgetimportmetaproperties)。それによれば、`import.meta`（によって得られるオブジェクト）は`url`というプロパティをひとつだけ持っています。これは、**そのモジュールのURL**です。次の例で考えてみます。

```js:module.mjs
console.log(import.meta.url);
```

```html:index.html
<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`というプロパティを入れようという[議論がされています](https://github.com/whatwg/html/issues/1013)。ところが、これはさまざまな理由から難航しています。かなりうまく作らないと使い物にならなそうだということが分かってきたからです。

`import.meta.scriptElement`というのは、そのモジュールが読みこまれるきっかけとなった`script`要素を得ることができるプロパティです（名前は変わるかもしれません）。上の例を少しいじってみましょう。

```js:module.mjs
console.log(import.meta.scriptElement.id); // "abc"が表示される
```

```html:index.html
<script id="abc" type="module" src="./module.mjs"></script>
```

`index.html`は`script`要素を用いて`module.mjs`を読み込みます。そうして実行された`module.mjs`が`import.meta.scriptElement`を見ると、`index.html`内の`script`要素が得られるというわけです。

なんとなく便利そうに見えますが、すでに問題が見えています。`index.html`を次のようにすると何が表示されるでしょうか。

```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`の例を見ましょう。

```js:module.mjs
console.log(import.meta.url);
```

node.jsで動作を確認するには次のようにします。

```sh
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つが見つかりました。

- [Node.jsのES Modulesサポートの現状確認と備え](https://teppeis.hatenablog.com/entry/2017/08/es-modules-in-nodejs)
- [Node.jsとECMAScript Modules](https://blog.hiroppy.me/entry/nodejs-esm)
- [ESModules について](https://qiita.com/nanocloudx/items/f541c7ff95b10b40d189)