コンポーネントパッケージ
管理画面、専用テーブルを持つミドルウェア、スタイル付きのコンポーネント一式。 こうした機能を別のプロジェクトへコピーすると、同じ実装を 2 か所で保守することに なります。片方に加えた変更は、やがてもう片方へ届かなくなります。
コンポーネントパッケージは、その機能をふつうの Go モジュールとして公開した ものです。利用する側が書くのは 1 か所だけです。
[[packages]]module = "example.com/widget"インストールに必要な宣言はこれだけです。pw generate がパッケージをリンクするための
import を書き、pw migrate up がテーブルを作ります。パッケージのソースは
アプリケーション側へコピーされません。
手を出さないほうがいい場面
Section titled “手を出さないほうがいい場面”共有したいものが素の Go だけ — .pw.html もマイグレーションもブラウザ資産も
無い — なら、ふつうの Go モジュールとして公開してください。ここから先は全部、
Go モジュール単体では運べないものを運ぶために存在します。それを何も持たない
モジュールに package セクションを書いても、誰も読まないマニフェストが増えるだけです。
もう一方の境界はもっと硬いものです。あるモジュールのコンポーネントを、別の
モジュールのテンプレートから呼ぶことはできません。 .pw.html が到達できる
のは自分と同じ生成単位で宣言されたコンポーネントだけなので、コンポーネントを
公開するためのパッケージは今日のところ作れません。ハンドラ、pages/ ツリー、
ミドルウェア、スキーマ、ブラウザ資産を配るパッケージは動きます。
パッケージ側
Section titled “パッケージ側”パッケージは kind = "package" を持ち、エントリポイントを持ちません。main を
定義するのは、それを import するアプリケーションです。
[project]name = "widget"kind = "package"
[package]module = "example.com/widget"summary = "自前のストレージを持つメモウィジェット"assets.declared = trueroutes.register = "Register"
[package.migrations]dir = "migrations"stem = "widget"engines = ["sqlite"]
[package.generated_with]pw = "v0.4.0"tinybind = "v0.3.5"
[generate]handlers = ["."]templates = ["."]queries = []config = []Go 側は、モジュールの識別子と埋め込んだファイルを init で登録します。実際に
リンクしたバイナリだけが代償を払う形です。
package widget
import ( "embed" "io/fs"
"github.com/shibukawa/popcornweb/pw")
//go:embed assets migrationsvar files embed.FS
const version = "v0.1.0"
func init() { pw.RegisterPackage(pw.Package{ Module: "example.com/widget", Version: version, Assets: mustSub(files, "assets"), Migrations: mustSub(files, "migrations"), MigrationStem: "widget", })}
func mustSub(root fs.FS, dir string) fs.FS { sub, err := fs.Sub(root, dir) if err != nil { panic("widget: " + err.Error()) } return sub}登録が運ぶのは識別子とバイトだけです。ミドルウェアは今までどおり
pw.RegisterExtension、設定はすでに自分で登録している生成済みバインディング、
ルートはアプリケーションが呼ぶ公開関数 Register を通ります。ここで登録したもの
が単独でリクエストに応えることはありません。
パッケージの Go がモジュールルートより下 — たとえば ui/ ディレクトリ — にある
場合は、package.import でそのパスを指す必要があります。利用側の生成された
ブートストラップが import するのはこのパスだからです。
[package]module = "example.com/widget"import = "example.com/widget/ui"書き忘れると、Go の無い場所を import することになり、利用側のビルドは Go ツールの
no required module provides package で落ちます。このメッセージは宣言のことも
このキーのことも教えてくれません。そうなる前に
pw doctor が PW0144 として報告します。
生成はここで、一度だけ走る
Section titled “生成はここで、一度だけ走る”パッケージでの pw generate は、アプリケーションでの実行とまったく同じものです。
同じジェネレータが同じオプションで、.pw.html をソースの隣の _pw_gen.go に
コンパイルします。パッケージ用のモードはありません。
ひとつだけ引っかかる点があります。生成コードはテンプレートランタイムを直接
import するので、一度生成するとパッケージの go.mod は tinybind-go を直接
依存として要求します。最初の生成のあとに go mod tidy を走らせれば自動で入り
ますが、これは実装の詳細ではなく、公開する依存集合の一部です。import している
のは、あなたがコミットした生成物そのものだからです。
生成ファイルはここではコミットする
Section titled “生成ファイルはここではコミットする”反転するのはこの 1 点だけです。アプリケーションでは _pw_gen.go は git が無視し、
ビルドのたびに作り直します。パッケージではコミットします。利用側のビルドは
go build であり、その生成器は依存先を読まないからです。module cache は読み取り
専用ですし、生成のスコープはプロジェクト相対のディレクトリ一覧だという構造上、
依存先はもともと範囲の外にあります。
だからパッケージのリポジトリは **/*_pw_gen.go の無視ルールを持ちません。
そしてリリースの門はこれになります。
pw check古い生成物を載せたタグは、導入したすべてのプロジェクトでコンパイルに失敗します。 これは手に入る中でいちばん大きな音の失敗で、しかも修復経路がありません。だから このチェックはリリース手順書ではなく CI に置くものです。
型付きクエリは持ち出せない
Section titled “型付きクエリは持ち出せない”パッケージでは generate.queries は空でなければならず、pw generate は空でない
ものを拒否します。.pw.sql は公開時に選んだ 1 つのエンジンのプレースホルダ構文
にコンパイルされますが、パッケージは利用側のエンジンを知りようがありません。
出荷すれば、どこでもコンパイルは通り、導入したプロジェクトの半分で最初の呼び出し
で落ちるクエリができあがります。クエリは手で書くか、*sql.DB を受け取って
アプリケーションに持たせてください。
pw add example.com/widgetこのコマンドが書くのは go.mod の require と [[packages]] の 1 エントリで、
あとは残りの手順を表示するだけです。ウィザードもレビュー画面もありません。
何もコピーしないからです。書き込む 2 行が編集のすべてで、手で書いても同じです。
なぜ go.mod だけではなく宣言が要るのか。go.mod が言うのはモジュールが
利用可能だということで、そこには頼んだ覚えのない推移的依存も全部含まれます。
宣言が言うのは使うつもりがあるということです。生成器が必要としているのは後者
です。すでに管理しているブートストラップファイルに、宣言されたパッケージごとに
blank import を 1 行ずつ書き出すのですが、依存グラフから意図を読み取ることは
できません。行を消せば、次の生成で import も消えます。
package セクションを持ちながら宣言されていないモジュールは、ふつうの Go の依存の
ままです。pw doctor はそれを報告します — 推移的依存が
資産とスキーマを持ち込んでくるのは、名指しする価値のある驚きだからです — が、
リンクは何もしません。
go mod tidy との順序に注意
Section titled “go mod tidy との順序に注意”宣言したばかりのパッケージは、生成が import を書くまで誰も import していません。
そして go mod tidy は、誰も import していない require を削除します。順序はこう
です。
go get example.com/widget # pw add がやってくれることでもあるpw generate # blank import を書くgo mod tidy先に tidy すると require が消えるので、次の pw generate は
packages "example.com/widget": not in the module graph で止まります。pw add
はこの順序で動きます。罠になるのは、宣言を手で書いたときです。
宣言が肩代わりできない 1 行
Section titled “宣言が肩代わりできない 1 行”ルートを提供するパッケージは Register 関数を公開し、あなたのエントリポイントが
それを呼びます。
mux := pw.NewServeMux()widget.Register(mux)これは意図的です。ルートを自動で流し込む仕組みはフレームワーク側のルート登録 API
になりますが、拡張のモデルはそれを持っていません。そしてマウント先は、
アプリケーションが意見を持っている唯一の貢献です。pw add がこの呼び出しを
表示します。名前はマニフェストの routes.register から取ります。
マイグレーションはコピーされずに届く
Section titled “マイグレーションはコピーされずに届く”パッケージはそれぞれ自分のマイグレーションストリームを持ちます。番号はあなたの ものとは独立で、記録するバージョンテーブルも別です。
pw migrate up宣言されたパッケージのマイグレーションは、すべてアプリケーション側のものより先に適用されます。 パッケージどうしの順序は Go のモジュールグラフから来ます。パッケージは見たことの ないテーブルを参照できず、あなたはパッケージのテーブルを参照できる。import の 向きと参照の向きは、もともと一致しているわけです。グラフから導くのはこれだけです。
適用の前に、保留中のバージョンが、それを持つパッケージ名とともに表示されます。 この一覧は重要です。依存パッケージのマイグレーションファイルはリポジトリにコピーされないため、 依存先がこれから自分のデータベースに何をするのかを、する前に見られる唯一の場所 だからです。
取引の内容ははっきり書いておきます。マイグレーションをプロジェクトにコピーすれば、
実行するリポジトリの中ですべての文がレビューを受けます。パッケージ側に置いたまま
にすると、go get -u と pw migrate up だけでアップグレードが終わり、誰かが
忘れうる 2 つ目のコマンドが存在しなくなります。アップグレードのたびに再実行が
必要なコピーは静かにずれていきますし、スキーマについては、見えにくいことより
黙ってずれることのほうが悪い。文が見たければ pw migrate up が表示しますし、
バイト列は go.sum が固定しています。
pw add auth が入れるフレームワークの機能はこの影響を受けません。あちらは今まで
どおりあなたのマイグレーションディレクトリに書かれます。運用者が一度ウィザードを
回して入れたもので、下でバージョンが動くモジュールが存在しないからです。
設定にセクションは要らない
Section titled “設定にセクションは要らない”パッケージの生成済み設定バインディングは、リンクされた時点で自分の既定値を登録
します。環境ファイルに何も書かなくても動きます。セクションを足すのは、何かを
上書きしたいときだけです。pw add は登録されるセクション名を表示しますが、
書き込みはしません。
ブラウザ資産
Section titled “ブラウザ資産”パッケージが埋め込んだファイルは、予約された内容アドレスのパスから配信されます。
/_pw/pkg/<digest>/<name>識別子はバイト列のダイジェストです。同じファイルを配る 2 つのパッケージは 1 つの
URL になり、1 バイト変われば URL が変わり、immutable のキャッシュヘッダは嘘に
なりません。あなたの public/ ツリーには何も書かれず、リクエスト時にファイル
システムを読むこともありません。ファイルシステムを持たない TinyGo ターゲットでも
動くのはそのためです。
ドキュメントシェルから参照するには、URL を問い合わせます。
url, ok := pw.PackageAssetURL("example.com/widget", "widget.js")これは暫定の形で、限界についても正直に書いておきます。パッケージが数個を超えると
破綻しますし、アップグレードでリンクしていない資産が増えたときに静かに壊れます。
コンポーネント自身が必要な資産を宣言し、URL はフレームワークが与える — その形は
tinybind-go 側の作業待ちです。
バージョンのずれは実在する
Section titled “バージョンのずれは実在する”アプリケーションはビルドのたびに再生成するので、生成器とランタイムは 1 つの依存の
1 つのバージョンです。パッケージは公開時点で生成物を凍結し、ランタイムは
go.mod がビルド時に選び、Go は両者の高いほうを取ります。つまり実際に動く組み
合わせは、作者が試した組み合わせとは限りません。
package.generated_with が、生成物を作ったバージョンを記録します。pw doctor は
それをプロジェクトが解決するバージョンと比べ、自分より新しいフレームワークで
生成されたパッケージを報告します。まだ存在しないランタイムのエントリを呼んでいる
可能性があるからです。それはコンパイルエラーになりますが、先に名指ししておくほう
がコンパイラから読み取るより安いというだけの話です。
古い生成物をコミットしたまま公開すると、利用側の go build で失敗します。
パッケージ名は表示されます。利用側からの修復経路はありませんし、あってはいけません。
依存先を再生成することは、作者が試したことのないバージョンでそれを再公開することに
なるからです。
2 つのパッケージが同じマイグレーション stem を持つことはできません。 登録時にも
pw doctor でも拒否します。バージョンテーブルを共有すると、それぞれが相手の適用済み
バージョンを自身の適用済みバージョンと誤認してしまうからです。
package.components.exported は拒否されます。 このキーは、モジュールをまたぐ
コンポーネントが実現したときの置き場所として存在します。今日書けばロードエラーで、
守れない約束にはなりません。
パッケージのダウングレードはスキーマのダウングレードではありません。 マイグレーションが少ないバージョンに戻すと、ソースの無い適用済みバージョンが残り ます。これは報告されるだけで、自動では戻しません。
ストリームの読み手は 2 つあり、一致していなければなりません。 pw migrate は
モジュールディレクトリからマイグレーションファイルを読みます。アプリケーションの
バイナリ無しで適用するので、別プロセスの埋め込みには手が届かないからです。
プロセス内の経路と pw test は埋め込みを読みます。同じファイルです — //go:embed
のパターンが取りこぼしていない限りは。取りこぼしを見つけられるのは、あなた自身の
リリースチェックだけです。
リファレンス
Section titled “リファレンス”両側のキーはすべて
ビルド設定にあります。コマンドは
pw add、pw generate、
pw migrate、
pw doctor です。
