技術書にコードを載せるとき、いちばん怖いのは写し間違いです。本文のコードが、実際に動いているコードと、少しずつずれていく。その事故を防ぐために、本の原稿に、実際のファイルを、機械的に取り込むビルドを作りました。仕組みと、実際に踏んだ落とし穴をまとめます。
やりたかったこと
- 原稿(Markdown)には、「ここに、このファイルを入れる」という目印だけを書く
- ビルドすると、目印が、実ファイルの中身に置き換わる
- 元のプロジェクトが進んでも、本の内容が勝手に変わらない(コミットを固定する)
目印の書き方
{{file:app/Services/Foo.php}} ← ファイル全文
{{grep:routes/web.php::callback}} ← 正規表現に合う行だけ(0件ならビルド失敗)
{{range:config/services.php::38-45}} ← 行範囲
{{file@abc1234:app/Services/Foo.php}} ← コミットを個別に指定
出力されるのは、Zennのコードブロック(ファイル名つき)です。
```php:app/Services/Foo.php
<?php
...
```
grepは、該当が0件ならビルドを失敗させるようにしました。元のコードが変わって、目印が空振りしたことに、気づけるためです。
コミットを固定して読む
ファイルは、作業フォルダの今の内容ではなく、gitのコミットを指定して読みます。
const { execFileSync } = require('child_process');
function readCode(rel, ref) {
const text = execFileSync('git', ['-C', REPO_ROOT, 'show', `${ref}:${rel}`], {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
});
return text.replace(/\r\n/g, '\n').replace(/\s+$/, '');
}
git show <コミット>:<パス>は、そのコミット時点のファイルを、そのまま出力します。本の設定ファイル(book.json)に、"codeRef": "abc1234"と書いておけば、元のプロジェクトが、その後どれだけ変わっても、本は同じ内容でビルドされます。
私の場合、本を公開した直後に、元のプロジェクトに機能が足されました。コミットを固定していたので、すでに売っている本の内容が、勝手に変わることがありませんでした。新しい内容は、別の章として、新しいコミットを指定して足す形にしました。
ビルドの結果が、前と同じか確かめる
ビルドの仕組みを直したときに、「前と同じ本ができるか」を、確かめます。
# 公開済みの内容(gitのHEAD)を取り出して、ビルド結果と比べる
git archive HEAD books/my-book | tar -x -C /tmp/before
node build.js
diff -r --strip-trailing-cr /tmp/before/books/my-book books/my-book
差分が0なら、ビルドの変更は、本の内容に影響していません。--strip-trailing-crは、Windowsの改行(CRLF)と、gitが出す改行(LF)の違いで、全行が違うように見えるのを避けるために付けています。
引っかかった点
1. 正規表現の記号が、書き換えのたびに消える
ビルドスクリプトを、シェルのヒアドキュメントの中でJavaScriptの文字列として組み立てて書き換えたところ、正規表現の\nや\.のバックスラッシュが消えて、スクリプトが壊れました。同じ事故が何度か起きたので、スクリプトは、エディタ(ファイル編集ツール)で直接書くことにしました。
2. Windowsでは、作業中のフォルダを消せない
ビルドは、出力先のフォルダを一度空にしてから、書き出します。ところが、そのフォルダをカレントディレクトリにしているプロセスがあると、WindowsはEPERMで、フォルダごとの削除を拒否します。
// 出力先は消さず、中身だけ消す
fs.mkdirSync(OUT, { recursive: true });
for (const name of fs.readdirSync(OUT)) {
fs.rmSync(path.join(OUT, name), { recursive: true, force: true });
}
3. コードに、自社の呼び名が出てくる
元のコードのコメントに、社内の呼び名が含まれていました。本には出したくなかったので、ビルドのときだけ置き換える設定(codeReplace)を入れました。
{ "codeReplace": { "社内の呼び名": "中継側", "ClassPrefix": "Relay" } }
コードの中と、ファイル名の表示の、両方に適用します。本文に、置き換えた旨を書いておくことも、忘れないようにしました。
まとめ
- 原稿には目印だけを書いて、ビルドで実ファイルに置き換える。手で写さない
-
git show <コミット>:<パス>で、コミットを固定する。元のコードが進んでも、売った本が変わらない -
grepの目印は、0件ならビルドを失敗させる - ビルドの仕組みを変えたら、公開済みの内容と、diffで比べる
- Windowsでは、出力先のフォルダ自体は消さず、中身だけを消す
この記事はZennにも投稿しています。