30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

ES2020 でリリースされた import.meta について

ES2020 でimport.metaがリリースされた。
この機能を使うと、モジュールのメタ情報を取得することができる。
例えば、当該モジュールのパスを取得することができる。

モジュールのメタ情報を取得する機能であるため、CommonJS や Script モードで使用するとエラーになる。

import.metaでどのような情報を取得できるか、Node.js、Deno、ブラウザで試してみた。

それぞれのバージョンは以下の通り。

  • Node.js: 14.7.0
  • Deno: 1.2.2
  • Google Chrome: 84.0.4147.105

Node.js

前述の通り CommonJS だとエラーになるため、ES Modules で使う必要がある。

// SyntaxError: Cannot use 'import.meta' outside a module
console.log(import.meta);

Node.js で ES Modules を使う方法については、以下の記事に書いた。

numb86-tech.hatenablog.com

ES Modules として書かれたファイルなら、import.metaを使える。

// [Object: null prototype] {
//   url: 'file:///Users/numb/index.mjs'
// }
console.log(import.meta);

ES Modules では__dirname__filenameを使えないが、import.meta.urlでファイルパスを取得することができる。

// console.log(__dirname); // ReferenceError: __dirname is not defined
// console.log(__filename); // ReferenceError: __filename is not defined
console.log(import.meta.url); // file:///Users/numb/index.mjs

Deno

Deno では、urlの他にmainという名前の値を取得することができる。

console.log(import.meta); // { url: "file:///Users/numb/index.ts", main: true }

mainは、プログラムの起点として Deno に渡されたモジュールかどうかを、真偽値で持っている。

例えば、以下のモジュールがあったとする。

// meta-sub.ts
console.log(import.meta.main);
export const x = 1;
// meta.ts
import { x } from "./meta-sub.ts";
console.log(x);
console.log(import.meta.main);

この状況でmeta.tsを実行すると、meta.tsがメインのスクリプトとなるため(meta-sub.tsmeta.tsに呼ばれているだけ)、meta-sub.tsimport.meta.mainfalseになる。

$ deno run meta.ts
false
1
true

meta-sub.tsを実行すれば、meta-sub.tsimport.meta.maintrueになる。

$ deno run meta-sub.ts
true

ブラウザ

以下のコードを Deno で実行して、サーバを立てる。

import {
  listenAndServe,
} from "https://deno.land/std@0.63.0/http/mod.ts";

listenAndServe(
  { port: 8080 },
  async (req) => {
    if (req.method !== "GET") {
      req.respond({
        status: 405,
      });
      return;
    }

    switch (req.url) {
      case "/":
        req.respond({
          status: 200,
          headers: new Headers({
            "content-type": "text/html",
          }),
          body: await Deno.readFile("./index.html"),
        });
        break;

      case "/module.js":
        req.respond({
          status: 200,
          headers: new Headers({
            "content-type": "text/javascript",
          }),
          body: await Deno.readFile("./module.js"),
        });
        break;

      default:
        req.respond({
          status: 404,
          headers: new Headers({
            "content-type": "text/plain",
          }),
          body: "Not found\n",
        });
        break;
    }
  },
);

console.log("Server running on localhost:8080");

index.htmlmodule.jsはそれぞれ、以下の内容。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ImportMeta</title>
</head>
<body>
  <script type="module" src="/module.js"></script>
</body>
</html>
console.log(Object.keys(import.meta));
console.log(import.meta.url);

この状態でhttp://localhost:8080/にアクセスするとindex.htmlが表示され、そのなかでmodule.jsを読み込む。
scriptタグにはtype="module"をつけているが、これがないと Script モードになってしまい、import.metaを使ったときにエラーになるので注意。

ブラウザのログには以下のように表示されており、import.meta.urlで当該ファイル(今回の場合はmodule.js)の URL を取得できることが分かる。

["url"]
http://localhost:8080/module.js

Node.js における ES Modules を理解する

記事執筆時点での最新版の Node.js では、モジュールシステムとして ES Modules を使うことができる。
また、CommonJS で書かれたモジュールを ES Modules で読み込むこともできる。

Node.js のモジュールシステムは複雑すぎて苦手意識があったので、整理した。

この記事の内容は、Node.js のv14.7.0で動作確認している。
Node.js のモジュールシステムはバージョン毎に挙動が大きく変わるので、注意が必要。

そのファイルは CJS なのか ESM なのか

Node.js で使えるモジュールシステムとして、ES Modules(以下、ESM)の他に CommonJS(以下、CJS)があり、CJS がデフォルトになっている。
Node.js におけるモジュールシステムを理解するためにはまず、Node.js が各ファイルをどのモジュールシステムとして扱うのかを、理解できるようになる必要がある。
CJS で書かれたファイルとして扱われるのか、ESM で書かれたファイルとして扱われるのかで、挙動が変わるためである。

現状、typeof moduleの値で、どちらのモジュールシステムなのかを判断できる。
CJS の場合はobjectに、ESM の場合はundefinedになる。

// CJS
console.log(typeof module); // object

// ESM
console.log(typeof module); // undefined

拡張子による判断

まず、拡張子による判断が行われる。

拡張子が.cjsのファイルは CJS、.mjsのファイルは ESM として扱われる。

// ./src/index.cjs
console.log(typeof module); // object
// ./src/index.mjs
console.log(typeof module); // undefined

そして拡張子が.jsのファイルは、package.jsontypeフィールドの値によって判断される。

package.json の type による判断

package.jsontypeフィールドがcommonjsの場合は、.jsファイルは CJS として扱われる。

// ./src/index.js

console.log(typeof module); // object

commonjsがデフォルト値なので、typeフィールドが存在しない場合も、CJS となる。

typeフィールドをmoduleにすると、.jsファイルが ESM として扱われるようになる。

// ./src/index.js

console.log(typeof module); // undefined

対象のファイルと同じディレクトリにpackage.jsonがない場合は、上位のディレクトリを検索していき、一番近い場所にあるpackage.jsonの内容が反映される。

例えば以下のディレクトリ構成の時に、./package.jsontypeフィールドがmoduleで、./src/utils/package.jsontypeフィールドがcommonjsだったとする。

├── package.json
└── src
    ├── index.js
    └── utils
        ├── index.js
        └── package.json

そうすると、各.jsファイルの実行結果は、次のようになる。

// ./src/index.js

// ESM として扱われている
console.log(typeof module); // undefined
// ./src/utils/index.js

// CJS として扱われている
console.log(typeof module); // object

なお、拡張子が.cjs.mjsのファイルについては、typeフィールドの影響は受けない。

ESM 同士の挙動

各ファイルがどちらのモジュールシステムで扱われるのかを理解できたので次は、ファイルのインポートとエクスポートについて見ていく。

まずは、ESM 同士の挙動について。

// ./src/utils/foo.mjs
export default 1;
export const x = 2;
// ./src/index.mjs
import Foo, {x} from './utils/foo.mjs';

console.log(Foo); // 1
console.log(x); // 2

特に問題なく扱える。特定のフラグやオプションが必要になることもない。

拡張子の省略はできない。

// ./src/index.mjs

// Error [ERR_MODULE_NOT_FOUND]
import Foo, {x} from './utils/foo';

--es-module-specifier-resolution=nodeフラグをつけて実行すると、拡張子が省略可能になる。

$ node --es-module-specifier-resolution=node src/index.mjs
1
2

また、このフラグをつけている場合、パスとしてディレクトリを指定することでindexファイルを読み込む、ということも可能になる。

// ./src/utils/index.mjs
export default 9;
// ./src/index.mjs
import Foo from './utils/';

console.log(Foo); // 9

CJS 同士の挙動

次は、CJS 同士の挙動について。

// ./src/utils/bar.cjs
module.exports = {
  y: 8,
}
// ./src/index.cjs
const Bar = require('./utils/bar.cjs');

console.log(Bar); // { y: 8 }

特に問題ないが、こちらも、拡張子の省略について注意点がある。
.cjsファイルの拡張子を省略するとエラーになる。
そのため、上記の./src/index.cjsを以下のように書き換えると、エラーになってしまう。

// ./src/index.cjs

// Error: Cannot find module './utils/bar'
const Bar = require('./utils/bar');

CJS として扱われている.jsファイルを読み込む場合は、拡張子を省略できる。

// ./src/utils/bar.js
module.exports = {
  y: 6,
}
// ./src/index.cjs
const Bar = require('./utils/bar');

console.log(Bar); // { y: 6 }

ディレクトリを指定することでindex.jsを読み込むこともできる。これも、index.cjsを読み込もうとするとエラーになるので注意。

// ./src/utils/index.js
module.exports = {
  y: 56,
}
// ./src/index.cjs
const Bar = require('./utils/');

console.log(Bar); // { y: 56 }

ESM から CJS を読み込む

デフォルトインポートは、問題なく行える。

// ./src/utils/bar.cjs
module.exports = {
  y: 8,
}
// ./src/index.mjs
import Bar from './utils/bar.cjs';

console.log(Bar); // { y: 8 }

名前付きインポートは、エラーになる。

// ./src/index.mjs

// SyntaxError
import {y} from './utils/bar.cjs';

これは、CJS から読み込むものはdefaultでラップされるため、このような挙動になる。

// ./src/index.mjs
import * as Bar from './utils/bar.cjs';

console.log(Bar); // [Module] { default: { y: 8 } }
console.log(Bar.default); // { y: 8 }
console.log(Bar.default.y); // 8

但し、標準モジュールは名前付きインポートを行える。

// ./src/index.mjs
import {readFile} from 'fs';

console.log(readFile); // [Function: readFile]

CJS から ESM を読み込む

以下の ESM ファイルを、CJS ファイルで読み込んでみる。

// ./src/utils/foo.mjs
export default 1;
export const x = 2;

まず、ESM ファイルに対してrequireを実行すると、エラーになる。

// ./src/index.cjs

// Error [ERR_REQUIRE_ESM]
const Foo = require('./utils/foo.mjs');

CJS ファイルのなかでimport文を使うと、シンタックスエラーになる。

// ./src/index.cjs

// SyntaxError: Cannot use import statement outside a module
import {Foo} from './utils/foo.mjs';

但し Dynamic import は実行可能なので、それを使って ESM ファイルを読み込むことができる。

// ./src/index.cjs

import('./utils/foo.mjs').then(res => {
  console.log(res); // [Module] { default: 1, x: 2 }
});

スキームが http や https の URL は読み込めない

エラーメッセージにあるように、スキームが file か data の URL しか対応しておらず、それ以外の URL を読み込むことはできない。

// ./src/index.mjs

// Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only file and data URLs are supported by the default ESM loader
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.esm.browser.js';

参考資料