Denoでファイル操作
Denoには標準でファイル操作を行うことのできる関数がいくつか備わっています。
例えば、これらのものです。
Deno.readFileSync("filepath") // filepathの読み込み
Deno.writeFileSync("filepath") //filepathに書き込み
これらによって、Denoで様々なファイル操作(読み書き・コピー・削除など)が簡単に行えます。
そして、これらの関数の第1引数には対象のファイルパスを入れるのですが、ここには絶対パスだけでなく相対パスも使うことができます。
test/
  ̩̩├data/
  │  └test.txt
  └main.ts
上記のようなディレクトリ構成になっている場合、main.tsを以下のようにして実行します。
const text = Deno.readTextFileSync("./data/test.txt");
console.log(text);
export {}
するとdataフォルダ内のtest.txtファイルの中身が出力されます。
相対パスの落とし穴
Denoのファイル操作関数の引数に与える相対パスのルートは、カレントディレクトリとなります。
下記の各コマンドはどれも同じmain.tsを実行しているが、test.txtが読み込めるのは一番上のカレントディレクトリをtest直下にした時のみです。
/user/test$ deno run -A main.ts // OK
/user$ deno run -A ./test/main.ts // NG
/user/test/data$ deno run -A ../main.ts // NG
ただこのようなコマンドは誰でも実行してしまうのではないでしょうか?
別ディレクトリで作業してて、カレントディレクトリを移動するほどでもないけどmain.tsを実行したいなって時とか。
解決案:pathResolverの作成
pathResolverとは、その名の通りパスを解決する(相対パス→絶対パス)ものです。
相対パスで問題が起こるなら、絶対パスに変換しちゃおうぜって考えです。
main.tsを次のように変えてみましょう。
import * as path from "https://deno.land/std@0.79.0/path/mod.ts";
function pathResolver(meta: ImportMeta): (p: string) => string {
  return (p) => path.fromFileUrl(new URL(p, meta.url));
}
const resolve = pathResolver(import.meta);
const text = Deno.readTextFileSync(resolve("./data/test.txt"));
console.log(text);
このようにすることで、resolve関数が相対パスを絶対パスに変換してくれるので、どのカレントディレクトリから実行しても同じ結果になります。
pathResolverの解説
pathResolverはimport.metaを引数にとります。import.metaは、そのファイル自体のファイルパスを格納しており、import.meta.urlで取得できます。これで取得できるのはファイルのURLなので、file:///~で始まるものになります。
pathResolverはそのmeta.urlを格納した新しい関数を返します。main.tsではこの関数の名前をresolveとしています。
このresolve関数に相対パスを渡してあげればURLクラスがいい感じにパスの関係を計算してくれます。ファイルにアクセスするには最初のfile://の部分は邪魔なので、Denoの標準モジュールstdのpathにあるfromFileUrl関数で消して素のファイルパスを返します。
注意点
solverはファイルごとに生成する必要があります。importしてはいけません。なぜならimport.metaはファイルによって違うからです。
まとめ
ファイル操作系...というか相対パスを扱うときにはpathResolverを使用しましょう。意図しないバグの発生を抑えることができます。
ちなみにimport文の相対パスは勝手に解決してくれるのでpathResolverは使わなくて大丈夫です。
参考にしたコード
このpathResolverは、僕がDenoでサーバを立てるときにお世話になっているモジュールservestのコードをヒントに作りました。
servestではpathResolverを次のようにしています。
実際のコード(Github)
export function pathResolver(meta: ImportMeta): (p: string) => string {
  return (p) => new URL(p, meta.url).pathname;
}
この記事内で示したものと外形は同じですが、pathモジュールを使っている所に違いがあります。
じつは、上記のコード1つ欠点があってWindowsではうまく動作しないということです。
Linux系(たぶんmacも)はパスの最初がスラッシュ/で始まっているときに絶対パスとみなします。
一方、Windows系は絶対パスの最初はドライブ名で始まるということになっています。
new URL().pathnameでは、import.meta.urlから取得したfile:///~付きパスのfile://しかとってくれません。Linux系だとスラッシュから始まることで絶対パスを表すので問題ないですが、Windowsではおかしくなります。
この問題を解決するのにpathモジュールを使用しています。
(このpathモジュールを探すまでに時間かかったんだよなぁ…)
/*** Linux系では ***/
const urlLinux = "file:///home/test/test.txt";
const pathLinux = new URL("",urlLinux).pathname;
console.log(pathLinux); 
// 出力:/home/test/test.txt
/*** Windowsでは ***/
const urlWin = "file:///c:/users/test/test.txt";
const pathWin = new URL("",urlWin).pathname;
console.log(pathWin);
// 出力:/c:/users/test/test.txt
// ↑パスの形式としておかしい
余談
ちなみにDenoのファイル操作系関数はURLクラスの引数にも対応してるっぽいのでpathResolverはいらないっぽい?(未確認)
ただ、URLクラス未対応だけどファイルパスを引数にとる関数とかには使えるよ。