importとMetaProperty

importに対するプロパティアクセス記法がどのように実装されているのか

ECMAScript Modules(ESM)ではCommonJSにおける__filename__dirnameが使えない.
例えば__filenameを参照したいとき、ESMではimport.meta.urlimport.meta.filenameのようにかける.

// CommonJSではok
// console.log(__filename);
// console.log(__dirname);

// ESMでは`import.meta`を介してアクセスする
const dirname = import.meta.dirname;
const filename = import.meta.filename;

importは識別子じゃないのになんで.metaにアクセスできるんだっけ?importってオブジェクトだっけ?と思い調べてみた.

結論、ECMAScript仕様書によるとimportは予約語であり、オブジェクトではないが、MetaPropertyという構文規則が定義されている.
これによりimport.metaのようにキーワードに対してプロパティアクセスのような表記が可能になっている.

importの文法規則

ECMA262をみると、importトークンで始まる文法規則は以下の3つが定義されている.

  • ImportDeclaration:
    • import ImportClause FromClause WithClause;\textbf{import}\ \pmb{\mathit{ImportClause}}\ \pmb{\mathit{FromClause}}\ \pmb{\mathit{WithClause}}\text{?}\ \texttt{;}
    • import ModuleSpecifier WithClause;\textbf{import}\ \pmb{\mathit{ModuleSpecifier}}\ \pmb{\mathit{WithClause}}\text{?}\ \texttt{;}
    • 例: import { xxx } from "yyy";などのstatic import
  • ImportCall:
    • import ( AssignmentExpression )\textbf{import}\ \pmb{(}\ \pmb{\mathit{AssignmentExpression}}\ \pmb{)}
    • 例: import("./data.json")のようなdynamic import
  • ImportMeta:
    • import . meta\textbf{import}\ \textbf{.}\ \textbf{meta}
    • 今回のimport.metaの記法を定義.

ImportDeclarationImportCallは共にそれぞれ自由に各Clauseを指定できるが、ImportMetaimport.metaという特定の表記に閉じている.
importがオブジェクトであればimport['meta']のような表記も許されるはずだが、実際にはimportは予約語であってオブジェクトではない. ImportMetaがあって初めてimport.metaのようにプロパティアクセスのような表記が可能になっている.

どうしてこうなった

MetaPropertyという文法規則はES6(ES2015)でnew.targetとともに提案された.
(new.target自体はサブクラス化のための機能で、newで呼び出されたコンストラクタ自体を参照できる. new.targetについてはこちらが詳しい.)

この時点でこの形式を取った理由は把握できなかったが, new.targetを提案した時点では「MetaPropertyという一般的パターンを作っている」という意識的な議論はなく、後からAllenがそれをパターンとして認識し、拡張を提案した、という経緯のようだった.

ここでは以下のように説明している.

The problem is that we have had no common way to approach making such values available. The space of available keywords and operator symbols that might be associated with such values is severely limited.
...
Syntactically a MetaProperty is a pre-existing reserved word followed by a period and then an IdentifierName.
...
this establishes a syntactic pattern that could be applied for accessing other contextually variable run-time values.

ざっくりまとめると、

  • 文脈依存の実行時の値を取得するための共通の方法がなかったが、これらを表現するためのキーワード・演算子が限られている
  • MetaPropertynew.targetに限らずにこれを実現する構文パターンを<reserved word> . <IdentifierName>の形でまとめた

と言っている.

new.target以前には予約語に.が続く構文規則が存在しなかったため、このような構文パターンであれば既存実装に影響を与えずに機能追加を行うことができる.

import.metaのモチベ

そしてその後TC39でimport.metaが提案された.
tc39/proposal-import-meta では以下のように説明されている.

  • モジュール内で評価されるコードに対し、ホスト環境がモジュール固有の情報を渡したい
  • CJSで取得出来ていたスコープ内の変数(__filename__dirname)がESMでは同じ方法では取得できない

このモチベーションもまさにMetaPropertyの提案で説明されていた実行時の値を取得する機能で、import.metaの構文として提案されている.
またこのプロポーザルではMetaProperty以外の構文も検討している.

Using a particular module specifier

js:contextのように特定のモジュール指定子を用いて、コンテキスト固有の値を取得するといったアプローチ.

import { url, scriptElement } from "js:context";

この場合はECMAScriptへの提案が不要で、ランタイム実装側で対応可能だった. ただWebKitチームからの反対があったとのこと.

Introducing lexical variables in another way

CJSでの__dirnameのように, モジュールのスコープ内に特定の変数を導入するアプローチ.

console.log(moduleURL);
console.log(moduleScriptElement);

このアプローチの場合もECMAScriptの仕様を変更せずにランタイム側で実現可能だが、TC39チームからは新しい変数がモジュールスコープを汚染することへの懸念があったとのこと.

現在のMetaProperty

MetaPropertyは13.3.12で以下のように定義されている.

現在、MetaPropertyは以下の2つが定義されている.

  • new.target
  • import.meta

脱線: V8の実装

importに続く表現がこれら3つの形に閉じているため、パーサは次にくるトークンが.(かをチェックするだけでよい.
実際V8の実装もそのようなロジックで実装されている.

if (next == Token::kImport) {
    // We must be careful not to parse a dynamic import expression as an import
    // declaration. Same for import.meta expressions.
    Token::Value peek_ahead = PeekAhead();
    if (peek_ahead != Token::kLeftParen && peek_ahead != Token::kPeriod) {
      ParseImportDeclaration();
      return factory()->EmptyStatement();
    }
  }

  return ParseStatementListItem();
}

https://github.com/v8/v8/blob/ced134e0753aeb2e3c1c669715812a9f2b1d2514/src/parsing/parser.cc#L1373-L1384

PeekAheadで先読みしたトークンが(.のどちらでもなければImportDeclarationとして処理し、そうでなければImportCallimport.metaのどちらかとしてParseStatementListItem()に流すようになっている.

その後ParseStatementListItem → ParseStatement → ParseExpressionStatement → ParseMemberExpression → ParsePrimaryExpression → ParseImportExpression と渡され、importの次のトークンに応じてパースを行う.

template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseImportExpressions() {
  Consume(Token::IMPORT);
  int pos = position();
  if (Check(Token::PERIOD)) {
    // `import`の次のトークンが`.`であれば`import.meta`としてパース
    ExpectContextualKeyword(ast_value_factory()->meta_string(), "import.meta",
                            pos);
    if (!flags().is_module()) {
      impl()->ReportMessageAt(scanner()->location(),
                              MessageTemplate::kImportMetaOutsideModule);
      return impl()->FailureExpression();
    }

    return impl()->ImportMetaExpression(pos);
  }

  // `import`の次のトークンが`(`かチェック、以降はdynamic importとしてパースしている
  if (V8_UNLIKELY(peek() != Token::LPAREN)) {

https://github.com/v8/v8/blob/5fe0aa3bc79c0a9d3ad546b79211f07105f09585/src/parsing/parser-base.h#L3693

所感

  • MetaPropertyの構文パターンは最初から意図して設計されたものではなく、new.target を設計する際にnewが予約語であること、new.が既存構文と衝突しないことを利用した結果として生まれたものだった.

参考

Commits

2026-04-18 00:41:15blog: ecma meta property77abad01
commit 77abad018679ccad7366ba927cc619ff5ee9a0d6
Author: koka &&gt;
Date:   Sat Apr 18 00:41:15 2026 +0900

  blog: ecma meta property

diff --git a/_posts/2026-02-14-ecma-meta.md b/_posts/2026-02-14-ecma-meta.md
new file mode 100644
index 00000000..a1cd845b
--- /dev/null
+++ b/_posts/2026-02-14-ecma-meta.md
@@ -0,0 +1,182 @@
+---
+title: importとMetaProperty
+date: 2026-04-16
+categories:
+- memo
+tags:
+- ECMA
+description: importに対するプロパティアクセス記法がどのように実装されているのか
+---
+
+ECMAScript Modules(ESM)ではCommonJSにおける`__filename`や`__dirname`が使えない.   
+例えば`__filename`を参照したいとき、ESMでは`import.meta.url`や`import.meta.filename`のようにかける.  
+
+\```javascript[data-file="src/test.js"]
+// CommonJSではok
+// console.log(__filename);
+// console.log(__dirname);
+
+// ESMでは`import.meta`を介してアクセスする
+const dirname = import.meta.dirname;
+const filename = import.meta.filename;
+\```
+
+`import`は識別子じゃないのになんで`.meta`にアクセスできるんだっけ?`import`ってオブジェクトだっけ?と思い調べてみた.
+
+結論、ECMAScript仕様書によると`import`は予約語であり、オブジェクトではないが、`MetaProperty`という構文規則が定義されている.  
+これにより`import.meta`のようにキーワードに対してプロパティアクセスのような表記が可能になっている.  
+
+## importの文法規則
+
+ECMA262をみると、`import`トークンで始まる文法規則は以下の3つが定義されている.
+
+- [**ImportDeclaration**](https://tc39.es/ecma262/2025/#prod-ImportDeclaration):
+  - $$\textbf{import}\ \pmb{\mathit{ImportClause}}\ \pmb{\mathit{FromClause}}\ \pmb{\mathit{WithClause}}\text{?}\ \texttt{;}$$
+  - $$\textbf{import}\ \pmb{\mathit{ModuleSpecifier}}\ \pmb{\mathit{WithClause}}\text{?}\ \texttt{;}$$
+  - 例: `import { xxx } from "yyy";`などのstatic import
+- [**ImportCall**](https://tc39.es/ecma262/2025/#prod-ImportCall): 
+  - $$\textbf{import}\ \pmb{(}\ \pmb{\mathit{AssignmentExpression}}\ \pmb{)}$$
+  - 例: `import("./data.json")`のようなdynamic import
+- [**ImportMeta**](https://tc39.es/ecma262/2025/#prod-ImportMeta): 
+  - $$\textbf{import}\ \textbf{.}\ \textbf{meta}$$
+  - 今回の`import.meta`の記法を定義.
+
+
+`ImportDeclaration`と`ImportCall`は共にそれぞれ自由に各Clauseを指定できるが、`ImportMeta`は`import.meta`という特定の表記に閉じている.  
+`import`がオブジェクトであれば`import['meta']`のような表記も許されるはずだが、実際には`import`は予約語であってオブジェクトではない. `ImportMeta`があって初めて`import.meta`のようにプロパティアクセスのような表記が可能になっている.
+
+## どうしてこうなった
+
+`MetaProperty`という文法規則はES6(ES2015)で`new.target`とともに提案された.  
+(`new.target`自体はサブクラス化のための機能で、`new`で呼び出されたコンストラクタ自体を参照できる. `new.target`については[こちら](https://js-next.hatenablog.com/entry/2015/03/24/190047)が詳しい.)  
+
+この時点でこの形式を取った理由は把握できなかったが, `new.target`を提案した時点では「MetaPropertyという一般的パターンを作っている」という意識的な議論はなく、後からAllenがそれをパターンとして認識し、拡張を提案した、という経緯のようだった.
+
+ここでは[以下のように説明](https://github.com/allenwb/ESideas/blob/master/ES7MetaProps.md)している.
+
+&gt; The problem is that we have had no common way to approach making such values available.
+&gt; The space of available keywords and operator symbols that might be associated with such values is severely limited.  
+&gt; ...  
+&gt; Syntactically a MetaProperty is a pre-existing reserved word followed by a period and then an IdentifierName.  
+&gt; ...  
+&gt; this establishes a syntactic pattern that could be applied for accessing other contextually variable run-time values.
+
+ざっくりまとめると、
+- 文脈依存の実行時の値を取得するための共通の方法がなかったが、これらを表現するためのキーワード・演算子が限られている
+- `MetaProperty`は`new.target`に限らずにこれを実現する構文パターンを`&lt;reserved word&gt; . &lt;IdentifierName&gt;`の形でまとめた
+
+
+と言っている.  
+
+`new.target`以前には予約語に`.`が続く構文規則が存在しなかったため、このような構文パターンであれば既存実装に影響を与えずに機能追加を行うことができる.
+
+## import.metaのモチベ
+
+そしてその後TC39で`import.meta`が提案された.  
+[tc39/proposal-import-meta](https://github.com/tc39/proposal-import-meta) では以下のように説明されている.
+
+- モジュール内で評価されるコードに対し、ホスト環境がモジュール固有の情報を渡したい
+- CJSで取得出来ていたスコープ内の変数(`__filename`や`__dirname`)がESMでは同じ方法では取得できない
+
+このモチベーションもまさに`MetaProperty`の提案で説明されていた実行時の値を取得する機能で、`import.meta`の構文として提案されている.  
+またこのプロポーザルではMetaProperty以外の構文も検討している.
+
+### *Using a particular module specifier*
+
+`js:context`のように特定のモジュール指定子を用いて、コンテキスト固有の値を取得するといったアプローチ.  
+
+\```js
+import { url, scriptElement } from "js:context";
+\```
+
+この場合はECMAScriptへの提案が不要で、ランタイム実装側で対応可能だった. ただWebKitチームからの反対があったとのこと.
+
+### *Introducing lexical variables in another way*
+
+CJSでの`__dirname`のように, モジュールのスコープ内に特定の変数を導入するアプローチ.  
+
+\```js
+console.log(moduleURL);
+console.log(moduleScriptElement);
+\```
+このアプローチの場合もECMAScriptの仕様を変更せずにランタイム側で実現可能だが、TC39チームからは新しい変数がモジュールスコープを汚染することへの懸念があったとのこと.
+
+## 現在のMetaProperty
+
+[`MetaProperty`](https://tc39.es/ecma262/2025/#sec-meta-properties)は13.3.12で以下のように定義されている.
+
+現在、MetaPropertyは以下の2つが定義されている.
+- `new.target`
+- `import.meta`
+
+
+### 脱線: V8の実装
+
+importに続く表現がこれら3つの形に閉じているため、パーサは次にくるトークンが`.`か`(`かをチェックするだけでよい.   
+実際V8の実装もそのようなロジックで実装されている.
+
+\```cpp[data-file="src/parsing/parser.cc"]
+  if (next == Token::kImport) {
+    // We must be careful not to parse a dynamic import expression as an import
+    // declaration. Same for import.meta expressions.
+    Token::Value peek_ahead = PeekAhead();
+    if (peek_ahead != Token::kLeftParen && peek_ahead != Token::kPeriod) {
+      ParseImportDeclaration();
+      return factory()-&gt;EmptyStatement();
+    }
+  }
+
+  return ParseStatementListItem();
+}
+\```
+&lt;small&gt;
+
+https://github.com/v8/v8/blob/ced134e0753aeb2e3c1c669715812a9f2b1d2514/src/parsing/parser.cc#L1373-L1384
+
+&lt;/small&gt;
+
+`PeekAhead`で先読みしたトークンが`(`か`.`のどちらでもなければ`ImportDeclaration`として処理し、そうでなければ`ImportCall`か`import.meta`のどちらかとして`ParseStatementListItem()`に流すようになっている.
+
+その後ParseStatementListItem → ParseStatement → ParseExpressionStatement → ParseMemberExpression → ParsePrimaryExpression → ParseImportExpression と渡され、`import`の次のトークンに応じてパースを行う.
+
+\```cpp[data-file="src/parsing/parser-base.h"]
+template &lt;typename Impl&gt;
+typename ParserBase&lt;Impl&gt;::ExpressionT
+ParserBase&lt;Impl&gt;::ParseImportExpressions() {
+  Consume(Token::IMPORT);
+  int pos = position();
+  if (Check(Token::PERIOD)) {
+    // `import`の次のトークンが`.`であれば`import.meta`としてパース
+    ExpectContextualKeyword(ast_value_factory()-&gt;meta_string(), "import.meta",
+                            pos);
+    if (!flags().is_module()) {
+      impl()-&gt;ReportMessageAt(scanner()-&gt;location(),
+                              MessageTemplate::kImportMetaOutsideModule);
+      return impl()-&gt;FailureExpression();
+    }
+
+    return impl()-&gt;ImportMetaExpression(pos);
+  }
+
+  // `import`の次のトークンが`(`かチェック、以降はdynamic importとしてパースしている
+  if (V8_UNLIKELY(peek() != Token::LPAREN)) {
+\```
+
+&lt;small&gt;
+
+https://github.com/v8/v8/blob/5fe0aa3bc79c0a9d3ad546b79211f07105f09585/src/parsing/parser-base.h#L3693
+
+&lt;/small&gt;
+
+## 所感
+
+- `MetaProperty`の構文パターンは最初から意図して設計されたものではなく、`new.target` を設計する際に`new`が予約語であること、`new.`が既存構文と衝突しないことを利用した結果として生まれたものだった.
+
+### 参考
+
+- https://github.com/allenwb/ESideas/blob/master/ES7MetaProps.md
+- https://github.com/tc39/proposal-import-meta
+- https://js-next.hatenablog.com/entry/2015/03/24/190047
+  - `new.target`について
+- https://qiita.com/uhyo/items/7b00ad577618554d3276
+  - `import.meta`の`meta`オブジェクトのプロパティについて詳しい.仕様では`ImportMeta`はオブジェクトとして定義されているが、そのオブジェクトの持つプロパティは実装依存.