🦕

Denoのimport.metaの使い方

に公開

JSの import に加えDenoにおけるimportの補強をするような機能がいくつかあります。今回は実際にそれらを使ってみたいと思います。

https://docs.deno.com/runtime/reference/deno_namespace_apis/#import.meta

import.meta.main

import.meta.main はエントリーポイントかどうかのフラグがあり、true の場合はエントリーポイントで、false の場合はモジュールとして読み込まれたとわかります。
Node.jsの場合は require.main === module で判定していたかと思いますがDenoでは真偽値で判定可能です。

使い道としては例えば基本ライブラリとして提供するものの、もしファイルがそのまま実行されたときにはライブラリを利用した初期の処理を行うといったことが可能です。
以下はBase64のライブラリだがコマンドとして実行されたら引数をエンコードして表示するサンプルです。

lib.ts
export function encodeBase64(str: string) {
	return btoa(str);
}

if (import.meta.main) {
	if (Deno.args.length <= 0) {
		console.log('Need arguments.');
	} else {
		for (const str of Deno.args) {
			console.log(encodeBase64(str));
		}
	}
}
main.ts
import { encodeBase64 } from './lib.ts'
console.log(encodeBase64('Hello, World!'));
実行結果
# 引数なし実行なのでエラーメッセージを出力
$ deno run lib.ts
Need arguments.
# 引数を与える。今回はスペースで区切られて2つの値が渡される。
$ deno run lib.ts Hello, World!
SGVsbG8=
V29ybGQh
# ライブラリとして使用。今回は一つの文字列が渡される。
$ deno run main.ts
SGVsbG8sIFdvcmxkIQ==

import.meta.url

これは現在のファイルのパスをURL形式で取得します。URL形式は file:// から始まるので、Windowsだと file:///C:/dir1/dir2/filename.ts のようになります。

こちらもライブラリとして読み込んだ場合の挙動を見てみます。

dir/lib.ts
export function getURL() {
	return import.meta.url;
}
main.ts
import { getURL } from './dir/lib.ts'
console.log(import.meta.url);
console.log(getURL());
実行結果
$ deno run main.ts
file:///C:/project_dir/main.ts
file:///C:/project_dir/dir/lib.ts

これとDeno標準ライブラリを使うとその環境でのファイルパスを取得したり、ファイルパスの合成が可能です。

sample.ts
import { fromFileUrl, dirname, join } from 'https://deno.land/std/path/mod.ts';

// 生の値
console.log(import.meta.url);
// URL形式→実行環境の形式でファイルの絶対パスを取得
const filePath = fromFileUrl(import.meta.url);
console.log(filePath);
// 実行環境の形式で親ディレクトリの絶対パスを取得
const dirName = dirname(filePath);
console.log(dirName);
// 実行環境の形式で親ディレクトリ/dir/libs.tsの絶対パスを取得
const joinedPath = join(dirName, 'dir/lib.ts');
console.log(joinedPath);

実行結果は以下です。

実行結果
file:///C:/project_dir/main.ts
C:\project_dir\main.ts
C:\project_dir
C:\project_dir\dir\lib.ts

Denoの標準ライブラリの path は環境に応じて /\ を使い分けてくれます。
import.meta.url からこれを使っているソースファイルの絶対パスをURL形式で入手可能なので、 fromFileUrl() を使ってローカルの絶対パスに書き換えます。

この後 dirName()join() を使うことで目的のファイルの絶対パスを取得することができます。

import.meta.dirname / import.meta.filename

import.meta.url から自分のパスを取得することも可能ですが、便利なものもあります。それが import.meta.dirnameimport.meta.filename です。前者はディレクトリ、後者はフルパスを取得できます。

sample.ts
console.log(import.meta.dirname);
console.log(import.meta.filename);
実行結果
C:\project_dir
C:\project_dir\main.ts

上で紹介したやり方よりかなり楽に取得できますね。

import.meta.resolve()

import.meta.resolve() は第一引数に渡したモジュールのパスをURL形式のファイルパス文字列( file:// から始まる文字列 )で返してくれる関数です。

これだけなら諸々のモジュール解決に使うくらいかと思いますが、Denoでは重要な使い方があります。

Denoには compile という対象のプログラムを単一実行可能な実行可能プログラムとして出力する機能があります。

https://docs.deno.com/runtime/reference/cli/compile/#compiling-executables

こちらに --include ファイルやディレクトリ というオプションを追加すると任意のファイルを取り込んだ状態で実行可能プログラムを出力できます。

Workerを含める

エントリーポイントだけで完結するなら特に問題はないのですが、Workerは別のファイルを読み込ませて実行します。このままではbundleで出力されたもの以外にWorkerのソースファイルを用意しなければいけなくなります。しかもそれが他のモジュールに依存してたらどうするんだ?など、様々な問題が発生します。

ですが、import.meta.resolve() 経由でWorker側のファイルパスを指定し、compile 時にWorkerのファイルも一緒に指定すると一緒に単一ファイルに混ぜてもらえる他、実行ファイル内に取り込まれたWorkerの実行も可能です。

worker.ts
declare const self: Worker;
self.onmessage = (event) => {
	console.log('worker received:', event.data);
	self.postMessage('hello from worker!');
};
main.ts
const worker1 = new Worker(import.meta.resolve('./worker.ts'), {
	type: 'module'
});
worker1.postMessage('Hello from main!');
worker1.onmessage = (event) => {
	console.log('main received', event.data);
	worker1.terminate();
};
実行結果
$ deno run --allow-read main.ts
worker received: Hello from main!
main received hello from worker!
$ deno compile --output example --include worker.ts main.ts
Check file:///C:/project_dir/main.ts
Check file:///C:/project_dir/worker.ts
Compile file:///C:/project_dir/main.ts to example.exe
$ ./example.exe
worker received: Hello from main!
main received hello from worker!

無事単純実行でも compile で単一ファイルにしてもWorkerを実行することができました。

includeしたファイルをそのまま扱う

他にもリソースとして取り込んだファイルをプログラム中で使いたいことがあると思います。その場合は以下のようにURL経由にするといい感じに読み込んでくれます。

sample.txt
Hello, sample.txt
sample.ts
const path = import.meta.resolve('./sample.txt');
console.log(path);
const url = new URL(path);
console.log(url);

// こっちはエラーかつ --allow-read が必要。
// console.log(await Deno.readTextFile(path));
// こっちは成功かつ --allow-read が不要。
console.log(await Deno.readTextFile(url));
実行結果
$ deno compile --output sample.exe --include sample.txt sample.ts
Compile file:///C:/project_dir/sample.ts to sample.exe

Embedded Files

sample.exe
├── sample.ts (519B)
└── sample.txt (19B)

Size: 538B
$ ./sample.exe
file:///C:/Users/XXXXXX/AppData/Local/Temp/deno-compile-sample.exe/sample.txt
URL {
  href: "file:///C:/Users/XXXXXX/AppData/Local/Temp/deno-compile-sample.exe/sample.txt",
  origin: "null",
  protocol: "file:",
  username: "",
  password: "",
  host: "",
  hostname: "",
  port: "",
  pathname: "/C:/Users/XXXXXX/AppData/Local/Temp/deno-compile-sample.exe/sample.txt",
  hash: "",
  search: ""
}
Hello, sample.txt

ちゃんと読み込めています。ファイルパスで読み込もうとすると普通にファイルを読み込もうとするので注意してください。

まとめ

今回はDenoの import.meta 周りのまとめでした。Denoで色々したいなという場合にちょくちょく使うことになると思います。

参考

Discussion