0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.env ↔ JSON ↔ YAML をブラウザ完結で相互変換する。N×N変換を『中間表現』で畳む設計(js-yaml)

0
Posted at

3行まとめ

  • .env / JSON / YAML を相互変換するツールをブラウザ完結で作った。APIキーや接続文字列もサーバーに送らない
  • 3形式 × 双方向で6通りの変換を、全パターン個別実装せず 「中間表現(共通のJSオブジェクト)」経由にして parse 3本 + serialize 3本に畳んだ
  • .env はフラット限定なのでネストはエラー。js-yaml の dumplineWidth: -1 を入れないと長い値が勝手に折り返される

Docker Compose の YAML、Node.js の .env、Kubernetes Secret の JSON。同じ設定値を、ツールごとに違うフォーマットで書き直す場面はやたら多い。手で変換すると地味にミスるし、オンライン変換ツールに API キーを貼るのは気が引ける。

ぱんだツールズに作った .env ↔ JSON ↔ YAML 変換ツールは、これをブラウザ内で完結させる。実装で面白いのは「3形式の総当たり変換をどう実装量を抑えて書くか」という設計の部分なので、そこを中心にまとめる。

N×N の変換を全部書くと地獄になる

3形式の相互変換は、素直に数えると .env→JSON .env→YAML JSON→.env JSON→YAML YAML→.env YAML→JSON6通り。形式が増えるたびに変換関数が二乗で増えていく(4形式なら12通り、5形式なら20通り)。これを1個ずつ実装するのは保守性が最悪だ。

なので 「中間表現」を1枚噛ませる。どの形式も一度 JavaScript のオブジェクト(プレーンな Record や配列)に parse し、そこから目的の形式へ serialize する。こうすると必要なのは「各形式の parser」と「各形式の serializer」だけ。3形式なら parse 3本 + serialize 3本の 計6本で、6通りの変換すべてをカバーできる。

type Format = 'env' | 'json' | 'yaml'

function convert(input: string, from: Format, to: Format): string {
  if (from === to) return input

  // 1. 入力形式 → 中間表現(JSオブジェクト)
  let parsed: unknown
  if (from === 'env') {
    parsed = parseEnv(input)
  } else if (from === 'json') {
    parsed = JSON.parse(input) as unknown
  } else {
    parsed = jsyaml.load(input)
  }

  // 2. 中間表現 → 出力形式
  if (to === 'json') {
    if (parsed === undefined) {
      throw new Error('入力を解析できませんでした(空のドキュメントまたは無効な形式)')
    }
    return JSON.stringify(parsed, null, 2)
  } else if (to === 'yaml') {
    return jsyaml.dump(parsed, { indent: 2, lineWidth: -1 })
  } else {
    return serializeToEnv(parsed)
  }
}

from === to のときは何もせず入力をそのまま返す。あとは parse 側と serialize 側がそれぞれ独立しているので、新しい形式(TOML とか)を足したくなっても parser と serializer を1本ずつ追加するだけで全組み合わせに対応できる。JSON と YAML は標準/ライブラリに parse・serialize が揃っているので、自前で書くのは .env だけ。

.env パーサを自前で書く

JSON は JSON.parse、YAML は js-yamlload で一発。問題は .env。これはブラウザ標準にもパーサが無いので自前で書く。フォーマット自体は素朴だが、地味に踏む点がいくつかある。

function parseEnv(text: string): Record<string, string> {
  const result: Record<string, string> = {}
  for (const line of text.split('\n')) {
    const trimmed = line.trim()
    if (!trimmed || trimmed.startsWith('#')) continue // 空行・コメント行はスキップ

    const eqIdx = trimmed.indexOf('=')
    if (eqIdx === -1) continue
    const key = trimmed.slice(0, eqIdx).trim()
    if (!key) continue

    let value = trimmed.slice(eqIdx + 1)
    // 値全体がクォートで囲まれていたら剥がす
    if (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'")
    }
    result[key] = value
  }
  return result
}

勘所は indexOf('=') で最初の = だけで分割すること。split('=') で割ってはいけない。.env の値には =: が普通に入る。

DATABASE_URL=postgresql://user:pass@localhost:5432/mydb?sslmode=require

この行を split('=') すると値の中の =require で割れてバラバラになる。「キーは最初の = の左、値はそれ以降全部」と決めて slice で切り出せば、値に何が入っていても壊れない。あとは空行・# コメント行をスキップし、値全体がクォートで囲まれていたら剥がしてエスケープを戻す。

.env へ書き出すときのクォート判定

逆方向(中間表現 → .env)はシリアライザを書く。.env は「1行1変数・値は文字列」というシンプルな形式だが、値に空白や特殊文字が入るときだけクォートで囲む必要がある。

function serializeToEnv(data: unknown): string {
  if (typeof data !== 'object' || data === null || Array.isArray(data)) {
    throw new Error('.env 形式はオブジェクト(キー・バリューのマップ)のみ変換できます')
  }
  const obj = data as Record<string, unknown>
  const lines: string[] = []
  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'object' && value !== null) {
      throw new Error(
        `.env 形式はネストされたオブジェクト・配列に対応していません(キー: "${key}")`
      )
    }
    const strValue = String(value ?? '')
    const needsQuote = /[\s"'\\=#]/.test(strValue) || strValue === ''
    lines.push(
      needsQuote
        ? `${key}="${strValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
        : `${key}=${strValue}`
    )
  }
  return lines.join('\n')
}

needsQuote の正規表現 /[\s"'\\=#]/ がクォート要否の判定。空白・ダブルクォート・シングルクォート・バックスラッシュ・=# のいずれかを含むか、空文字ならダブルクォートで囲む。これらをクォート無しで素のまま書くと、コメント開始(#)や区切り(=)と誤読されたり、別ツールの .env パーサで値が途中で切れたりする。

囲む場合はさらに、値の中の \\\ に、"\" にエスケープする。置換の順序が重要で、先にバックスラッシュをエスケープしてからダブルクォートを処理する。逆にすると、" をエスケープして入れた \ を後段の \\\ 処理がもう一度食ってしまって二重エスケープになる。

.env はフラット限定 — ネストは正直にエラーにする

JSON や YAML はネスト(階層構造)や配列を持てるが、.envフラットなキー・バリューしか表現できない。なので JSON/YAML 側にネストオブジェクトや配列があると .env には落とせない。

ここで無理やり parent.child=... みたいに平坦化する手もあるが、ツールとしては素直にエラーを投げることにした。どのキーで詰まったかを添えて返す。

if (typeof value === 'object' && value !== null) {
  throw new Error(`.env 形式はネストされたオブジェクト・配列に対応していません(キー: "${key}")`)
}

平坦化は「区切り文字をどうするか(_.?)」「配列のインデックスは?」で流儀が割れて、変換先のツールが同じ規則で読み戻せる保証がない。曖昧な変換で静かに壊れたデータを吐くより、「これは .env にできない構造です」と明示するほうが事故が少ない。トップレベルが配列やプリミティブのときも同様に弾く。

js-yaml の罠:lineWidth: -1 を入れないと長い値が折り返される

YAML 生成で一度ハマったのが、js-yamldump がデフォルトで 80桁を超える文字列を勝手に折り返すこと。長い接続文字列や URL が複数行に割れて出力され、別ツールで読むと意図しない改行が混ざる。

jsyaml.dump(parsed, { indent: 2, lineWidth: -1 })

lineWidth: -1 で折り返しを無効化(実質無制限)すると、どんなに長い値でも1行のまま出る。indent: 2 はインデント幅。設定ファイルの変換ツールでは、値を勝手に折り返さないことのほうが大事なので -1 一択。

もう一点、型の扱いが形式で変わるのも押さえどころ。js-yamlload は YAML の数値・真偽値・null・配列を JavaScript の型として復元し、JSON 変換時もその型が保たれる。一方 .env は値がすべて文字列なので、String(value) で文字列化される。「YAML だと DEBUG: true(真偽値)が、.env だと DEBUG=true(文字列)になる」のはこの違いによる。

まとめ

設定ファイルの相互変換ツールを作るときの要点。

  • N形式の総当たり変換は 中間表現(共通のJSオブジェクト)を1枚噛ませる。parse N本 + serialize N本で N×N 通りを賄える
  • .env パーサは 最初の = だけで分割split('=') は厳禁。値に = : が入る)。空行・#・クォートを処理
  • .env シリアライズは 空白・"'\=#・空文字でクォート。エスケープは \" の順
  • ネスト構造は .env に落とせないので 曖昧に平坦化せず明示的にエラー
  • js-yaml.dumplineWidth: -1 で折り返し無効化。型は YAML/JSON で保持、.env は全部文字列

中間表現を挟む設計は、フォーマット変換系のツールでは定番だが効果が大きい。形式が増えても実装が線形にしか増えないし、parse と serialize を別々にテストできる。.env まわりの細かいエスケープだけ丁寧にやれば、機密値をサーバーに送らず安全に変換できる。

ぱんだツールズ では他にもJSON整形・JSON差分・CSV変換・正規表現テストなど、開発者向けのブラウザ完結ツールを多数公開中。全部無料・登録不要・入力はサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?