【Deno】相対パスでファイル操作をするときに気を付けること

# Denoでファイル操作

Denoには標準でファイル操作を行うことのできる関数がいくつか備わっています。
例えば、これらのものです。

```javascript
Deno.readFileSync("filepath") // filepathの読み込み
Deno.writeFileSync("filepath") //filepathに書き込み
```

これらによって、Denoで様々なファイル操作（読み書き・コピー・削除など）が簡単に行えます。
そして、これらの関数の第１引数には対象のファイルパスを入れるのですが、ここには絶対パスだけでなく相対パスも使うことができます。

```
test/
  ̩̩├data/
  │  └test.txt
  └main.ts
```

上記のようなディレクトリ構成になっている場合、`main.ts`を以下のようにして実行します。

```javascript:main.ts
const text = Deno.readTextFileSync("./data/test.txt");
console.log(text);

export {}
```

するとdataフォルダ内の`test.txt`ファイルの中身が出力されます。

# 相対パスの落とし穴
Denoのファイル操作関数の引数に与える相対パスのルートは、カレントディレクトリとなります。
下記の各コマンドはどれも同じ`main.ts`を実行しているが、`test.txt`が読み込めるのは一番上のカレントディレクトリを`test`直下にした時のみです。

```console
/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`を次のように変えてみましょう。

```typescript: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](https://servestjs.org/)のコードをヒントに作りました。

servestでは`pathResolver`を次のようにしています。
[実際のコード(Github)](https://github.com/keroxp/servest/blob/master/_util.ts#L5-L7)

```typescript
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モジュールを探すまでに時間かかったんだよなぁ…）

```javascript
/*** 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クラス未対応だけどファイルパスを引数にとる関数とかには使えるよ。