のらねこの気まま暮らし

技術系だけど、Qiita向きではないポエムとかを書くめったに更新されないヤツ

zellij(ゼリージュ)の設定メモ

わすれるので

概要

先日導入したけど、細かいことを結構忘れるのでメモを残しておきたい。

設定ファイル

以下のファイルに保存されいてる

./config/zellij/config.kdl

設定ファイルの書き出し

現在の設定を書き出してくれる。便利

zellij setup --dump-config > ~/dotfiles/zeliij/config.kdl

スクロール

Ctrl + s でsearchモードに入る 上下キーでバッファを動かせる

マウスモードを有効にしてれば何もしなくてもスクロールしてくれる。

コピーペースト

copy_commandはosxなのでpbcopyを有効にする。 copy_on_selectはデフォルトのままtrue

copy_command "pbcopy"
copy_on_select true

ubuntu

curl -L -O https://github.com/zellij-org/zellij/releases/download/v0.43.1/zellij-no-web-x86_64-unknown-linux-musl.tar.gz
tar -xzf zellij-no-web-x86_64-unknown-linux-musl.tar.gz -C /usr/local/bin
zellij setup --dump-config > ~/.config/zellij/config.kdl

xselをインストール

sudo apt install xsel -y

zellijのcopy_commandにxselを設定 ```~/.config/zellij/config.kgl copy_command "xsel --clipboard --input"

iPhoneからMac Book Pro上で実行しているclaude-codeに指示を出す

忘れそうなので備忘録。

概要

背景

何となくやりかけだったり、claudeに依頼してそのままでかけることがままある。 返答待ちで時間かかっちゃうのはもったいなっていうのと、特段頭を使うでもないささいな修正をさくっとやっといてほしいなと思った。

調べてみると人気な構成がこれっぽかったので試しにやってみた。

MacOSの共有設定

一般 > 共有 > リモートログイン > ON

MacOS > 一般 > 共有 > リモートログイン

スイッチの隣にある(!)を開いて、アクセスを許可するユーザーを追加。

私は一般ユーザーを追加した。 厳密にやるならアカウントわけたりとかもあるんだけど、session共有のあれこれの手間を増やしたくなかった。

Tailscaleのインストール

tailscale.com

VPNかーと思いつつ、不安になったら権限絞ったり端末わけたりやりようはあるかと思ってえいや。

とても分かりやすいオンボーディングで特に説明することがない。

  1. MacOSでアカウントのセットアップ
  2. MacOSでクライアントインストール・権限の許可
  3. iPhoneAndroid)でクライアントのインストール
  4. iPhoneAndroid)でログイン
  5. iPhone(Andoird)からMacOSに接続

Zellijのインストール

zellij.dev

tmuxよりZellij(ゼリージュって読むらしい)の方が流行らしいとXで知った。 Rust製で軽量、どう操作するればいいかの案内が充実していて使いやすい。

久々にtmux使おうとするとコマンドわかんなくて詰むのはよくあるのでこれはありがたい。

brewでinstallできる。

brew install zellij

セッションを開始

zellij

iPhoneAndroid)にTermiusをインストール

termius.com

iPhoneだとa-Shellなどが無料で利用できるが、Androidからも使いたいのとTermiusが調べた感じ感触よさそうだったので年間契約。 14日のトライアルがあるので不安なら試すだけもできる。

  1. インストール
  2. アカウントのセットアップ
  3. New Host を選択
  4. Labelに任意の接続先名を入力
  5. IP or Hostname に tailscaleの接続先からMagicDNSかIPアドレスをコピーしてきて入力
  6. Username・Password に MaxOSのログインユーザ・パスワードを入力

いったんこれで接続を開始。 コマンドラインシェルが開けて、ホームディレクトリにいることが確認できる。

iPhoneからzellijのセッションに参加

termiusのCLIからzellijのセッションを探す

> zellij list-sessions
loyal-apple [Created 15m 8s ago]

セッションの一覧が表示されるので、接続したいセッションを選択してアタッチする。

> zellij attach loyal-apple

MacOSで開始したclaude-codeのスレッドに合流できる。

あとがき

思ったより簡単にできた。かつてVPSsshしてiPadから障害対応とか考えた時代が懐かしい。

個人的にはVibe codingとかAI Agent Codingにたいしては懐疑的なスタンスで触ってはいるんだけど、 部分を選べばとても便利で優秀なソリューションだと思っている。

ダメ元でもプロトタイピングさせてみることで気付きを得られる、FBサイクルを早く回せるようになる。 これだけでも何らかの価値はあると思うので、この1年くらいはしっかり付き合ってみようと思っている。

株式会社ディー・エヌ・エーを退職しました。

5月末を持って株式会社ディー・エヌ・エーを退職しました。

 

 

2022/05から3年と数日の在籍でしたが、多くの方と関わり、幅広い仕事をできたことに感謝しています。

 

入社してから新規案件の開発に携わり、当時はまだあまりなの知れてなかったRemixを導入したり、サマーインターンの採用と受け入れに関わり、組織づくりをしたり、新卒採用したり、とにかく広くいろんなことをかなり好き勝手やらせてもらえました。

組織というものを相手にすることについては余白なく実力を発揮したと思いますし、同時に自身の限界を痛感することもありました。

関わってくれた、関わらざるを得なかったみなさんに、様々な経験をいただき感謝しています。あざす。

 

さて、退職ともなれば次の話なるわけですが、転職の予定はありません。

しばらくはフリーランスとしていくつかの会社から案件をいただきながら生活していこうと思っています。

 

フリーランスを選んだ背景は色々ありますが、大雑把に言うとこういうことです。

 

「組織を見る動きに自身の天井が見えたこと、およびその先に望んだ世界がないこと」

 

端的に言えば、無理をしない範囲で継続的に仕事として動くことはできるけど、自分にとってそれは停滞だし、無理を強いて背伸びをしたところで、ほしいものは得られないなと。それであれば、技術者として探求・貢献に人生を割いたほうが楽しめそうだなと感じたためです。

ーーまあそうなるかはともかく、試すにはちょうどいいタイミングだなというのもありました。

 

実際すでにいくつかお話をいただけており、しばらく暇のない生活が続きそうです。感謝しかない。

 

しばらく新しい案件はいっかなーと思ってますが、近況聞きたいとか相談だけでもって方は声かけてください。飲みにいくのはウェルカムだ。酒は飲めんけど。

 

そんなわけで、引き続きよろしくお願いします。

 

 

logone-jsにパスワードのマスク処理を追加した

logone-jsのアップデートを行ったのでブログとして残す。

logoneについては過去の記事を参照してね。 mizuki-r.hatenablog.com

リリース内容

新機能

  • シークレット文字列のマスク機能

不具合修正

  • maxmum call stackの修正

シークレット文字列のマスク機能

開発環境下でのデバッグログや、API通信のペイロードなどをログとして出力したいことは多くある。その中に認証情報やパスワードなどが含まれてしまうこともよくある。

次のようなコードは頻出パターンと言える。

try {
  const data = await axios.post('/authorize', { username, password })
catch (error) {
  logger.error(error.message, { error })
  throw error
}

この時、errorがrequestやresponseなどの情報構造を内包している場合(AxiosErrorとか)、意図せずデータがログに露出する可能性がある。 開発環境のみで動作するものであれば良いが、開発環境フラグを設定し忘れたり、エラーログの中に紛れこんで困ることがあるかもしれない。

RailsやLaravelといった大手のフレームワークで用いるロガーにはそのようなシークレットトークンに対してマスクするような機能が内包されていたりする。

今回logoneに追加したのは任意キーワードにマスクをかけられるようにする機能だ。

以下のような設定を行う。

const app = express()

app.use(logone({
  maskKeywords: [
    'password', // "password"というプロパティに一致する値をマスクする
    /password$/i, // 正規表現で大文字小文字を識別せずpasswordという文字列に後方一致する値をマスクする
    /password:¥s"(.+)"$/, // 'password: "XXXXXXX"' という文字列を検索し、'XXXXXXX'をマスクする
  ]
}))


app.post('/login', (req, res) => {
  req.logger.debug("request", { req })
})

文字列、正規表現のリストをパラメータとして受け取り、それにマッチするプロパティの値を同じ文字列長だけ*でマスクします。プロパティはpayloadのオブジェクトを再起的に探索します。messageに指定された値はmaskされません(が、まあログのメッセージにシークレットを書き出すような運用はしないでしょう...)

また、正規表現がキャプチャグループを持つとき、それはプロパティではなく値そのものから対象を検索します。 これは、HTTP通信のログなどで値がシリアライズされている場合もあるためこのようにしています。

マスク処理自体は単純なリプレイスです。

value.replace(/./g, '*')

maximum call stackの修正

前述の仕組みはpayloadというunkownなオブジェクトに対しての再起探索が必要になります。

今まさにlogoneを利用しているプロジェクトではHTTP通信にAxiosを利用しており、そのエラーオブジェクトは再帰的なオブジェクト構造を持ちます。

再帰構造を持つオブジェクトに再帰処理を行うと Maximum call stack exceededの例外が発生します。 まあ... 処理が終わらないので...

それ故に、JSONに置き換える処理では再帰参照を止めるように仕組みを作っていたんですが、それはAdapter側の出力の範囲でした。今回の対応はAdapterに渡す前のオブジェクトだったため、オブジェクト構造を判定して再帰構造を解体するようにしました。

まとめ

以下の2点を@logone/coreに追加したv0.0.4をリリースしました。 - 再帰的なオブジェクト探索によるパスワード文字列のマスク - 再帰構造を持つオブジェクトの解体

今回は必要に迫られて突貫で対応しましたが、パラメータ名などもうちょっと考慮の余地あったんじゃないかな〜と思ったり。

次は出力できるLogLevelの設定とかを追加したいですね。

宣言的にFormのルールを定義できるdeclaformというライブラリを書いている

まえがき

2年ほど前から実務でRemixを利用している。 使い始めた当時はライブラリも情報もなく、かなり手探りで始めた開発だった。

当時はプロダクトのMVP検証を目的として、手数少なく動くものが作れるという点でRemixを採用した。 実際に使ってみて、その期待は間違ってなかったと感じたし、今も結構手に馴染んでいる。−−不満はあれど。

そんな中でFormのバリデーションについては、あまり深く考えずにRemix Validated Formを採用していた。

当時は選択肢が多くなく、Remix専用となるとこれくらい。 あとは、React Hook Formを使っていくのが無難だっただろう。

最近だとConformなんてのもできており、Form周りのライブラリは何年立っても賑わうものだなぁと感心する。

そんな渦中に一石を投じるほどの意欲はないが、それでもどうしても物申したかったのでゴールデンウィーク返上で筆を取ることにした。

React界隈におけるForm Validationの課題

−−言えるほど偉い立場ではないんだけども。 −−言うほどReact使ってサービス作ってないんだけども。

Formという箱の中にしか書けないルール

例えば、React Hook Formを使かおうと思うとこういうコードになる。

export default function App() {
  const { register, handleSubmit } = useForm<{ username: string, password: string }>()
  
  const onSubmit = (data) => console.log('submit', data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username", { required: true })} />
      <input {...register("password", { required: true })} />
      <button type="submit">Login</button> 
    </form>
  )
}

この場合、useFormを実行することが前提となるので、これらの子となるinput要素はこのuseFormの結果を子まで伝搬させる必要がある。 Componentを切ろうとしても、PropsにReactHookFormの子であることを示す値を設定する必要が出てくる。

また、正しくuseFormしてない箇所に記述はできない。 任意のFormの下でしか動作しないInput要素ということになる。

これは依存性が逆転していると感じている。

Remix Validated Formの場合はこう

import { Form } from 'remix-validated-form'
import { withZod } from '@remix-validated-form/with-zod

const validator = z.object({
  username: z.string(),
  password: z.string(),
})

export default function App() {
  const { register, handleSubmit } = useForm<{ username: string, password: string }>()
  
  const onSubmit = (data) => console.log('submit', data)

  return (
    <Form onSubmit={onSubmit} validator={withZod(validator)}>
      <input type="text" name="username" />
      <input type="password" name="password" />
      <button type="submit">Login</button> 
    </Form>
  )
}

これも似たような、あるいは同様の問題を孕むことになる。 RemixValidatedFormにおいてはvalidationが完全に上位のProviderに依存するので完全に暗黙的な依存を持つことになる。

useFormFieldフックをPresentationalなComponentで呼び出すことになる。そしてStorybookで死ぬ。

これらの構造的制約によって、アプリケーションの開発の幅を狭めざるを得ないことが増えていく。 例えば、Input要素はForm要素の子でなければならない、などの制約がDOM上に露出していくことになるわけだ。それを避けるためには開発者ががんばって頭を捻る必要がある。

とてもつらい。

Schema based validationという外側の制約

React Hook Formは特段使わなければ気にならないかもしれない。

少なくとも、Schema basedなバリデーションを実施しているFormライブラリは最上位にSchemaを食わせる必要がある。

これはInputのnameとruleが外側の何某かによって成約されると言え、要素単位でバラして再利用することをあまり考えていない。 特に業務システムのような複雑な入力フォームを無数に持つ画面の開発において、Form単位でしか共通化できないというのはかなりナンセンスといえる。

重ねて課題を述べるなら、RemixにおいてはForm=Actionの単位なので、無数に一画面にFormにいれたくない。 ある画面では3つですむが、ある画面では5まで関連する要素が増えるみたいなときの共通化を少しは考えさせてほしい。

そのschema、他で使う?問題

最初のころはFormとServerでのバリデーションを共通のSchemaを利用して記述していた。 まあそのままサーバーに送られてくるんだし、問題ないよね。

−−そんなわけなかった。

クライアントとサーバーのバリデーションは意図が異なる。

クライアントでは即座にユーザに問題をフィードバックすることで体験を改善することができる。 パスワードの再入力なんかはは良い例で、クライアントでは必要だけど、別にサーバーでチェックすることはない。

サーバーでのバリデーションは防御的な側面がある。これ入力されちゃマズい、を弾くためだ。 別に処理に関係ないならそもそも使わないしね。

そして、画面を埋めつくするようなフォームを設計する場合、そのフォームはフラットに一つのSchemaなのではなく、複数のSchemaを組み合わせて使うことになる。 つまり、そこにモデルが存在している。

それらを総合的に考えていくと、結局Client Formに向けた専用のSchemaをForm毎に作ることになる。FormのComponentとは別に。

つまり、ただただ関心を分離させているだけと言える。

宣言的ではない

React Hook FormやConformを見るに、hooksでいい感じにパラメータをうめこみますよ〜という手続き的な記述を多用する。 わかるんだよね、便利だし、型制約効くし。

でもそれじゃなんのためにJSXでMarkupしてんのかわからんだろがいという気持ちになる。

ルールを宣言させてくれ。

宣言的にFormを書きたい

色々懸念や課題や愚痴を並べてはいるけど、解決したいのはこれ。 書き心地よくサクッと実装したい。

なので、今回目指したのは「宣言的にかけるForm Validation」

github.com

使い方

  1. バリデーションルールを定義する
  2. フォームを定義する

バリデーションルールの定義

Form単位ではなく、Input(入力)単位でルールを記述する。 ZodとかYup使ってもいいし、使わなくてもいい。

import { defineValidationRule } from '@declaform/core'

const usernameSchema = z.string().min(1).max(64)
defineValidationRule('username', {
  validate(value) {
    return usernameSchema.safeParse(value)
  }
})

const passwordSchema = z.string().min(8).max(64).regex(passwordRegex)
defineValidationRule('password', {
  validate(value) {
    return passwordSchema.safeParse(value)
  }
})
defineValidationRule('passwordCOnfirm': {
  validate(value, data, { ruleReferenceInputName }) {
    return passwordSchema
      .refine(() => {
        return value === data[ruleReferenceInputName]
      }, {
        message: 'Password mismatch'
      })
      .safeParse(value)
  }
})

Formを記述する

単純なケースではあるけど、前提として何らかのComponentにそれぞれのInputやFieldを隠蔽することを想定している。 そのため、カスタマイズが効くようにrenderPropsで受け渡しできるようになっている。

import { HTMLFormElement } from 'react'
import { FormProvider, FormInput } from '@declaform/react'

const App = () => {
  const handleSubmit = (ev: FormEvent<HTMLFormElement>) => {
    // 型の整合性に責任は負わないので、使う側で精査してね
    const formData = new FormData(ev.target)
  }

  return (
    <FormProvider onSubmit={handleSubmit}>
      {({ ref, onSubmit, hasError }) => (
        <form ref={ref} onSubmit={onSubmit}>
          <div>
            <label>User ID</label>
            <FormInput name="user_id" rule="username">
              {(props, errors) => (
                <div>
                  <input {...prop} type="text" autoComplete="username" />
                  {errors.map((error) => (
                    <div key={error}>{error}</div>
                  ))}
                </div>
              )}
            </FormInput>
          </div>
          <div>
            <label>Password</label>
            <FormInput name="password" rule="password">
              {(props, errors) => (
                <div>
                  <input
                    {...prop}
                    type="password"
                    autoComplete="new-password"
                  />
                  {errors.map((error) => (
                    <div key={error}>{error}</div>
                  ))}
                </div>
              )}
            </FormInput>
          </div>
          <div>
            <label>Password (confirm)</label>
            <FormInput
              name="password_confirm"
              rule="passwordConfirm"
              ruleReferenceInputName="password"
            >
              {(props, errors) => (
                <div>
                  <input
                    {...prop}
                    type="password"
                    autoComplete="new-password"
                  />
                  {errors.map((error) => (
                    <div key={error}>{error}</div>
                  ))}
                </div>
              )}
            </FormInput>
          </div>

          <div>
            <button type="submit" disabled={hasError}>
              Submit
            </button>
          </div>
        </form>
      )}
    </FormProvider>
  )
}

ガチガチにComponentに隠蔽したらこうなる。

    <Form onSubmit={handleSubmit}>
      {({ hasError }) => (
        <FormField label="User ID" name="user_id" required>
          <FormInputUsername />
        </FormField>
        <FormField label="Password" name="password" required>
          <FormInputPassword />
        </FormField>
        <FormField label="Password(confirm)" name="passwordConfirm">
          <FormInputPasswordConfirm referencePropName="password" />
        </FormField>
        <FormField>
          <Button type="submit" disabled={hasError}>Submit</Button>
        </FormField>
      )}
    </Form>

つまるところ、分解しやすく、組み合わせやすいところまでFormの制御要素を整理したいわけよ。

おわりに

−−という未来を目指して書いてるライブラリ、 declaform の解説でした。 まだ完成してない。絶賛道半ばです。

実際やってみて、2010年代のフロントエンド開発とは明らかに難しさが違うな〜と感じている。

TypeScriptとReactのかけ合わせでライブラリ書こうとするとめちゃくちゃむずい。 ライブラリ化せずにただ動くものってレベルだと半日くらいでできていた。型とStateの伝搬の整合性とるのに数日かかった。まじむずい。

なんで、まあ、しばらくしたら「無理っす、Conformつかいましょう」って言ってるかもしれない。 これ一人でなんとかできる気がしない。

世の中のライブラリ開発者の皆さんに、溢れんばかりの感謝と敬意を。

nodejsで利用する構造化ロギングライブラリ、logoneを作成した

概要

www.npmjs.com

主にexpressなどのサーバー上で動作することを期待した、構造化ロギングライブラリを作成した。 この記事は、その辺の概要を覚書程度に記すものである。

背景

AWSのCloud WatchGCPのCloud Loggingといった環境の中で、Nodejsサーバーからログを書き出す方法論の一つとして構造化ロギングがある。

Nodejsが標準で搭載する console.log は柔軟にログを書き出せるが、前述する環境だと単なるテキストと扱ったり、複雑なデータ構造のデバッグに向かない。 そのため、JSON.stringify%jを用いてデータなどをJSONに変換したり、severityをその中に埋め込んだりと様々な工夫がある。

一方で、console.logは直ちにstdoutやstderrに書き出してしまうため、一つのリクエストの中であっても同時にリクエストが走った場合、ログの関係性が追いづらくなってしまう。

こんな感じで入り乱れるよね

request1 aaa
request2 aaa
request1 bbb
request1 ccc
request2 bbb

そこで、コンテクスト(例えば、リクエスト単位など)毎にログを集約・構造化することでログの関係性・前後関係を追いやすくすることが求められる。

こんな感じでまとめるイメージ。

{ context: request1, lines: [aaa, bbb, ccc] }
{ context: request2, lines: [aaa, bbb] }

今回、お仕事の中で紹介してもらった logone-go を参考にNodejs向きに実装した。 大本がGoであり、Nodejsの事情や利用シーンの想定から大幅に手を加えているが、大変参考になったのでここで作成者の hixi-hyi に感謝を示したい。

アーキテクチャ

今回の用途ではexpressから利用することを想定している。 −−が、その一方で、お仕事ではRemixを利用していたり、クライアントサイドから利用される可能性も考慮したかった。

なので、選択的にどこにログを出力するか、どこから利用できるかという観点と構造化ログを作成する核心の機能を別のパッケージで提供したかった。

multi-package architecture

現在提供しているライブラリは以下の3つある

www.npmjs.com

www.npmjs.com

www.npmjs.com

@logone/core

ログを集約し、構造化するコア。 adapterを注入することで、出力先を選択できる

@logone/adapter-node

nodejs上で動作するアダプタ。 stdout, stderrにログを書き出す。

他の環境やファイル出力など、このadapterを実装・交換することで可能になる。

※同期性や複数出力先などは考慮が足りてない... 今後の課題

@logone/express

express上でlogoneを開始・出力するためのライブラリ。 リクエストを受け付けたらログの収集を開始し、レスポンスが発行されたらログを出力する。

現在はadapterを内包しているが外から注入できたほうがいいかもなーと思っている。

monorepo

これらのパッケージの運用・リリースを行うのにmonorepoの構成を採用している。 そのため、開発リポジトリは以下の一つのみ。

github.com

monorepoの運用については以下の記事を書いた際に検証し、得た知見をそのまま利用している。

mizuki-r.hatenablog.com

Usage

以下のような実装を考える。

import { createLogone } from '@logone/core'
import { createAdapter } from '@logone/adapter-node'

const logone = createLogone(createAdapter())
const main = () => {
  const { logger, finish } = logone.start('example')

  logger.info('hello world')

  finish()
}

これを実行すると、このようなログを1行にまとめて出力する。

{
    "type": "example",
    "config": {
        "elapsedUnit": "1ms"
    },
    "context": {},
    "runtime": {
        "severity": "INFO",
        "startTime": "2024-05-01T01:02:03.456Z",
        "endTime": "2024-05-01T01:02:04.456Z",
        "elapsed": 1000,
        "lines": [
            {
                "severity": "INFO",
                "message": "hello world",
                "payload": [],
                "fileLine": 8,
                "fileName": "/path/to/example.ts",
                "time": "2024-05-01T01:02:03.456Z"
            }
        ]
    }
}

finish関数が実行されるまでログはlogoneに収集され、出力されない。 finish関数が実行されて初めてログはadapterを経由して出力される。

ここから、logoneにおける要素と想定ケースを説明していく。

context

ログの集約単位を示す情報をcontextには設定できる。 例えば、リクエストなんかでは以下のように使うことができる。

const { logger, finish } = logone.start('request', {
  url: req.url,
  method: req.method,
  host: req.headers.host,
  userAgent: req.headers['user-agent'],
})

この設定によって、そのログがどのリクエスト(などのコンテクスト)で実行されたかの関係性が自明になる。

runtime elapsed

ログの開始から終了までの経過時間を1ms単位で出力する。 configで渡すInterfaceにはしているが今のところ1ms固定なので注意。

runtime severity

Severityは以下のものを用意している。 - DEBUG - INFO - WRNING - ERROR - CRITICAL

各Lineで出力されたSeverityのなかでプライオリティが一番高いものをruntimeのSeverityとして指定している。 プライオリティは上のリストで下に行くほど高い。

今後の展望としてSeverityの脚切りができるようにはしたい。

file information

今回工夫したものとしては、このファイル情報の出力である。 なぜならば、JavaScriptは関数の呼び出し元という情報を原則保持しないためである。(実行環境によって差異がある、Nodeはサポートしていない)

ただ唯一の例外がErrorオブジェクトによるStack Traceである。 今回はErrorオブジェクトを発行して、呼び出した関数のStackTraceからファイル情報を抜き出している。

  private getCallerPosition(): [string | null, number | null, number | null] {
    const stack = new Error().stack
    const rows = stack ? stack.split(/\n/) : []
    const current = rows[4]
    if (!current) {
      return [null, null, null]
    }
    const [file, lines, chars] = current.replace(/\s*at\s+/, '').split(/:/)
    return [
      file ?? null,
      lines ? Number(lines) : null,
      chars ? Number(chars) : null
    ]
  }

github.com

雑に引っこ抜いているので、フォーマット崩れはあるかも... とはいえ、これによって(生成後のJSファイルではあるが...)実行場所を把握しやすくなっている。

まとめ

−−というわけで、logone-goの移植、@logone/coreを作成しました! adapterを追加すればあちこちで使えるんじゃないかな〜というのと、多分NodeJSだけで動くようには作ってないと思うのと、動作についてはzero-dependencyなので非常にライトに使えると思っています。

NodeJS界隈の構造化ログだと、child-processなどで集約していたり、かなり工夫して頑張っているライブラリも多いですが、logoneは汎用性に重きを置いている(それが本当に必要なのかはわからない)ため、非常にライトな実装と動作になっています。 気軽に試してみたい方はぜひ触ってみてください。

turborepoとchangesetを使って、monorepoのライブラリ開発における構成を検討する

概要

monorepoなライブラリをnpmに公開するための構成を調査、検討する。

検証リポジトリ

以下にこの記事で検証したリポジトリを示す。

github.com

動作環境

macOS Ventura バージョン13.6.6 Node 21.4.0

前提となる構成

turborepo

turboを使ってmonorepoを構成する

turbo.build

changeset

turborepoによると@changesets/cli - npmを推奨している。

www.npmjs.com

verdaccio

localで動作するnpm registry。 実験でpublishしまくるので公式のregistryは流石に使いにくい。

verdaccio.org

Verdaccioの構築

docker imageを使うのがお手軽そうなので、取得して立ち上げる。

以下の点に注意。 * portの指定忘れると開けない * volumeの指定しないと再起動でデータが消える

$ docker pull verdaccio/verdaccio:nightly-master 
$ docker run -it -p4873:4873 --rm verdaccio/verdaccio:nightly-master
(node:8) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
 info -=- local storage path /verdaccio/storage/data/.verdaccio-db.json
 info -=- no private database found, recreating new one on /verdaccio/storage/data/.verdaccio-db.json
 info --- using htpasswd file: /verdaccio/storage/htpasswd
 info --- http address http://0.0.0.0:4873/
 info --- version: 7.0.0-next-7.13
 info --- server started

指定したportをブラウザで開く。

open http://0.0.0.0:4873/

ひとまず初期画面が表示される。

Verdaccioの初期画面

検証用のリポジトリを作成

こちらに作成しました。作成の意図としては - nodejs で動作するpackageをイメージ - turborepoで構成 - 複数のpackageを構成

github.com

changesetの導入

packageにインストール

$ npm -w packages/core i -D @changesets/cli 
$ npm -w packages/express i -D @changesets/cli 

coreとexpressのpackage.jsonchangesetスクリプトを追加

diff --git a/packages/core/package.json b/packages/core/package.json
index f8facde..c7876fd 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -7,12 +7,14 @@
   "scripts": {
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
-    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check"
+    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
+    "changeset": "changeset"
diff --git a/packages/express/package.json b/packages/express/package.json
index f8facde..c7876fd 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -7,12 +7,14 @@
   "scripts": {
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
-    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check"
+    "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
+    "changeset": "changeset"

changesetの初期化

$ npm -w packages/core run changeset init
$ npm -w packages/express run changeset init

publishのaccessをpublicに変更

diff --git a/packages/core/.changeset/config.json b/packages/core/.changeset/config.json
index 91b6a95..fce1c26 100644
--- a/packages/core/.changeset/config.json
+++ b/packages/core/.changeset/config.json
@@ -4,7 +4,7 @@
   "commit": false,
   "fixed": [],
   "linked": [],
-  "access": "restricted",
+  "access": "public",
   "baseBranch": "main",
   "updateInternalDependencies": "patch",
   "ignore": []
diff --git a/packages/core/package.json b/packages/core/package.json
index 5f59f4d..1987880 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -14,5 +14,8 @@
     "@changesets/cli": "^2.27.1",
     "@example-monorepo/dev-config": "*",
     "tsup": "^8.0.2"
+  },
+  "publishConfig": {
+    "access": "public"
   }
 }
diff --git a/packages/express/.changeset/config.json b/packages/express/.changeset/config.json
index 91b6a95..fce1c26 100644
--- a/packages/express/.changeset/config.json
+++ b/packages/express/.changeset/config.json
@@ -4,7 +4,7 @@
   "commit": false,
   "fixed": [],
   "linked": [],
-  "access": "restricted",
+  "access": "public",
   "baseBranch": "main",
   "updateInternalDependencies": "patch",
   "ignore": []
diff --git a/packages/express/package.json b/packages/express/package.json
index c7876fd..586fd64 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -19,5 +19,8 @@
     "@types/express": "^4.17.21",
     "express": "^4.19.2",
     "tsup": "^8.0.2"
+  },
+  "publishConfig": {
+    "access": "public"
   }
 }

changesetの設定を行う

$ npm -w packages/core run changeset                                            [branch:main](untracked)[~/project/example-monorepo]

> @example-monorepo/core@0.0.0 changeset
> changeset

🦋  Which packages would you like to include? · @example-monorepo/core, @example-monorepo/express
🦋  Which packages should have a major bump? · No items were selected
🦋  Which packages should have a minor bump? · No items were selected
🦋  The following packages will be patch bumped:
🦋  @example-monorepo/core@0.0.0
🦋  @example-monorepo/express@0.0.0
🦋  Please enter a summary for this change (this will be in the changelogs).
🦋    (submit empty line to open external editor)
🦋  Summary · the first update
🦋  
🦋  === Summary of changesets ===
🦋  patch:  @example-monorepo/core, @example-monorepo/express
🦋  
🦋  Note: All dependents of these packages that will be incompatible with
🦋  the new version will be patch bumped when this changeset is applied.
🦋  
🦋  Is this your desired changeset? (Y/n) · true
🦋  Changeset added! - you can now commit it
🦋  
🦋  If you want to modify or expand on the changeset summary, you can find it here

bump up

version, publishへのエイリアスを作成する。

diff --git a/package.json b/package.json
index 106fd31..3f6c163 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,8 @@
     "build": "turbo run build",
     "lint:eslint": "turbo run lint:eslint",
     "lint:prettier": "turbo run lint:prettier",
+    "version": "turbo run version",
+    "publish": "turbo run publish",
     "format": "prettier --write \"**/*.{ts,tsx,md}\""
   },
   "devDependencies": {
diff --git a/packages/core/package.json b/packages/core/package.json
index 1987880..74c6f4a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -8,7 +8,9 @@
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
     "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
-    "changeset": "changeset"
+    "changeset": "changeset",
+    "version": "changeset version",
+    "publish": "changeset publish"
   },
   "devDependencies": {
     "@changesets/cli": "^2.27.1",
diff --git a/packages/express/package.json b/packages/express/package.json
index 586fd64..531b6b9 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -8,7 +8,9 @@
     "build": "tsup src/*",
     "lint:eslint": "eslint --max-warnings 0 --ext .ts,.tsx .",
     "lint:prettier": "prettier './src/**/*.{ts,tsx,js,json,css}' --check",
-    "changeset": "changeset"
+    "changeset": "changeset",
+    "version": "changeset version",
+    "publish": "changeset publish"
   },
   "dependencies": {
     "@example-monorepo/core": "*"
diff --git a/turbo.json b/turbo.json
index 8402ed6..c3df47b 100644
--- a/turbo.json
+++ b/turbo.json
@@ -18,6 +18,15 @@
     "dev": {
       "cache": false,
       "persistent": true
+    },
+    "version": {
+      "cache": false,
+      "persistent": true
+    },
+    "publish": {
+      "cache": false,
+      "persistent": true
     }
+
   }
 }

versionを上げる

$ npm run version
commit 62802584c2bbc5be7fec65d73d33d2efc12b01c3
Author: mizuki_r <ry.mizuki@gmail.com>
Date:   Mon Apr 29 15:40:07 2024 +0900

    bump 0.0.1

diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
new file mode 100644
index 0000000..726ce34
--- /dev/null
+++ b/packages/core/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @example-monorepo/core
+
+## 0.0.1
+
+### Patch Changes
+
+- the first update
diff --git a/packages/core/package.json b/packages/core/package.json
index 74c6f4a..8b1b54c 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@example-monorepo/core",
-  "version": "0.0.0",
+  "version": "0.0.1",
   "private": true,
   "main": "build/index.js",
   "types": "src/index.ts",
diff --git a/packages/express/CHANGELOG.md b/packages/express/CHANGELOG.md
new file mode 100644
index 0000000..07d956c
--- /dev/null
+++ b/packages/express/CHANGELOG.md
@@ -0,0 +1,9 @@
+# @example-monorepo/express
+
+## 0.0.1
+
+### Patch Changes
+
+- the first update
+- Updated dependencies
+  - @example-monorepo/core@0.0.1
diff --git a/packages/express/package.json b/packages/express/package.json
index 531b6b9..1d38dc0 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@example-monorepo/express",
-  "version": "0.0.0",
+  "version": "0.0.1",
   "private": true,
   "main": "build/index.js",
   "types": "src/index.ts",

verdaccioとの接続

verdaccioのURLをnpmrcに登録

diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..30fb789
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+registry=http://0.0.0.0:4873

adduserしてverdaccioにユーザを登録

$  npm adduser
npm notice Log in on http://0.0.0.0:4873/
Username: mizuki_r
Email: (this IS public) XXXXX@gmail.com
Logged in on http://0.0.0.0:4873/.

いざ、publish、とおもったのに....

$ npm run publish  

> example-monorepo@0.0.0 publish
> turbo run publish

• Packages in scope: @example-monorepo/core, @example-monorepo/dev-config, @example-monorepo/express
• Running publish in 3 packages
• Remote caching disabled
@example-monorepo/core:publish: cache bypass, force executing 6d170e85bb8ec0a1
@example-monorepo/express:publish: cache bypass, force executing 97213dc3ac891ae0
@example-monorepo/express:publish: 
@example-monorepo/express:publish: > @example-monorepo/express@0.0.1 publish
@example-monorepo/express:publish: > changeset publish
@example-monorepo/express:publish: 
@example-monorepo/core:publish: 
@example-monorepo/core:publish: > @example-monorepo/core@0.0.1 publish
@example-monorepo/core:publish: > changeset publish
@example-monorepo/core:publish: 
@example-monorepo/express:publish: 🦋  warn No unpublished projects to publish
@example-monorepo/core:publish: 🦋  warn No unpublished projects to publish

 Tasks:    2 successful, 2 total
Cached:    0 cached, 2 total
  Time:    701ms 

packageがprivate: trueだったからです。

diff --git a/packages/core/package.json b/packages/core/package.json
index 8b1b54c..cc96e00 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@example-monorepo/core",
   "version": "0.0.1",
-  "private": true,
+  "private": false,
   "main": "build/index.js",
   "types": "src/index.ts",
   "scripts": {
diff --git a/packages/express/package.json b/packages/express/package.json
index 1d38dc0..df29381 100644
--- a/packages/express/package.json
+++ b/packages/express/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@example-monorepo/express",
   "version": "0.0.1",
-  "private": true,
+  "private": false,
   "main": "build/index.js",
   "types": "src/index.ts",
   "scripts": {

今度こそpublish

$ npm run publish

> example-monorepo@0.0.0 publish
> turbo run publish

• Packages in scope: @example-monorepo/core, @example-monorepo/dev-config, @example-monorepo/express
• Running publish in 3 packages
• Remote caching disabled
@example-monorepo/express:publish: cache bypass, force executing 95f6040856482a97
@example-monorepo/core:publish: cache bypass, force executing 304ea4a437f8c0b8
@example-monorepo/express:publish: 
@example-monorepo/express:publish: > @example-monorepo/express@0.0.1 publish
@example-monorepo/express:publish: > changeset publish
@example-monorepo/express:publish: 
@example-monorepo/core:publish: 
@example-monorepo/core:publish: > @example-monorepo/core@0.0.1 publish
@example-monorepo/core:publish: > changeset publish
@example-monorepo/core:publish: 
@example-monorepo/express:publish: 🦋  info npm info @example-monorepo/core
@example-monorepo/core:publish: 🦋  info npm info @example-monorepo/core
@example-monorepo/core:publish: 🦋  info npm info @example-monorepo/express
@example-monorepo/express:publish: 🦋  info npm info @example-monorepo/express
@example-monorepo/core:publish: 🦋  warn Received 404 for npm info "@example-monorepo/core"
@example-monorepo/express:publish: 🦋  warn Received 404 for npm info "@example-monorepo/express"
@example-monorepo/express:publish: 🦋  warn Received 404 for npm info "@example-monorepo/core"
@example-monorepo/express:publish: 🦋  info @example-monorepo/core is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/express:publish: 🦋  info @example-monorepo/express is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/express:publish: 🦋  info Publishing "@example-monorepo/core" at "0.0.1"
@example-monorepo/express:publish: 🦋  info Publishing "@example-monorepo/express" at "0.0.1"
@example-monorepo/core:publish: 🦋  warn Received 404 for npm info "@example-monorepo/express"
@example-monorepo/core:publish: 🦋  info @example-monorepo/core is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/core:publish: 🦋  info @example-monorepo/express is being published because our local version (0.0.1) has not been published on npm
@example-monorepo/core:publish: 🦋  info Publishing "@example-monorepo/core" at "0.0.1"
@example-monorepo/core:publish: 🦋  info Publishing "@example-monorepo/express" at "0.0.1"
@example-monorepo/core:publish: 🦋  error an error occurred while publishing @example-monorepo/express: E409 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fcore - this package is already present 
@example-monorepo/express:publish: 🦋  error an error occurred while publishing @example-monorepo/express: E409 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fexpress - this package is already present 
@example-monorepo/core:publish: 🦋  error npm notice Publishing to http://0.0.0.0:4873 with tag latest and public access
@example-monorepo/core:publish: 🦋  error npm ERR! code E409
@example-monorepo/core:publish: 🦋  error npm ERR! 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fcore - this package is already present
@example-monorepo/core:publish: 🦋  error 
@example-monorepo/core:publish: 🦋  error 
@example-monorepo/express:publish: 🦋  error npm notice Publishing to http://0.0.0.0:4873 with tag latest and public access
@example-monorepo/express:publish: 🦋  error npm ERR! code E409
@example-monorepo/express:publish: 🦋  error npm ERR! 409 Conflict - PUT http://0.0.0.0:4873/@example-monorepo%2fexpress - this package is already present
@example-monorepo/express:publish: 🦋  error 
@example-monorepo/express:publish: 🦋  error 
@example-monorepo/express:publish: 🦋  success packages published successfully:
@example-monorepo/core:publish: 🦋  success packages published successfully:
@example-monorepo/express:publish: 🦋  @example-monorepo/core@0.0.1
@example-monorepo/express:publish: 🦋  Creating git tag...
@example-monorepo/core:publish: 🦋  @example-monorepo/core@0.0.1
@example-monorepo/core:publish: 🦋  Creating git tag...
@example-monorepo/express:publish: 🦋  New tag:  @example-monorepo/core@0.0.1
@example-monorepo/core:publish: 🦋  New tag:  @example-monorepo/core@0.0.1
@example-monorepo/core:publish: 🦋  error packages failed to publish:
@example-monorepo/core:publish: 🦋  @example-monorepo/express@0.0.1
@example-monorepo/express:publish: 🦋  error packages failed to publish:
@example-monorepo/express:publish: 🦋  @example-monorepo/express@0.0.1

あー、個別にpublishするとだめなのか。 一応publishできているけど、多分rootに入れれば動くっぽい

rootにchangesetを設定する

$ npm install -D @changesets/cli
$ npm run changeset init
$ npm run changeset
$ npm run changeset version
$ npm run changeset publish

> example-monorepo@0.0.0 publish
> changeset publish

🦋  info npm info @example-monorepo/core
🦋  info npm info @example-monorepo/express
🦋  info @example-monorepo/core is being published because our local version (0.0.2) has not been published on npm
🦋  info @example-monorepo/express is being published because our local version (0.0.2) has not been published on npm
🦋  info Publishing "@example-monorepo/core" at "0.0.2"
🦋  info Publishing "@example-monorepo/express" at "0.0.2"
🦋  success packages published successfully:
🦋  @example-monorepo/core@0.0.2
🦋  @example-monorepo/express@0.0.2
🦋  Creating git tags...
🦋  New tag:  @example-monorepo/core@0.0.2
🦋  New tag:  @example-monorepo/express@0.0.2

version upに成功した画面

まとめ

  • turborepo x changesetsで良さそう
  • changesetsはrootに置く
    • 必要なパッケージを選択式でpublishできる
    • 変更をstackしてまとめてbumpできる
    • publishも一括でやってくれる
  • パッケージ作成実験にverdaccioは便利