ライブラリとして公開する
ritsu gen は、モジュールを言語ごとのパッケージにして書きます。このパッケージは、アプリケーションがそのまま動かすためのものです。モジュールのコードと、モジュールが require するすべてのモジュールのコード(依存のものは deps/<名前>/ の下)を持ち、ランタイムは ritsu が試した版に固定します。ritsu gen --library は、各パッケージをライブラリとして、それぞれのレジストリが受け取る形で書きます。npm のパッケージ、Python のディストリビューション、Go のモジュールです。ライブラリが持つのは、自分のモジュールのコードです。依存のモジュールにあるものは、その依存のライブラリから名前で import し、マニフェストでは、ライブラリとランタイムを範囲で求めます。使う側は、自分で生成しなくても、モジュールの規則、日付、帳簿、ワークフローを自分のコードから呼べます。--library を付けなければ、ritsu gen はこれまでと同じものを書きます。
ritsu gen |
ritsu gen --library |
|
|---|---|---|
| 使い道 | アプリケーション。そのまま動かす | ほかのコード。レジストリを通して使う |
| 依存のコード | 中に持つ(deps/<名前>/) |
依存のライブラリにあり、名前で import する |
| ランタイムと依存の版 | 試した版に固定 | 試した版からの範囲 |
| npm のパッケージ | "private": true |
公開する。dist/ に組んで出す |
このページの例は、ritsu のテストの三つのモジュール crates/ritsu/tests/projects/modules です。tax は VAT の税率を、pricing は送料と注文の合計を、shop は注文のフローとかごを持ちます。pricing の注文の合計は tax の税率を当て、shop は pricing で値段を付けます。
ライブラリの名前を書く
ライブラリの名前は、言語ごとに、ritsu.mod の library の行で決めます。
# The prices of the Acme shop: shipping fees, and order totals with the VAT rate of tax.
module example.com/acme/pricing
version 1.1.0
description "Shipping fees and order totals of the Acme shop"
license "MIT OR Apache-2.0"
source "https://github.com/acme/pricing"
library typescript "@acme/pricing"
library python "acme-pricing"
library go "example.com/acme/pricing-go"
require tax path "../tax"
library typescript|python|go "<名前>"は、その言語のレジストリでのパッケージの名前です。npm のパッケージの名前、Python のディストリビューションの名前、Go のモジュールのパスを書きます。Python では、ディストリビューションacme-pricingをacme_pricingとして import します。名前を小文字にし、-・_・.の連なりを一つの_にした名前です。版 2 からは、Go の決まりに従って Go のモジュールのパスの最後に/v2が付き、ritsu はそれを import するコードもそのパスで書きます。versionは、どの言語のライブラリの版にもなります。--libraryにはversionの行が要ります。Python の版(PEP 440)にできるプレリリースは、alpha、beta、rcに番号を付けたものだけです。1.0.0-rc.1は1.0.0rc1になります。1.0.0-dev.1なら、ritsu は止まります。descriptionとlicenseは、package.json と pyproject.toml に入ります。sourceは package.json のrepositoryに入り、npm の provenance がこれを確かめます(npm に公開する)。sourceは pyproject.toml の[project.urls]のRepositoryにも入ります。sourceはパッケージと一緒に公開されるので、ユーザー、パスワード、クエリを持つ URL は E302 になります。
生成する
$ ritsu gen --library --out ../out/pricing --lang ja
生成しました: ../out/pricing/typescript/rules/order_total.ts
…
生成しました: ../out/pricing/python/pyproject.toml
…
生成しました: ../out/pricing/go/go.mod
ritsu gen --library は、library の行のある言語ごとに、--out の下の typescript/、python/、go/ にライブラリを書きます。--target typescript|python|go を付ければ、その一つだけを書きます。ライブラリはモジュール全体(exclude の行が外すものを除く)なので、--library にはパスを渡せず、--name と --module も付けられません。ライブラリの名前は library の行から取ります。--check は、--library を付けないときと同じく、いまあるものとの違いを言います。
依存は依存のライブラリから読む
shop は pricing を require します。shop のコードは、pricing の規則を pricing のライブラリから、pricing の ritsu.mod が言語ごとに付けた名前で import します。
マニフェストに書くのは、ライブラリのコードが import するものだけです。pricing の注文の合計は tax の規則を当てますが、その規則はコードの中に組み込まれます。そのため pricing のコードは tax から何も import せず、pricing のマニフェストにも tax は載りません。
それでも、どの依存にも、その依存自身のライブラリか --embed が要ります。依存の ritsu.mod に、ある言語の library の行が無ければ、ritsu はその依存を挙げて止まります。
$ ritsu gen --library --out ../out/pricing --lang ja
エラー: 依存 `tax`(example.com/acme/tax)の ritsu.mod に、`library typescript` の行がありません。ライブラリの形のパッケージは、依存の部分を依存のライブラリから読みます。その ritsu.mod に `library typescript "<名前>"` を書くか、`--embed tax` を付けて、このパッケージの deps/ に入れてください
--embed tax を付けると、pricing のライブラリは tax のコードを deps/tax/ に持ちます。--library を付けないときのパッケージと同じ持ち方です。このとき、pricing を通してだけ tax に届く shop は、tax を pricing のライブラリから読み、shop で --embed tax を付けると止まります。ライブラリを通してだけ届くモジュールは、そのライブラリが持つからです。
範囲
ライブラリは、import するライブラリを、モジュールの木が選んだ版からの範囲で求めます。ランタイムは、ritsu が試した版からの範囲で求めます。範囲は次のメジャー版の手前までで、版が 0.y.z なら次のマイナー版の手前までです。
| 選んだ版 | npm | Python | Go |
|---|---|---|---|
1.1.0 |
^1.1.0 |
>=1.1.0,<2.0.0 |
v1.1.0 |
0.17.9 |
^0.17.9 |
>=0.17.9,<0.18.0 |
v0.17.9 |
0.0.3 |
>=0.0.3 <0.1.0 |
>=0.0.3,<0.1.0 |
v0.0.3 |
npm の ^0.0.3 は 0.0.3 しか受け取らないので、ritsu はこの範囲だけ上限まで書きます。Go には、ritsu は範囲ではなく版を書きます。Go は require を、そのモジュールが受け取るいちばん低い版として読み、各モジュールが求める版のうちいちばん高いものでビルドするからです。
--library を付けないパッケージには、ritsu は範囲ではなく、試した版そのものを書きます。アプリケーションは、試した版でそのまま動かすものだからです。ライブラリは、アプリケーションのいくつもの依存の一つです。二つのライブラリが同じランタイムを別々の版に固定すれば、Python は両方を入れられず、npm は二つのコピーを入れます。そこでライブラリは範囲を書き、範囲の中のどの版にするかは、アプリケーションのロックファイルが決めます。shop は pricing と Temporal の SDK を import します。
"dependencies": {
"@temporalio/activity": "^1.24.0",
"@temporalio/client": "^1.24.0",
"@temporalio/common": "^1.24.0",
"@temporalio/worker": "^1.24.0",
"@temporalio/workflow": "^1.24.0"
},
"peerDependencies": {
"@acme/pricing": "^1.1.0"
},
require (
example.com/acme/pricing-go v1.1.0
go.temporal.io/api v1.63.5
go.temporal.io/sdk v1.49.0
)
npm では、ritsu は、ライブラリが import するライブラリを peerDependencies に書きます。アプリケーションが、自分の分も、pricing を import するどのライブラリの分も、一つの pricing で済ませるためです。
単位の型はライブラリをまたいで一つ
shop のお金の額は、pricing の型です。shop の単位のモジュールは、型を pricing のライブラリから読み込み、同じ名前で公開します。
import pricinggounits "example.com/acme/pricing-go/units"
// GBPc is an amount of money[GBPc], the type of example.com/acme/pricing-go/units.
type GBPc = pricinggounits.GBPc
shop のコードが返す額は、変換せずに pricing の規則に渡せます。それでも、各言語の型の検査は、ほかの単位の額を通しません。依存の関係の無い二つのライブラリが同じ単位を持っていれば、両方を import するパッケージの中に、その単位の型が二つできます。そのため ritsu は止まります。片方のライブラリをもう片方に依存させるか、--library を付けずに生成してください。
レジストリごとの中身
npm
{
"name": "@acme/pricing",
"version": "1.1.0",
"description": "Shipping fees and order totals of the Acme shop",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/acme/pricing.git"
},
"type": "commonjs",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
},
"files": [
"dist"
],
"scripts": {
"prepare": "tsc -p tsconfig.json"
},
"devDependencies": {
"typescript": "7.0.2"
}
}
- 公開するのは、組んだあとのライブラリです。npm は、ディレクトリを入れるときと、パッケージにまとめる前や公開する前に、
prepare(横の tsconfig.json でのtsc)を走らせます。パッケージに入るのはdist/(JavaScript とその宣言)だけです。Node は node_modules の下の.tsの型を外さないので、TypeScript のままでは使えません。 - ライブラリは CommonJS で、ES モジュールからも import できます。生成したコードの相対の import には拡張子が無く、Node の ES モジュールでは拡張子が要るからです。
descriptionは、モジュールのdescriptionの行です。npm は、これをパッケージのページに出します。- 最初のキー
"//"(上の例では省きました)には、ritsu が生成したファイルであることを示す一文Code generated by ritsu <版>. DO NOT EDIT.が入ります。JSON には注が書けないので、キーに書きます。npm はこのキーを読み飛ばします。ritsu genは、ほかの生成したファイルでは頭の注にあるこの一文を、package.json ではこのキーから読みます。 optional-modules.d.tsは、コードが頼まれたときだけ読み込む SDK を、型の無いモジュールとして宣言します(ワークフローのエージェントは、モデルの SDK をawait import(…)で読み込みます)。そのため、ライブラリはそれらを入れずに組めます。使うアプリケーションが、頼むものだけを入れます。
PyPI
[project]
name = "acme-pricing"
version = "1.1.0"
description = "Shipping fees and order totals of the Acme shop"
license = "MIT OR Apache-2.0"
requires-python = ">=3.10"
dependencies = [
]
[project.urls]
Repository = "https://github.com/acme/pricing"
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
requires-pythonは、--libraryを付けないパッケージと同じです。setuptools は、77 からlicenseを SPDX の式(PEP 639)として読みます。Repositoryは、モジュールのsourceの行です。ritsu は、モジュールにsourceの行があるときだけこれを書きます。そのリポジトリの GitHub Actions から信頼された発行者(PyPI に公開する)としてアップロードすると、PyPI はこの URL を確かめたものとして示します。
Go
goの行は、サポートが続いている Go のリリースのうち、いちばん古いものです(Go は新しい二つのメジャーリリースをサポートします)。ライブラリが require するランタイムのモジュールがそれより新しい版を求めれば、ritsu はその版を書きます。- ritsu は go.sum を書きません。ライブラリを使うコードが、
go getを走らせたときに、自分の go.sum にモジュールのハッシュを書きます。 - 生成したディレクトリは、それだけではビルドできません。go.mod は、ライブラリが import するモジュールを書きますが、そのモジュールがさらに import するものは書かないからです(Go は両方を求めます)。それでも、そこで
go mod tidyを走らせないでください。go mod tidyは残りを go.mod に書き足します。するとritsu gen --checkは go.mod が古いと言い、次のritsu genは書き足したものを取り除きます。タグを付ける前にライブラリをビルドして確かめるときは、別のモジュールからそのライブラリを require してreplaceで生成したディレクトリを指し、そのモジュールでgo vet -mod=mod example.com/acme/pricing-go/...を走らせてください。 - ライブラリ自身のリポジトリの CI では、
go build -mod=mod ./...(またはgo vet -mod=mod ./...)で組んでください。Go は、組むのに要るものを CI の手元の go.mod と go.sum に書き足します。そのどちらもコミットしないでください。
公開する
ライブラリは、モジュールが require する順に公開してください。tax、pricing、shop の順です。ライブラリは依存を、木が選んだ版から求めるので、その版が先にレジストリになければなりません。npm は、ライブラリを組むときにもそれを入れます。
下のワークフローは、モジュールのリポジトリ(source の行のもの)で、モジュールの版のタグを付けたときに走ります。どちらも、ritsu を GitHub Action で入れ、その場でライブラリを生成します。ritsu のテストは、ライブラリを各レジストリの道具と同じやり方で組みますが、ワークフローそのものは走らせません。
npm に公開する
GitHub Actions から OIDC のトークン(id-token: write)で公開すると、npm はパッケージに provenance を付けます。provenance は、パッケージを組んだリポジトリとワークフローを示す、署名つきの記録です。npm は、package.json の repository がそのリポジトリと一致すること(大文字と小文字の違いも認めません)を求めます。ritsu は、repository を source の行から、npm が公開するときに直したあとの形で書きます。GitHub、GitLab、Bitbucket、sourcehut のリポジトリと gist の URL は、npm の形になります。たとえば https://github.com/acme/pricing は git+https://github.com/acme/pricing.git になります。npm はこの形をそのまま公開し、package.json を直したという警告も出しません。
npmjs.com でパッケージに信頼された発行者(trusted publisher)を設定すれば(npm 11.5.1 以上、Node 22.14.0 以上)、ワークフローにトークンは要らず、npm が provenance を自分で付けます。
name: Publish the npm package
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: i2y/ritsu@v0.37.0
- uses: actions/setup-node@v6
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false
- run: ritsu mod fetch
- run: ritsu gen --library --target typescript --out lib
- run: npm install
working-directory: lib/typescript
- run: npm publish
working-directory: lib/typescript
ritsu mod fetch は、ロックファイルが固定するモジュールのうち、まだ手元に無いものを取ってきます。npm install は、TypeScript と、パッケージが import するライブラリを入れ、dist/ を組みます。npm publish は、もう一度組んでから公開します。信頼された発行者の代わりにトークンを使うなら、最後の手順は次のとおりです(パッケージを初めて公開するときは --access public が要ります)。
- run: npm publish --provenance --access public
working-directory: lib/typescript
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
PyPI に公開する
PyPI の信頼された発行者は、トークンなしで、ワークフローからリリースを受け取ります。PyPI で、プロジェクトに発行者を設定してください。設定に書くのは、リポジトリ、ワークフローのファイル名、環境の名前 pypi です。PyPI にまだ無いプロジェクトなら、保留中の発行者(pending publisher)が、最初のアップロードのときにプロジェクトを作ります。Python Packaging Authority のガイドのとおり、一つのジョブがディストリビューションを組み、OIDC のトークンを受け取る別のジョブだけが公開します。
name: Publish the Python package
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: i2y/ritsu@v0.37.0
- uses: actions/setup-python@v6
with:
python-version: "3.x"
- run: ritsu mod fetch
- run: ritsu gen --library --target python --out lib
- run: python3 -m pip install build --user
- run: python3 -m build lib/python
- uses: actions/upload-artifact@v5
with:
name: python-package-distributions
path: lib/python/dist/
publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/acme-pricing
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
このアクションは、ディストリビューションと一緒に、その attestation(どこで組んだかを示す署名つきの記録)も標準でアップロードします。
Go で公開する
Go は、モジュールのパスにあるリポジトリから、版のタグでモジュールを取ります。アップロードするものはありません。そのリポジトリのクローンにライブラリを生成し、そこでタグを付けてください。下の lib/go は、example.com/acme/pricing-go のリポジトリのクローンです。
$ ritsu gen --library --target go --out lib
$ cd lib/go
$ git add -A
$ git commit -m "pricing-go v1.1.0"
$ git tag v1.1.0
$ git push origin HEAD v1.1.0
$ GOPROXY=proxy.golang.org go list -m example.com/acme/pricing-go@v1.1.0
最後のコマンドは、Go の公開の手引きと同じく、Go のモジュールプロキシにその版を取りに行かせます。モジュールがリポジトリのサブディレクトリにあるときは、サブディレクトリの名前がモジュールのパスに入り、タグの頭にも付きます。リポジトリ example.com/acme/pricing のディレクトリ go にあるモジュール example.com/acme/pricing/go のタグは、go/v1.1.0 です。一度 push したタグは動かさないでください。Go は、ある版を取るたびに最初に取ったものと照らし合わせ、違えばセキュリティのエラーにします。
レジストリの道具が書くもの
ライブラリの横には、tsc が dist/ を、setuptools が build/ と <名前>.egg-info/ を、Go が go.sum を書きます。--library を付けたときの ritsu gen --check と次の ritsu gen は、ライブラリのディレクトリの直下にあるこれらを、そのまま残します。tsc と setuptools は、ritsu が生成したことを言うファイルの最初の行もコピーしますが、それでも残します。
ライブラリではできないこと
- import するライブラリの規則に寄与すること。ライブラリのコードは、生成したときにモジュールにあった寄与で組まれていて、使う側のコードからの寄与は受けられません。パッケージの寄与が、ライブラリの持つ規則に寄与していれば、ritsu は止まります。
--libraryを付けずに生成して、寄与を合わせた規則をパッケージに持たせるか、寄与をライブラリの側のモジュールに移してください。 - ライブラリをまたいで kaname の帳簿の型を渡すこと。kaname が帳簿に渡す型(
glue/books/)は、パッケージが自分で持ちます。同じ帳簿を持つライブラリは、その型を別に持ちます。Python と Go はそれらを別の型として扱い(TypeScript は型を形で比べるので、一つの型として扱います)、KanameErrorもライブラリごとに別のものになります。モジュールの間で帳簿の値を渡すなら、--libraryを付けずに、一つのパッケージとして生成してください。 - 依存の関係の無い二つのライブラリに、同じ単位を持たせること。理由と代わりの手は、単位の型はライブラリをまたいで一つに書きました。