npmに公開していたパッケージをjsrにもpublishしてみた
DenoがJSRというnpmとは別のjs/ts用のレジストリを公開しました。DenoではなくNode.jsを対象としたTypeScriptのパッケージもJSRにpublishできるとのことなので、試しに自分がnpmに公開していたパッケージをJSRにも公開してみました。
最初にゴールを書いておきます。この記事はこれらを達成するために行った作業を記録したものです。
- 今までnpmに公開していた junit2json をJSRにも公開する
- npm -> JSRへの乗り換えではなく、両方とも同じバージョンで公開する
- JSRへのpublishもnpm同様にGitHub Actionsで自動化
- JSR公開時にはprovenanceを付与する
JSRのパッケージ作成
まず最初にスコープとパッケージをjsrのサイトから作る必要があります。
スコープは既に @kesin11
を取得済み。最終的にnpmと同様に junit2json
という名前でpublishしたいので、この名前でパッケージも作っておきます。
Createを押すと次の画面で手順が表示される。わかりやすい。
jsr.jsonを用意し、そこで name
を指定できるみたいです。実はnpm用のpackage.jsonの方では junit2json
となっているので、JSRの方だけ @kesin11/junit2json
にリネーム可能なのかちょっと心配していたのですが杞憂でした。
最初は実験のためにローカルからpublishしますが、最終的にはGitHub Actionsから全自動でpublishすることになるので今の段階でLinkだけはしておきましょう。
Kesin11/ts-junit2jsonがリポジトリなので入力して"Link"を押します。
リンクが成功して右側のGitHub Actionsからpublishするサンプルコードが表示されました。npmと異なりjsrにpublishする際はトークンを一切使用しないコードなのがちょっと不思議ですが、これについては後で実際にpublishする際に考えましょう。
そろそろ実際にpublishに必要なコードを書いていきます。まずは先ほど表示されていたjsr.jsonをコピーして実際のファイルを作成します。versionはとりあえず0.1.0のままにしておきます。exports
はエントリポイントのファイルを指定するようなので ./src/index.ts
にします。
npmではtsをそのままpublishできないのでトランスパイル後のjsである "main": "./dist/index.js",
などと指定する必要がありました。まずここがnpmと異なる点ですね。
{
"name": "@kesin11/junit2json",
"version": "0.1.0",
"exports": "./src/index.ts"
}
先ほどの表示されていたガイドではこの3項目だけなのでこのままでもpublishできると思いますが、npmに公開していたときのpackage.jsonを踏襲して次はpackage.jsonの files
に相当する設定を追加します。
jsr publish
は.gitignoreのファイルは自動的にpublish対象から外してくれるらしいですが、自分で明示的にinclude/excludeを設定も可能なようです。npmでも files
で明示的にpublish対象のファイルを設定していたので、ドキュメントにあるように LISCENSE
, README.md
, src
だけをincludeにしておきます。
{
"name": "@kesin11/junit2json",
"version": "0.1.0",
"exports": "./src/index.ts",
"publish": {
"include": [
"LICENSE",
"README.md",
"src/**/*.ts"
]
}
}
実際にpublishする前に npx jsr publish
コマンドにはどういう機能があるのかヘルプを確認しておきます。
$ npx jsr --help
jsr.io cli for node
Usage:
jsr add @std/log Install the "@std/log" package from jsr.io.
jsr remove @std/log Remove the "@std/log" package from the project.
Commands:
<script> Run a script from the package.json file
run <script> Run a script from the package.json file
i, install, add Install one or more JSR packages.
r, uninstall, remove Remove one or more JSR packages.
publish Publish a package to the JSR registry.
info, show, view Show package information.
Options:
-P, --save-prod Package will be added to dependencies. This is the default.
-D, --save-dev Package will be added to devDependencies.
-O, --save-optional Package will be added to optionalDependencies.
--npm Use npm to remove and install packages.
--yarn Use yarn to remove and install packages.
--pnpm Use pnpm to remove and install packages.
--bun Use bun to remove and install packages.
--verbose Show additional debugging information.
-h, --help Show this help text.
-v, --version Print the version number.
Publish Options:
--token <Token> The API token to use when publishing. If unset, interactive authentication will be used.
--dry-run Prepare the package for publishing performing all checks and validations without uploading.
--allow-slow-types Allow publishing with slow types.
--provenance From CI/CD system, publicly links the package to where it was built and published from.
Environment variables:
JSR_URL Use a different registry URL for the publish command.
DENO_BIN_PATH Use specified Deno binary instead of local downloaded one.
publish以外jsrのパッケージをインストールしたりするためのコマンドなので関係なさそうです。 --dry-run
オプションはこのあとの実験で使えそう。
--token
はローカルから実験的にpublishするのに使うのか?
--provenance
は多分 npm publish --provenance
と同じだろうからGitHub Actionsからpublishする際に使いそうな気がします。
ちょっと気になったのは、npmでは npm version
でpackage.jsonの version
を書き換えたりタグ打ちを自動でやってくれたましたが、それっぽいコマンドが一覧に存在しない。 jsr.json
の version
を書き換えるようなコマンドは jsr
には提供されてなさそう?
探してみたらissueはありました。まだ実装されてなさそう(コントリビューションチャンス?)
最終的にはpackage.jsonとjsr.jsonでバージョンを揃えたいので何かしらの方法を考える必要がありますが一旦後回し。
ローカルからのpublish
とりあえずはv0.1.0としてローカルから jsr publish
してみます。
$ npx jsr publish
Downloading JSR binary...
[00:01] [#################################################>] 44.3 MiB/44.7 MiB
Checking for slow types in the public API...
Visit https://jsr.io/auth?code=WYJQ-JMZB to authorize publishing of @kesin11/junit2json
Waiting...
ブラウザで認証画面が開いたのでApproveします。
Authorization successful. Authenticated as Kenta Kase
Publishing @kesin11/junit2json@0.1.0 ...
Successfully published @kesin11/junit2json@0.1.0
Visit https://jsr.io/@kesin11/junit2json@0.1.0 for details
Completed in 1m
えええ、これだけでpublishできてしまった。実際にローカルからpublishしたときのv0.1.0がこれです。
トークンの発行とか一切なかったのですがこれでいいのか・・・。@kesin11
のスコープに対して権限を持っているアカウント(自分)がブラウザからApproveしたことで2FAにはなる。確かにトークンとか無しに認証はこれで十分なのかもしれない。
ちなみに現時点でのJSRのscoreは70%でした。ドキュメントとprovenanceが足りないようです。
ちなみにJSRはREADME用のバッジも提供してくれています。このような記述をREADMEに追加するだけです。
[![JSR](https://jsr.io/badges/@kesin11/junit2json)](https://jsr.io/@kesin11/junit2json)
ドキュメント(JSDoc)
ちょうどいい機会なのでJSDocを書こうかと思いましたが、直すたびにjsrへpublishして確認するのは面倒なのでまずローカルで確認できる環境を整えます。
によるとDeno用の deno doc
のコマンドがそのまま使えるらしいです。試してみると、junit2jsonはもともとDenoで書いていないので色々エラーになりました。
$ deno doc --html --name=junit2json src
error: Failed resolving 'fs' from 'file:///home/kesin/github/kesin11/ts-junit2json/src/cli.ts'.
import fs from 'fs'
が解釈できないようです。これはDenoの限らず今では import fs from 'node:fs'
と書く方が現代的なので直しましょう。
$ deno doc --html --name=junit2json src
error: Failed resolving 'yargs' from 'file:///home/kesin/github/kesin11/ts-junit2json/src/cli.ts'.
yargs
は3rdパーティのパッケージですが、Denoの流儀でimportしていないので解決できなかったようです。Denoにnode_modulesを読み込むための --unstable-byonm
オプションがあるのでこれを追加しましょう。
$ deno doc --unstable-byonm --html --name=junit2json --lint src
error: Failed resolving './index.js' from 'file:///home/kesin/github/kesin11/ts-junit2json/src/cli.ts'.
import { parse } from './index.js
が解釈できないらしいです。ここまでくればtsをトランスパイルしてdist/に.jsを生成すれば多分動くと思いますが、これはimportする際の探索パスに.tsを含めるかどうかの問題のはず。Denoのオプションにimportの挙動を緩める--unstable-sloppy-imports
があるのでこれも追加してみましょう。
$ deno doc --unstable-byonm --unstable-sloppy-imports --html --name=junit2json src
Warning Sloppy module resolution (hint: update .js extension to .ts)
at file:///home/kesin/github/kesin11/ts-junit2json/src/cli.ts:5:23
Written 17 files to "./docs/"
エラーなく完走して生成できたようです。最終的なコマンドはこのようになりました。
deno doc --unstable-byonm --unstable-sloppy-imports --html --name=junit2json src
ローカルでドキュメントを確認する方法を用意できたのでJSDocを書いていきます。
といっても、junit2jsonは関数1つと型しかexportしておらず、型情報に関してはTypeScriptの型をそのままドキュメントに使ってくれるのでJSDocで型の二重管理をする必要はないでしょう。結局exampleを用意しておくぐらいになりました。
/**
* Parses the given JUnit XML string into a JavaScript object representation using xml2js library.
*
* @example Basic usage
* ```ts
* import { parse } from 'junit2json'
*
* const junitXmlString = "..."
* const output = await parse(xmlString)
* ```
*
* If you want to filter some tags like `<system-out>` or `<system-err>`, you can use `replacer` function argument in [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
*
* @example Filter some tags
* ```ts
* import { parse } from 'junit2json'
*
* const junitXmlString = "..."
* const output = await parse(xmlString)
* const replacer = (key: any, value: any) => {
* if (key === 'system-out' || key === 'system-err') return undefined
* return value
* }
* console.log(JSON.stringify(output, replacer, 2))
* ```
*/
export const parse = async (xmlString: convertableToString, xml2jsOptions?: OptionsV2): Promise<TestSuites|TestSuite|undefined|null> => {
一応再度jsrにpublishしてドキュメントを確認。
ローカルで確認したときと同じだったので deno doc --html
を信用しても大丈夫そうです。
package.jsonとjsr.jsonのversionを揃えてpublish
package.jsonとjsr.jsonのversionを揃えるためにpackage.jsonからjsr.jsonにコピーします。GitHub Copilot Chatに聞いてみたらワンライナーを生成してくれました。このワンライナーを npm run jsr:version
として呼び出せるようにpackag.jsonに書いておきます。
"jsr:version": "jq -r '.version' package.json | xargs -I {} jq '.version = \"{}\"' jsr.json > temp.json && mv temp.json jsr.json"
npm version
はpackage.jsonのversionを書き換えた後に自動的にgit tagを打ってコミットまでしてくれるコマンドなのですが、npm run jsr:version
でpackage.jsonのバージョンをコピーしてからコミットしたいので、その前にコミットされると困ります。仕方がないのでタグ打ちとコミットを行わないようになる --no-git-tag-version
オプションを付けて、jsr.jsonにバージョンをコピーした後に自前でgit commitしてからpublishすることにした。
まずは実験なのでprereleaseバージョンでpublishしてみます。
# package.jsonのバージョンを書き換え
npm version prerelease --no-git-tag-version && \
# jsr.jsonにバージョンをコピー
npm run jsr:version && \
# package.jsonとjsr.jsonをコミット
git add jsr.json package*.json && \
jq -r '.version' package.json | xargs -I {} git commit -m "{}"
# npmとjsrにpublish
npm publish --provenance --tag=beta && \
npx -y jsr publish
実際にworkflow_dispatchでprereleaseから試してみます。
npmとjsrに同じバージョンでpublishできました。
npmではこの実験的なバージョンが普通に npm install
したときにインストールされないようにチャンネルを分けて beta
という名前でリリースしています。jsrではチャンネルの概念は存在しないようですが、3.1.10-2
のようにsemverとしてprereleaseなバージョンであれば自動的に認識されて、通常のインストールではこのバージョンは使われない挙動になるようです。
ちなみにjsrへのpublishは、npmや他の多くの言語のレジストリで当たり前に必要なトークンを使わずにGitHub Actionsからpublish可能です。素晴らしい!代わりにGitHub Actionsのワークフローのyamlで以下のように permissions
に id-token: write
を指定する必要があります。
permissions:
contents: write # 最低でもreadは必要だが、どのみちGitHub Releasesに新しいバージョンを公開するためにwriteが必要
id-token: write
トークン無しでpublish可能な理由はOIDCの仕組みを利用していると想像できるのですが、jsrのドキュメントを改めて読んでも詳しい仕組みの解説を見つけることはできませんでした。要件としてあらかじめGitHubリポジトリとリンク済みであることと、先ほどの id-token: write
について書かれたドキュメントがあるぐらいです。
気になる人は自分で探してみるか、将来誰かが記事を書いてくれるのを待ちましょう。とりあえずjsrはトークン無しでpublishできて画期的であるということを覚えてみんなに広めてください。
JSRにpublishしたパッケージの動作確認
Deno
Denoはjsrをネイティブに扱えるのでimportに jsr:
でパッケージ名を指定するだけで使えます。
junit2jsonのREADMEに書いていたサンプルコードのimportを以下のように変更しただけで動きました。
// jsr_deno.ts
import { parse } from "jsr:@kesin11/junit2json@3.1.10-2";
const main = async () => {
const xmlString = `<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="gcf_junit_xml_to_bq_dummy" tests="2" failures="1" time="1.506">
<testsuite name="__tests__/basic.test.ts" errors="0" failures="0" skipped="0" timestamp="2020-01-26T13:45:02" time="1.019" tests="1">
<testcase classname="convert xml2js output basic" name="convert xml2js output basic" time="0.01">
</testcase>
</testsuite>
<testsuite name="__tests__/snapshot.test.ts" errors="0" failures="1" skipped="0" timestamp="2020-01-26T13:45:02" time="1.105" tests="1">
<testcase classname="parse snapshot nunit failure xml" name="parse snapshot nunit failure xml" time="0.013">
<failure>Error: Something wrong.</failure>
</testcase>
</testsuite>
</testsuites>
`
const output = await parse(xmlString)
console.log(JSON.stringify(output, null, 2))
}
main()
Node.js
今回はパッケージの動作確認なのでややこしくならないように tsc
やバンドラーは介さず、素のNode.jsで使えることを確認します。
jsrがESMしかサポートしていないので、まずはプロジェクト全体をデフォルトのcommonjsからESMに変更する必要があります。最低限 "type": "module"
が書かれたpackage.jsonを用意します。
{
"name": "jsr_node",
"private": true,
"type": "module",
}
jsrのドキュメントに書かれているように npx jsr add @kesin11/junit2json@3.1.10-2
でパッケージを追加すると dependencies
に npm:@jsr/
付きでパッケージ名が書き込まれます。kesin11__junit2json
という微妙に違った名前ですがスラッシュがエスケープされてそうな雰囲気を感じます。
{
"name": "jsr_node",
"private": true,
"type": "module",
"dependencies": {
"@kesin11/junit2json": "npm:@jsr/kesin11__junit2json@^3.1.10-2"
}
}
同時に.npmrcも作成され、 @jsr
はいつものnpmレジストリではなくnpm.jsr.ioを向くように設定が書かれていました。
@jsr:registry=https://npm.jsr.io
このあたりはjsrがnpmとの互換レイヤーのためにそうせざるを得なかった雰囲気をjsrのドキュメントから察することができます。npm install
を使ってjsrのパッケージを追加する場合はこのあたりをしっかり理解する必要がありますが npx jsr add
で追加する分には、上記のようにほぼ意識しなくても使えるようにやってくれます。
あとはNode.jsなので.tsではなく.jsを用意し、普通にimportを書くだけで動きました。意外と簡単ですね。
// jsr_node.js
import { parse } from "@kesin11/junit2json";
const main = async () => {
// Denoのコードと同じなので省略
}
main()
ちなみにNode.jsで使う場合には当然 node_modules
にインストールした実体が置かれるわけですが、jsrへのpublishはトランスパイルした.jsではなく.tsを直接アップロードしていました。では何が node_modules
に追加されたのか?ちょっと見てみましょう。
$ ls node_modules/@kesin11/junit2json/**/*
node_modules/@kesin11/junit2json/src/cli.ts
node_modules/@kesin11/junit2json/src/index.js
node_modules/@kesin11/junit2json/src/index.js.map
node_modules/@kesin11/junit2json/src/index.ts
node_modules/@kesin11/junit2json/_dist/src:
index.d.ts index.d.ts.map
index.jsと型定義の.d.ts、それぞれに対応したsourcemapの.mapが存在していることがわかります。npmと違ってjsrにpublishする場合は自分でts -> jsに変換はしていないので、これらはjsr側が自動的に生成してくれたファイルということですね。
今までnpmにpublishする場合にはtsconfig.jsonのオプションを理解し、.jsに加えて.d.tsや.mapも正しく生成する必要がありましたが、jsrの場合は.tsから全て自動で生成してくれるので圧倒的に楽です。
GitHub ActionsからのPublishとProvenance
最後にGitHub Actionsからprereleaseではなく実際に次のバージョンをpublishします。
コードは先ほどのprereleaseのときとほぼ同じで、 npm version prerelease
の代わりに npm version $VERSION
で外から次のバージョンを指定できるようにしているだけです。junit2jsonは自分の趣味で無駄に複雑なフロー[1]になっていて参考にならないので省略します。
リリース用のワークフローは workflow_dispatch
トリガーを設定しているので手動で実行します。
無事に完走してv3.1.10としてnpmとjsrにリリースされました。
最後にjsrでもProvenanceが設定されているかを確認します。Provenanceは最近のサプライチェーン攻撃への対策の1つで、パッケージがどこでビルドされたかを公開することで信頼性を高めるものです。実はnpmは以前からProvenanceをサポートしている[2]のでまずはnpmの方を見てみましょう。
npmはGitHub Actionsからpublishする際に npm publish --provenance
オプションを付けることでnpmのページ下部にこのような表示が追加され、publishが実行されたビルドログへのリンクが追加されます。
ぜひ以下のURLから一番下に表示されているProvenanceの項目の View build summary
をクリックしてみてください。上述のPublishが行われたビルドログである github.com/Kesin11/ts-junit2json/actions/runs/9257531431
にリンクされているはずです。
ではjsrの方も見てみましょう。jsrもGitHub ActionsからPublishするとnpmと若干似ている表示がページの下部に追加されます。 jsr publish
にも --provenance
オプションは存在しますが、単に jsr publish
だけでも表示されていたのでデフォルトで有効なようです。
View transparency log
をクリックするとsigstoreのページが開きます。npmでは単にGitHub Actionsのビルドログへのリンクだけでしたが、もっと色々な情報が表示されています。
下の方に Run Invocation URI: https://github.com/Kesin11/ts-junit2json/actions/runs/9257531431/attempts/1
というURLを見つけることができ、jsrにもnpm同様にProvenanceが設定されていることが確認できました。
まとめ
この記事は最初に書いていた以下のゴールを達成するための作業を記録したものでした。
- 今までnpmに公開していた junit2json をJSRにも公開する
- npm -> JSRへの乗り換えではなく、両方とも同じバージョンで公開する
- JSRへのpublishもnpm同様にGitHub Actionsで自動化
- JSR公開時にはprovenanceを付与する
実際にやってみた感想としては、DenoではなくNode.jsによるTypeScriptで書かれたパッケージであってもjsrに公開するのはnpmと比べて圧倒的に簡単です。正直、jsrの体験と比べると今まで使っていたにも関わらずnpmはかなり面倒くさいと感じてしまいますね。
今後新しいパッケージを作成した場合にもうjsrだけで公開したいぐらいなのですが、さすがにまだしばらくはjsrとnpmの両方で公開することになるかなあ。jsrが流行ればnpmもjsrの良いところを取り込んで発展してくれる可能性もあると思うので、みんなでjsrを流行らせていきましょう。
-
GitHub Actionsから手動トリガーでpublishするワークフローを組んでいますが、次期バージョンをrelease-drafterで決定し、実際のビルドの大半はEarthlyというビルドツールで行っています。興味がある人はリリースのワークフローとEarthfileを見てみてください。 ↩︎
Discussion