のらねこの気まま暮らし

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

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

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は便利

App Engineを試す vol4 ~サービス同士を連携させる~

概要

AppEngine勉強日記、4日目。

この記事では、defaultサービス上のNuxtからexpressを経てapisサービスのAPIにアクセスするところを検証します。

すごい、4日連続で続いたよ。アドベントカレンダーいけんじゃね?

背景

AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。

作るサービスの仕様

  1. 題名と本文を投稿できる
  2. 投稿すると、題名と本文を閲覧できるページに遷移する

以上。いわゆるnopasteと呼ばれるサービスです。slackの時代、もう知らないひともいるんじゃなかろうか。

あ、コードやテストについては、日付更新まで2hしかなかったのでとりあえずなるはやの全力で書いたので結構雑です。 そのうちリファクタする。

defaultサービスの変更

通信用にaxiosをとりあえず入れる。

> cd root
> npm i axios

ページの作成

ついでにlayoutも作るけど省略。

./root/pages/index.vue

投稿用の画面を作る。 titlebodyを入力して、postを押下すると/apis/postsPOSTリクエストを投げる。

<template>
  <form class="form">
    <div class="form-group">
      <label for="title" class="form-label">Title</label>
      <div>
        <input type="text" id="title" class="form-control" v-model="input.title">
      </div>
    </div>
    <div class="form-group">
      <label for="body" class="form-label">Body</label>
      <div>
        <textarea id="body" rows="10" v-model="input.body"></textarea>
      </div>
    </div>
    <div class="form-group">
      <button type="button" class="btn-primary" :disabled="!is_valid" @click="submit">Post</button>
    </div>
  </form>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return {
      input: { title: null, body: null }
    }
  },
  methods: {
    is_valid() {
      return this.body
    },
    async submit() {
      const { data } = await axios.post('/apis/posts', {
        title: this.input.title,
        body: this.input.body
      })
      this.$router.push(`/posts/${data.id}`)
    }
  }
}
</script>

pages/posts/_id.vue

閲覧画面の作成。 /apis/posts/:idから文書情報を取得して表示する。

<template>
  <div v-if="content">
    <h2>{{content.title || 'no title'}}</h2>
    <div>
      {{content.body}}
    </div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return { content: null }
  },
  mounted() {
    axios.get(`/apis/posts/${this.$route.params.id}`)
      .then(({ data }) => {
        this.content = data
      })
  }
}
</script>

expressサーバにAPIを追加

POSTリクエストを受け取れるようにbodyParserをセット。

GET /apis/posts/:idPOST /apis/posts のルーティングを作成。 apisサービスのAPIを立たけるようにする。

このとき、apisサービスのオリジンは環境変数から渡せるようにする。

diff --git a/root/app.js b/root/app.js
index 35a950f..c9120af 100644
--- a/root/app.js
+++ b/root/app.js
@@ -5,17 +5,34 @@ const express = require('express');
 const app = express();
 const { Nuxt, Builder } = require('nuxt')

+app.use(express.json())
+app.use(express.urlencoded({ extended: true }));
+
 // Nuxt.js をオプションとともにインスタンス化する
 const config = require('./nuxt.config')
 config.dev = process.env.NODE_ENV !== 'production'
 const nuxt = new Nuxt(config)

-app.get('/message', (req, res) => {
-  res
-    .status(200)
-    .send('Hello, world!')
-    .end();
-});
+const axios = require('axios')
+app.post('/apis/posts', (req, res) => {
+  const data = req.body
+  axios.post(`${process.env.APIS_ORIGIN}/posts`, {
+    title: data.title,
+    body: data.body
+  })
+    .then(({ data }) => {
+      res.json({ id: data.id })
+    })
+})
+app.get('/apis/posts/:id', (req, res) => {
+  axios.get(`${process.env.APIS_ORIGIN}/posts/${req.params.id}`)
+    .then(({ data }) => {
+      res.json(data)
+    })
+    .catch(({ response }) => {
+      res.status(response.status).json(response.body)
+    })
+})

 app.use(nuxt.render)

app.yamlにapisサービスのオリジンを設定

diff --git a/root/app.yaml b/root/app.yaml
index eed1e47..6c4cf07 100644
--- a/root/app.yaml
+++ b/root/app.yaml
@@ -1 +1,4 @@
 runtime: nodejs10
+
+env_variables:
+  APIS_ORIGIN: https://apis-dot-astral-web-260712.appspot.com

deploy

> npm run build
> gcloud app deploy

これだけではまだ見れないので、続けてapisサービスを作っていく

apisサービスの変更

GET /posts/:idにリクエストがきたらサービスから任意のIDのレコードを取得して返却。

POST /postsにリクエストが来たらサービスにtitlebodyを渡してidを返却。

import { Controller, Post, Body, Get, Param, NotFoundException } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly postService: PostsService) {
  }

  @Get(':id')
  async fetchById(@Param('id') id: string): Promise<{id: number, title: null|string, body: string}> {
    const row = await this.postService.fetchById(Number(id));
    if (!row) {
      throw new NotFoundException();
    }
    return row;
  }

  @Post('')
  async save(
    @Body('title') title: null|string,
    @Body('body') body: string,
  ): Promise<{ id: number }> {
    return await this.postService.save({ title, body });
  }
}

サービスを定義する。面倒なのでメモリ上にレコードを保持する。

import { Injectable } from '@nestjs/common';

@Injectable()
export class PostsService {
  private rows: Array<{ id: number, title: string|null, body: string }>;

  constructor() {
    this.rows = [];
  }

  async fetchById(id: number) {
    return this.rows.find((row) => row.id === id);
  }

  async save({ title, body }: { title: string|null, body: string }): Promise<{ id: number }> {
    const id = this.rows.length + 1;
    this.rows.push({ id, title, body });
    return { id };
  }
}

PostsModuleを定義して、ControllerとServiceを関連付ける。

import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';

@Module({
  imports: [],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

最後にAppModuleにPostsModuleを追加して完了

diff --git a/apis/src/app.module.ts b/apis/src/app.module.ts
index 8662803..5c11c51 100644
--- a/apis/src/app.module.ts
+++ b/apis/src/app.module.ts
@@ -1,9 +1,10 @@
 import { Module } from '@nestjs/common';
 import { AppController } from './app.controller';
 import { AppService } from './app.service';
+import { PostsModule } from './posts/posts.module';

 @Module({
-  imports: [],
+  imports: [PostsModule],
   controllers: [AppController],
   providers: [AppService],
 })

deployする

> npm run build
> gcloud app deploy

動作を確認する

gcloud app browse -s defaultすると投稿画面が開く。 投稿フォーム実行画面

値を入力してPostすると表示ページに遷移する。 表示画面

できたー!

まとめ

なんとか4日目の対応を乗り切ったぜ...!

ここまで勧めてみたけど、いくつか気になる点がある。

  • メモリ上に乗せてるけどインスタンス増えたら任意のデータが取れないよね...?
  • apisサービスのURIに直接アクセスできちゃうよね...?

今後の課題にしていきます!

次回予告

というわけで、明日はデータストア周りさわってみたいなー!

App Engineを試す vol3 ~NestJSでサブシステムを動かす~

概要

AppEngine勉強日記、3日目。 この記事ではNestJSで立てたAPIサーバをAppEngineでサブシステムとして動かすとこまでを解説します。

背景

AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。

ディレクトリの整理

昨日作ったNuxtのサービスをディレクトリを掘って移動する。 defaultサービスになるのでわかり易い名前を...と思ったが、defaultでは流石にわかりにくい。 とりあえずroot命名して移動する

> git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

  renamed:    app.js -> root/app.js
    renamed:    app.yaml -> root/app.yaml
    renamed:    nuxt.config.js -> root/nuxt.config.js
    renamed:    nuxt/pages/index.vue -> root/nuxt/pages/index.vue
    renamed:    package-lock.json -> root/package-lock.json
    renamed:    package.json -> root/package.json

NestJSのセットアップ

APIサーバには最近気になっているNestJSを使う。

@nestjs/cliを入れる。

> npm i -g @nestjs/cli
> nodenv rehash

apisディレクトリを指定してプロジェクトをセットアップする。

> nest new apis
⚡  We will scaffold your app in a few seconds..

CREATE /apis/.prettierrc (51 bytes)
CREATE /apis/README.md (3370 bytes)
CREATE /apis/nest-cli.json (64 bytes)
CREATE /apis/package.json (1687 bytes)
CREATE /apis/tsconfig.build.json (97 bytes)
CREATE /apis/tsconfig.json (336 bytes)
CREATE /apis/tslint.json (426 bytes)
CREATE /apis/src/app.controller.spec.ts (617 bytes)
CREATE /apis/src/app.controller.ts (274 bytes)
CREATE /apis/src/app.module.ts (249 bytes)
CREATE /apis/src/app.service.ts (142 bytes)
CREATE /apis/src/main.ts (208 bytes)
CREATE /apis/test/app.e2e-spec.ts (630 bytes)
CREATE /apis/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? npm
✔ Installation in progress... ☕

🚀  Successfully created project apis
👉  Get started with the following commands:

$ cd apis
$ npm run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest

こんな感じのディレクトリが作成される。

> ls -lah apis/
total 848
drwxr-xr-x   15 mizuki  staff   480B 12  3 23:12 .
drwxr-xr-x    9 mizuki  staff   288B 12  3 23:11 ..
drwxr-xr-x    9 mizuki  staff   288B 12  3 23:11 .git
-rw-r--r--    1 mizuki  staff   375B 12  3 23:11 .gitignore
-rw-r--r--    1 mizuki  staff    51B 12  3 23:11 .prettierrc
-rw-r--r--    1 mizuki  staff   3.3K 12  3 23:11 README.md
-rw-r--r--    1 mizuki  staff    64B 12  3 23:11 nest-cli.json
drwxr-xr-x  668 mizuki  staff    21K 12  3 23:12 node_modules
-rw-r--r--    1 mizuki  staff   389K 12  3 23:12 package-lock.json
-rw-r--r--    1 mizuki  staff   1.6K 12  3 23:11 package.json
drwxr-xr-x    7 mizuki  staff   224B 12  3 23:11 src
drwxr-xr-x    4 mizuki  staff   128B 12  3 23:11 test
-rw-r--r--    1 mizuki  staff    97B 12  3 23:11 tsconfig.build.json
-rw-r--r--    1 mizuki  staff   336B 12  3 23:11 tsconfig.json
-rw-r--r--    1 mizuki  staff   426B 12  3 23:11 tslint.json

とりあえずローカル環境でサーバを起動する。

> cd apis
> npm start
> apis@0.0.1 start /Users/mizuki/workspace/20191203-example-appengine/apis
> nest start

[Nest] 47954   - 2019-12-03 11:15:03 PM   [NestFactory] Starting Nest application...
[Nest] 47954   - 2019-12-03 11:15:03 PM   [InstanceLoader] AppModule dependencies initialized +18ms
[Nest] 47954   - 2019-12-03 11:15:03 PM   [RoutesResolver] AppController {/}: +5ms
[Nest] 47954   - 2019-12-03 11:15:03 PM   [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 47954   - 2019-12-03 11:15:03 PM   [NestApplication] Nest application successfully started +2ms
> curl -v localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 12
< ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
< Date: Tue, 03 Dec 2019 14:15:52 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
Hello World!%

プロジェクトをデプロイする

例によってapp.yamlを作成する。 今回はdefaultのサービスではなく、サブシステムとなるapisserviceに指定する。

> echo '
service: apis
runtime: nodejs10
' > app.yaml

apisディレクトリでデプロイコマンドを実行する。 以下のようにターゲットが表示されるので、問題なければyで実行。 target urlhttps://apis-dot-astral-web-260712.appspot.comになっていることに注目。

> gcloud app deploy
Services to deploy:

descriptor:      [/Users/mizuki/workspace/20191203-example-appengine/apis/app.yaml]
source:          [/Users/mizuki/workspace/20191203-example-appengine/apis]
target project:  [astral-web-260712]
target service:  [apis]
target version:  [20191203t232045]
target url:      [https://apis-dot-astral-web-260712.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [apis]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 29 files to Google Cloud Storage               ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [apis]...done.
Setting traffic split for service [apis]...done.
Deployed service [apis] to [https://apis-dot-astral-web-260712.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s apis

To view your application in the web browser run:
  $ gcloud app browse -s apis

さー、デプロイできたぞー、と思ったがーー

> curl https://apis-dot-astral-web-260712.appspot.com

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>500 Server Error</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Server Error</h1>
<h2>The server encountered an error and could not complete your request.<p>Please try again in 30 seconds.</h2>
<h2></h2>
</body></html>

ーーなんかエラーになるな... 困ったときはとりあえずログを確認。-sでサービスを指定する。

> gcloud app logs tail -s apis
Waiting for new log entries...
2019-12-03 14:22:46 apis[20191203t232045]  "GET / HTTP/1.1" 500
2019-12-03 14:22:46 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:22:48 apis[20191203t232045]  "GET /favicon.ico HTTP/1.1" 500
2019-12-03 14:22:49 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:22:52 apis[20191203t232045]  "GET / HTTP/1.1" 500
2019-12-03 14:22:52 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:22:52 apis[20191203t232045]  "GET /favicon.ico HTTP/1.1" 500
2019-12-03 14:22:53 apis[20191203t232045]  sh: 1: exec: nest: not found
2019-12-03 14:23:22 apis[20191203t232045]  "GET / HTTP/1.1" 500
2019-12-03 14:23:22 apis[20191203t232045]  sh: 1: exec: nest: not found

nest not foundらしい... @nestjs/clidevDependenciesに含まれているため、インストールされないようだ。

npm startのコマンドからnestの依存を外す。

diff --git a/package.json b/package.json
index 6f344f7..01bd10c 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
     "prebuild": "rimraf dist",
     "build": "nest build",
     "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
-    "start": "nest start",
+    "start": "npm run start:prod",
     "start:dev": "nest start --watch",
     "start:debug": "nest start --debug --watch",
     "start:prod": "node dist/main",

さあさ、デプロイデプロイっ

> gcloud app deploy
Services to deploy:

descriptor:      [/Users/mizuki/workspace/20191203-example-appengine/apis/app.yaml]
source:          [/Users/mizuki/workspace/20191203-example-appengine/apis]
target project:  [astral-web-260712]
target service:  [apis]
target version:  [20191203t233112]
target url:      [https://apis-dot-astral-web-260712.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [apis]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 1 file to Google Cloud Storage                 ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [apis]...done.
Setting traffic split for service [apis]...done.
Deployed service [apis] to [https://apis-dot-astral-web-260712.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s apis

To view your application in the web browser run:
  $ gcloud app browse -s apis

ログを起動しとく

> gcloud app logs tail -s apis
2019-12-03 14:33:24 apis[20191203t233112]
2019-12-03 14:33:24 apis[20191203t233112]  > apis@0.0.1 start:prod /srv
2019-12-03 14:33:24 apis[20191203t233112]  > node dist/main
2019-12-03 14:33:24 apis[20191203t233112]
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [NestFactory] Starting Nest application...
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [InstanceLoader] AppModule dependencies initialized +21ms
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [RoutesResolver] AppController {/}: +6ms
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [RouterExplorer] Mapped {/, GET} route +3ms
2019-12-03 14:33:25 apis[20191203t233112]  [Nest] 28   - 12/03/2019, 2:33:25 PM   [NestApplication] Nest application successfully started +3ms

満を持してcurlを叩くーー

> curl https://apis-dot-astral-web-260712.appspot.com

ーーが、レスポンスが帰ってこない!なんぞ!? 首をかしげながら考える私。 んー通信出来へんてこたぁサービス起動してへんのかな? やー、ゆうてもログでは起動しるやん?

... ... ...あっ 唐突に思いつく私。

diff --git a/src/main.ts b/src/main.ts
index 13cad38..7357152 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,6 +3,6 @@ import { AppModule } from './app.module';

 async function bootstrap() {
   const app = await NestFactory.create(AppModule);
-  await app.listen(3000);
+  await app.listen(Number(process.env.PORT) || 3000);
 }
 bootstrap();

あー

> npm run build
> gcloud app deploy

ああー

> gcloud app logs tail -s apis
2019-12-03 14:40:25 apis[20191203t233845]  > apis@0.0.1 start:prod /srv
2019-12-03 14:40:25 apis[20191203t233845]  > node dist/main
2019-12-03 14:40:25 apis[20191203t233845]
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [NestFactory] Starting Nest application...
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [InstanceLoader] AppModule dependencies initialized +23ms
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [RoutesResolver] AppController {/}: +7ms
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [RouterExplorer] Mapped {/, GET} route +25ms
2019-12-03 14:40:27 apis[20191203t233845]  [Nest] 28   - 12/03/2019, 2:40:27 PM   [NestApplication] Nest application successfully started +3ms

あ〜〜〜〜〜〜〜

> curl https://apis-dot-astral-web-260712.appspot.comHello World!%

まとめ

AppEngineかんたんでとてもわかり易いですね!!!!!

次回予告

サービス2つ作っただけじゃね? 明日は連携できるようにコード書きます!がんばります!