kasuのブログ

勉強していく

Remix & Cloudflare Pages で KV を触る

作業ログです。

Remix のアプリを Cloudflare Pages にデプロイして KV にアクセスするまでをやってみました。

github.com

結果としては、id:leader22 さんの記事*1と同じところにハマりました。 こちらの記事も参考になるかと思います。

違いはTypeScript用に型定義を用意したくらいでしょうか。

create-remix

とりあえず、脳死create-remixしていきます。

> npx create-remix@latest`
> Just the basics
> Cloudflare Pages
> TypeScript
> Run npm install ? > n

yarn を使いたいのでnpm installは断っておきます。

さて、テンプレートが展開されて完成しました。よかったですね。

とはいえ、このままyarn devyarn buildを行うとNo matches found: "dev:*"のように怒られてしまいますので修正しておきます。

- "build": "run-s build:*",
+ "build": "run-s 'build:*'",
- "dev": "remix build && run-p dev:*",
+ "dev": "remix build && run-p 'dev:*'",

azu on Twitter: "No matches found: "dev:*"ってなるのなんでだろな。 npm-run-all -p dev:api dev:worker みたいな個別指定ならいけるな。 * がどこかで展開されてるのかな?"

KV access in Pages Functions

間違ったやり方

私が躓いた、Cloudflare Pages にとって間違っていた方法です。 「remix cloudflare kv」などで調べると、その多くが Cloudflare Workers での実装なのでそのまま写すと動いてくれません。

Remix | Data Loading

Remix のドキュメントでは KV の名前をそのまま呼べると書かれており、加えてそれ用の型定義を宣言しておくことをオススメされます。

例えば KV の名前がGOOD_KVNAMEだとすると、実装イメージは以下のような感じです。

// global.d.ts
declare const GOOD_KVNAME: KVNamespace

// app/routes/index.tsx
export const loader: LoaderFunction = async () => {
  const cached = await GOOD_KVNAME.get<Cache>('cache-key', 'json')
  ...
}

RemixをCloudflare Workersで動かす & KVでデータをキャッシュする

しかし、これらは Cloudflare Pages の環境では動きません。ではどうしたら良いのでしょうか?

正しいやり方

まずはローカルで実行するために、package.jsonにあるdevスクリプトに手を加えます。

https://developers.cloudflare.com/pages/platform/functions/#develop-and-preview-locally

ドキュメントに従って、静的アセットのディレクトリを指定した後ろにKV_NAMESPACEを渡します。

package.json
- "dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public",
+ "dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public -k GOOD_KVNAME",

ローカルでの準備が終わったので、yarn devで開発環境を立ち上げてみます。すると、以下のようなエラーが返ってきます。

You must use the 2nd `env` parameter passed to exported handlers/Durable Object constructors, or `context.env` with Pages Functions.

どうやら、Pages Functions から KVNamespace を指定するには context.env から取得する必要があるようです。

プロジェクトルートにある、テンプレートから生成されたserver.jsを見てみると以下のような実装になっています。 この実装から、ルーティングファイルにあるloaderに渡ってくるリクエストのcontextcontext.envの値が入っているのが分かります。

// server.js
const handleRequest = createPagesFunctionHandler({
  build,
  mode: process.env.NODE_ENV,
  getLoadContext: context => context.env,
})

つまり、ルーティングファイルのloaderに渡ってくる context から、以下のように呼び出すことが出来ます。

const loader: LoaderFunction = async ({context}) => {
  const kv = context.GOOD_KVNAME as KVNamespace
  ...
}

とはいえ、context: anyを無理やりKVNamespaceへキャストするのもムズムズするので型定義を用意します。

// global.d.ts
import type {AppData, DataFunctionArgs} from '@remix-run/cloudflare'

interface LoaderFunctionArgs extends Omit<DataFunctionArgs, 'context'> {
  context: {
    GOOD_NAMESPACE: KVNamespace
  }
}

type LoaderFunctionResult = Promise<Response> | Response | Promise<AppData> | AppData

declare global {
  type LoaderFunction = (args: LoaderFunctionArgs) => LoaderFunctionResult
}

型定義を用意したことで、キャストの必要がなくなり以下のような実装になりました。

// app/routes/index.tsx
import {useLoaderData} from '@remix-run/react'

type Cache = {
  a: number
}

type LoaderData = {
  pageCache: Cache
}

export const loader: LoaderFunction = async ({context}): Promise<LoaderData> => {
  const kv = context.GOOD_KVNAME
  const cached = await kv.get<Cache>('cache-key', 'json')
  const updateCache = {...cached, a: (cached?.a ?? 0) + 1}
  await kv.set('cache-key', updateCache)
  return updateCache
}

以上でとりあえずは動くようになりました。

やっと開発が始められます。やったね。

cdkで定義したDynamoDBのテーブルをDynamoDBLocalに作る

この記事はPRを含みます

cdkを使っている場合のDynamoDBLocalには、どのようにテーブルを作ってますか?

皆さんは、アプリケーションの構成をcdkによって定義している際のローカル開発環境のDynamoDBのテーブル作成はどうしていますか。

LocalStack - A fully functional local cloud stack

localstackを用いた、cdk deployによってローカル開発環境のDynamoDBを用意している人も多いかと思います。

しかし、localstackはDynamoDBを使うだけなのに起動サービスにcloudformationを必要としていたりと、DynamoDBだけ欲しいんじゃーというかたには大げさではないでしょうか。

amazon/dynamodb-local

localstack以外でローカル環境にDynamoDBを用意する方法としては、Dockerイメージのamazon/dynamodb-localを使うのが一般的でしょうか。*1

dynamodb-localaws dynamodbコマンドで操作が出来ます。

例えばテーブルを作成するには、テーブル定義を書いたtable.jsonを用意してaws dynamodb create-table --cli-input-json file://table.jsonとコマンドを実行します。 以後紹介する方法はこれを利用してテーブルを作成することになります。

cdk synth(synthesize)

aws dynamodbでテーブルを作成するにはjsonファイルが必要になります。

そこで、cdk synthを実行してみましょう。 すると、cdk.outディレクトリへDynamoDBのテーブル定義が吐き出されます。

このファイル自体はCloudFormationテンプレートなので、ブログタイトルのcdkで定義したテーブルというよりはCloudFormationテンプレートのテーブル定義からDynamoDBLocalにテーブルを作成する方法と言った方が正しいかもしれません。

とはいえ、cdk synsthした結果をDynamoDBLocalのテーブルを作成するにはどんな手段があるのか見ていきましょう。

cloudformation-dynamodb-export · PyPI

これはPythonで書かれたスクリプトです。使用する際は、pipでインストールします。 最後に紹介する方法に変更する前はこちらを利用していました。

CloudFormationの定義からDynamoDB localのTableを作成する

こちらはRuby製のスクリプトです。MacならRubyが既に入っているので、そのまま使えそうですね。

npm scriptsで完結させる

いろいろ方法はありましたが、せっかくaws-cdkをnpmで管理しているので、npm完結したいですよね?

なにか良さげなパッケージはないのでしょうか?

ここに良さげなパッケージがあるので、こちらを使いましょう。

ErgoFriend/cf-to-dynamodb-schema

www.npmjs.com

実際にaws-cdkを使った/exampleフォルダがあるのでそこを見るとよくわかると思います。

使う際には、下記のようにcdk synthした結果のCloudFormationテンプレートを指定すると、ローカル環境のDynamoDBにテーブルが出来上がります。

cf-to-dynamodb-schema create-table ./cdk.out/SampleStack.template.json -e http://localhost:8000 -p c2dexample

*1:serverless-dynamodb-localもあると思いますが、今回はcdkをメインとしたいのでおいておきましょう。

TypeScript: 渡した値が返ることを保証したい

メモです

例えばstyle要素のように、渡せる値が限られているプロパティに動的な値を渡したいとします。

このとき、渡す型を明示しないで戻り値の型を判別可能な関数を用意します。 イメージとしては以下のような型を持つpositionに、indexに応じて動的に値を渡すことになります。

type Position = "absolute" | "static"
type Top = number
type Style = {position: Position; top: Top;}
const style: Style = {
  // error: Type 'number | "static"' is not assignable to type 'Position'.
  position: getStyle(index, {first: 100, second: "static"}),
  // ok
  top: getStyle(index, {first: 100, second: 50}),
}

上記の例では、getStyle()にはpositionとtopが取りうるPosition,Topどちらもを渡すことができますが、positionプロパティはPosition型のみを受け入れるのでgetStyleへ渡した数値100が返る可能性があるとエラーで教えてくれます。

1. Generics

TypeScript: Documentation - Generics

最初に思いついたGenericsを下記のように使ってみます。

const getStyle = <T extends StyleValue>(index, style: { first: T; second: T; }): T => { .. }
const style: Style = { position: getStyle(0, { first: "static", second: 0 })}
> Type 'number' is not assignable to type '"static"'.

もちろんgetStyle<Position | Top>(...)とすれば可能ですが、今回は渡した値によって返す型を決めたいのでこの方法は取りません。

2. プロパティごとにGenericsを用意するする

TypeScript: Documentation - Generics

Genericsは複数の型を渡せるので、それぞれのプロパティに用意してみます。

const getStyle = <A extends StyleValue, B extends StyleValue>(index,
  style: { first: A; second: B; }): A | B => { ... }

戻り値をUnion型で指定できるのでよさそうです。 しかしstyleへ渡すプロパティが増えていくと、A|B|C|D|...冗長化しそうです。

3. Indexed Access Types

TypeScript: Documentation - Indexed Access Types

冗長化を防ぐ方法として、Indexed Access Typesというものがあります。 これはその型のプロパティを呼ぶことで、その型を入手することが出来ます。 type User = { id: number; name: string; }という型を定義すると、User["id"]でidの型であるnumberを取得できます。 また、keyof UserでUser型が持つプロパティ名のUnion型を得ることが出来ます。それをUser型に渡すと以下ようになります。

User[keyof User] ≒ User["id"|"name"] ≒ User["id"] | User["name"] ≒ number | string
type StyleMap = {
  first: StyleValue;
  second: StyleValue;
};
const getStyle = <T extends StyleMap>(index, style: T): T[keyof StyleMap] => { ... };

プロパティの数だけ宣言するよりもスッキリました。 Indexed Access Typesでも、渡したstyleの各プロパティが持つ値のUnion型を返すことが出来ているのでよさそうです。

使っているnpmパッケージの名前が変わってた

依存パッケージのアップデートはしていますか?

フロントエンドの開発を行っていく中で依存パッケージのアップデートは欠かせないものだと思います。

例えば、以下のようなnpm updateyarn upgradenpm-check-updatesコマンドラインから実行したり、GitHub Appで自動化するならRenovate や Dependabot を使っているのではないでしょうか。

今回は、4ヶ月もアップデートから取り残されていたパッケージを見つけたお話です。

パッケージ名が変わっていた

今回は開発中にreact-use-gestureのドキュメントを読みに行く際に、パッケージ名前が@use-gesture/reactへと変更していたことに気が付きました。

名前が変更された後の最初の安定版10.0.0が4ヶ月前にリリースされていたようです。

パッケージ名の変更を知る

一般的にnpmではパッケージが非推奨になるとdeprecatedが付きます。

今回パッケージ名が変更されていた react-use-gesture では、以下のように表示されています。

This package has been deprecated
Author message:

This package is no longer maintained. Please use @use-gesture/react instead

最近更新されていないパッケージを察してnpmレジストリのページへ毎回見に行くわけにもいきません。 では、どのような方法で非推奨かを知ると良いのでしょうか?

パッケージの情報をみる

Changelogや噂を追ってない限りは、 npm レジストリに対してパッケージの情報を問い合わせて非推奨かどうかを確認するしかないようです。

❯ npm info react-use-gesture | grep -i deprecated
DEPRECATED ⚠️  - This package is no longer maintained. Please use @use-gesture/react instead

❯ yarn info react-use-gesture | grep deprecated
  deprecated: 'This package is no longer maintained. Please use @use-gesture/react instead'

jq

会社のSlackで聞いた際に、id:utgwkk が教えてくださったissueには jq を駆使して グラフィカルに表示してくれるスクリプトがありました。 npmコマンドを叩いて、使用しているパッケージが非推奨かをビジュアライズしてくれます。

github.com

check-is-deprecated

これも内部的にはnpmコマンドを叩いているようです。 package.jsonのファイル名指定すると、非推奨のパッケージを検出してくれます。

github.com

❯ check-is-deprecated -f package.json
react-use-gesture:
✖ npm
❯ check-is-deprecated -f package.json
🎉 All is OK!

自動化したい

cliを実行する

最初に思いつくのは、上記の手動でcliを叩くものをGitHub Actionsなどで実行する方法です。

tinovyatkin/action-check-deprecated-js-deps

似たようなことを GitHub Actions で公開されているものがありました。 npm レジストリに対してパッケージの情報を問い合わせて、非推奨かどうかを確認しているようです。

github.com

Renovate Replacement Presets

今回、調べていくなかで Renovate の issue に似たような話題をみつけました。

github.com

これは、なんらかのパッケージ名が変更された場合に変更後のパッケージ名に置き換える機能を用意しよう。というものでした。

issueが作られたのが2018年、PRの作成日が2020年2月でマージされたのが2021年11月だったので長らく議論していたようです。

紆余曲折あり最終的にリリースされたのは、変更前後のパッケージ名をマッピング出来るReplacement Presetsという機能のようです。

docs.renovatebot.com

この機能は、自動という訳ではなく変更後のパッケージ名が判明してから設定する必要があります。

今回のreact-use-gestureなら以下のように書くことが出来ます。

{
  "packageRules": [
    {
      "matchDatasources": [
        "npm"
      ],
      "matchPackageNames": [
        "react-use-gesture"
      ],
      "replacementName": "@use-gesture/react",
      "replacementVersion": "10.0.0"
    }
  ]
}

まとめ

長く開発・運用をしていると、いつのまにか非推奨になっているパッケージもあるかと思います。 普段の依存更新に加えて、パッケージ名の変更はどうのように追えばよいのか調べてみました。

現時点では、使用しているパッケージの名前が変更されているかはnpmレジストリを直接確認していくしかないようです。

かっこよく自動で変更後のパッケージ名をsuggestする機能を作りたいですね。

Vue3でFirabaseログイン

zenn.dev

2年前に書いたVue vuexでfirebaseのログイン保持を、Vue3+Composition APIで書き直したものです。

未だにちょくちょく読まれているようなのですが、流石に2年前のバージョンのチュートリアルは申し訳ないと感じたので書き直すことにしました。 需要があるかはさておき。

1年ぶりくらいにVueを書きましたが、やっぱり書きやすいですね。 TypeScriptから逃げられないのでReactを選んでいますが、Vueが完全に対応したらVueを選んでもいいかもしれない。

引越しました 2020

引越し 2020

諸事情により引っ越すことになり、大学の近くへ引っ越しました。

あと1~2年だけだけど。

f:id:ergofriend:20200112010618j:plain

手続き

初めての引越しだったのでめっちゃ調べた。あんまやることは無い。

  1. 旧役所で転出届を出す
  2. 新役所で転入届を出す
  3. 新役所に年金手帳持ってく
  4. 警察署で免許の住所変更

So-net、12/29に申し込んだら開通1/14って言われた。おっそ〜い。 (工事なし)

無派遣工事で24,000円とるのぴえん。

費用

初期費用は¥208,367

  • 敷金礼金なし、クリーニング・仲介手数料などで10万した
  • 家賃は木造 7帖の1Kで¥68,000 
家具

小物とか合わせて¥25,000

家電

冷蔵庫・洗濯機・電子レンジ・PCデスクの合計¥55,000

その他

趣味的なもの。合計¥35,000