ビルド時の参照補完機能とベースパスの実装

PR13を出しました。

今回の新機能実装

  1. コピー対象のファイルをホワイトリスト形式からブラックリスト形式にする
  2. デバッグサーバー・ビルド時のベースパスに対応
  3. aタグのsrc、metaタグのhref、scriptタグのsrcのURLの自動補完機能

細かいバグフィックス・リファクタリング

  1. Content-Typeにmime typeを指定
  2. URLのnormalizeをtransformer以下でしない

実装の話

コピー対象のファイルをホワイトリスト形式からブラックリスト形式にする

前回問題になっていたやつですね。ビルド時にフォントのファイルがコピーされずに取り残されてしまって、 これをホワイトリスト形式で対応していたら数が増えていくばかりなので、ブラックリスト形式に切り替えました。

Optionsの見直し

その前に、設定ファイル or CLIの引数で引数で自由選択したかったのでOptionの見直しをしました。 変数名もoptsだったりoptionsだったりoptionsだったりしたので、この際に調整を行いました。

cliはmeowを使っているのですが、型はおまけ程度のもので、そこまで協力なものありません。なので、anyを扱っているような感じで取り扱います。 設計は次の通りです。

[CLIからの入力] --> [parser] --> [規格化されたパラメーター]

[規格化されたパラメーター]はインターフェースを切っておきます。 cliの用途は2分できて、開発用ビルド用の2つです。(もしかしたら、デプロイ用も出来るかもしれません)

interface Options {
  develop?: DevelopOption; // 開発用
  build?: BuildOption;     // ビルド用
}

整理してみると、開発用とビルド用はほとんど同じ設定値を持っていました。 これをCommonOptionとして切り出してextendsさせておきます。

interface CommonOption {}

interface DevelopOption extends CommonOption {}

interface BuildOption extends CommonOption {}

また、命名において、複数形と単数形の使い方を明確にするようにしました。 I/FのOptions開発用ビルド用の2つを持っているため、複数に。 CommonOptionDevelopOptionBuildOptionは固有のドメインにおいてのオプションなので単数にしました。 変数名はI/Fの複数、単数に依存するようにし、略称を使わずに記述するようにしました。

// bad
function (opt: BuildOption);
// good
function (option: BuildOption);
// bad
function (opts: Options);
// good
function (options: Options);

他にも組み合わせがあると思いますが、方針はこれで決めです。

オプションの優先度

外部から指定できるオプションの優先度も決めて置く必要があります。 外から注入できる方法は3パターンあります。

  • cliの引数
  • package.jsonの"custom-site": {}部分
  • config.json

実装はおいおい見直して行こうかなぁ、という気分ですが、優先度と取扱方法は決めておくことにしました。 揮発性の高い書き方、ほとの依存度が高くなる場合は利用の優先度を下げていくようにすると

[CLIの引数] < [package.jsonのcustom-siteパラメータ] < `config.json`

という順番になりました。取り扱い方法は、優先度の高いものが、優先度の低いものをオーバーライドする形式を取りました。 spread operatorを利用するとこれが一瞬ででき、

{
  ...cliOption,
  ...pkgJsonOption,
  ...ConfigJsonOption,
}

といった具合に実装したかったのですが、config.jsonは別の所で管理してしまっていてしまったー、という感じです。 config.jsonはheadタグのデフォルト設定値などを取り扱っており、別のPRで修正しようと思います。

ブラックリストの実装

オプションの整理ができたので、I/Fを信じて実装箇所まで引っ張り回すだけです。 ブラックリストにも、拡張子やファイル名、正規表現が考えられますが、今回は拡張子のみに対応しました。

const isNotBlacklistPattern = (src: string, blacklist: CommonOption["blacklist"]) => {
  return !blacklist.extensions.includes(path.extname(src));
};

ブラックリストに該当しない場合のみ、コピー対象とするようにできました。

デバッグサーバー・ビルド時のベースパスに対応

Github Pagesにデプロイしてみたところ発覚したんですよね。 https://example.comの要な場合はPRを入れる前の実装でよかったのですが、 https://example.com/a/b/cのようにデプロイ先のベースパスが異なる場合はcssやjs、画像、aタグのリンクまで壊れてしまうんです。

で、ベースパス対応をするようにしました。これが思った以上に手ごわかったです。

  • ベースパスのついたURLを内部でローカルディレクトリにあるファイルパスに変換
  • 静的なウェブサーバーのURL変換に対応
    • https://example.com/hoge = https://example.com/hoge/ = https://example.com/hoge/index.html
  • ビルド後の成果物内で簡易ウェブサーバーを立ち上げた場合でもちゃんと使えること

確認する対象がデバッグサーバー、ビルド後の簡易サーバーの2つで挙動を合わせる必要がありました。 (まだ完全にカバーできていないが...)

ここはまだゴリ押し実装をやっていて、

  1. ベースパスのついた状態でローカルのファイルを探索
  2. ベースパスを取り除いた状態でローカルのファイルを探索 <-- ココを実装
  3. ジェネレータで生成されたPageObjectから探索
  4. ない場合はエラーを返す

といった感じです。serverのコードに張り付いてしまっているのでテスタブルになるように分割しないとなー(棒)と思っています(やれ)。

aタグのsrc、metaタグのhref、scriptタグのsrcのURLの自動補完機能

ベースパス対応に伴って自動変換するようにしました。ここに一つのルールを決めておく必要がでてきました。 当初は./から始まる参照を許容していたのですが、次のような理由から、すべての参照は絶対パスで参照するようにししました。

  1. Markdownから変換されたHTMLファイルのリンクが絶対パス出会ったほうが挙動の確認が明確
  2. 実装が楽になる

相対パスは脳内で変換しないといけないので、人間がやると間違える!だからレンダラー側で計算してもらおうという魂胆です。

これを行うにはいくつかのタグ内を書き換える必要があります。 <meta><link><script><a>が主に利用されているタグですが、 <head>内にあるか<body>内にあるかで実装方法が異なっていました。

<head>内にある参照の書き換え

文章を書いているファイルのヘッダーの設定値や、config.jsonなどに参照が記述されてるので、 それらのファイル読み込み語にベースパスを追加するように実装しました。

[draft.mdx] --> [読み込み] --> [処理] --> [レンダリング]
[config.json] --> [読み込み] --> [処理] --> [レンダリング]

図は楽ですが実装がやや複雑になってきたのでパイプライン構造にしたい。

<body>内にある参照の書き換え

これはなかなか大変です。が、このジェネレータは簡単です。 カスタムコンポーネント機能を利用することが可能で、MarkdownからHTMLに変換するときに利用するaタグの変換を自分で定義することが可能です。

const createBodyContent = transformRawStringToHtml({
  customComponents: {
    a: (props: JSX.IntrinsicElements["a"]): React.Element<any> => { /* 実装 */ };
  },
  props: {},
});

内部のhrefプロパティに対してベースパスを追加するようにすれば実装が完了です。 ただし、/^https?/にマッチするようなパターンはそのまま利用するようにしています。

今回追加した実装によって、Markdown内の書き方がかなり楽になります。

index.mdx                      [1]
article/auto-link.mdx          [2]
article/copy-assets-file.mdx   [3]

とファイルがあった場合、[2]から[1]へ、[2]から[3]へリンクしたい場合

aritcle/auto-link.mdx内

[2 --> 1](../)
[2 --> 3](./copy-assets-file)

という書き方でレンダリング時に保管されるようになります。つまり、Markdownのファイルツリーからリンクを使えるようになるので、 エディターの恩恵(パス補完とか)を受けることが可能となります。

実際のリンクはこちら

次やること

次はカスタムテンプレート機能の実装をしていこうと思います。

どうやろうかな〜。

関連記事