ECMAScript Modules(ESM)ではCommonJSにおける__filenameや__dirnameが使えない.
例えば__filenameを参照したいとき、ESMではimport.meta.urlやimport.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 { xxx } from "yyy";などのstatic import
- ImportCall:
- 例:
import("./data.json")のようなdynamic import
- ImportMeta:
- 今回の
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についてはこちらが詳しい.)
この時点でこの形式を取った理由は把握できなかったが, 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.
ざっくりまとめると、
- 文脈依存の実行時の値を取得するための共通の方法がなかったが、これらを表現するためのキーワード・演算子が限られている
MetaPropertyはnew.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.targetimport.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();
}
PeekAheadで先読みしたトークンが(か.のどちらでもなければImportDeclarationとして処理し、そうでなければImportCallかimport.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)) {
所感
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はオブジェクトとして定義されているが、そのオブジェクトの持つプロパティは実装依存.
Comments