Sendai Frontend Meetup #9でLTしてきた

sfeug.connpass.com

場所はいつものとこ。 今回は新しい顔ぶれが多かったかも。

今回の話も資料に中身がないのでデモ・コードで察して。大爆笑よ。知らんけど。

資料: バーチャル背景の話 - Speaker Deck

デモ: Sendai Frontend Meetup #9 | sfeug9

コード: GitHub - yohfee/sfeug9: Sendai Frontend Meetup #9

@kan_dai さんの発表。前半見れなかったけど面白そうなことやっててよい。 マージンを使わずに Webサイト構築してみた - Google スライド

@akathea_ さんの発表。package.jsonの奥の深さというか業の深さというか闇の深さを感じたが、深いのは懐だそうなのでそういうことにしておけ。 package.json がすごい - Speaker Deck

  • はてなブックマークのユーザーテストしてました
  • レガシーJava屋が昨今のフロントエンドに立ち向かうには
  • 大手SIerの若者がコードを書きたい話
  • T社ェ。。。
  • どこまでがフロントエンドなの(発注的な意味で)
  • 羊先輩はシンガポールに高跳びしたんだった
  • 各某社のフロントエンドの話
  • R社ハイイカイシャデスヨ
  • Chromatic使いたいから事例を募集
  • 円卓の騎士とは
  • 仙台支社です・フルリモートです
  • 甲子園北海道大会大反省会
  • スプラトゥーンのおっさんコミュニティがありましてね
  • 名取・富谷・利府・多賀城は仙台
  • 最近仙台にやってきました・戻ってきましたな人が意外と多い

みたいな話を懇親会でした気がする。

次回はまた2-3カ月後だろうから今度はプロダクトに絡む話もできるくらいだといいな。

Microsoft Build ふりかえり!Azure & AI 勉強会 JAZUG TOHOKU 行ってきた

jazug.connpass.com

前回の Microsoft Azure & AI 勉強会 JAZUG TOHOKU 行ってきた - @yohfee.blog! の次回予告だったやつ。 ほぼ身内しか来てなくて、狭い仙台界隈をさらに煮詰めた感がすごい。

事前に大筋は把握してたけど、案の定AI話がめっぽう多かった。 そんなご時世なのでIoTのオワコン感がすごくて茂出木さんの話が哀愁を誘う。 これからどこにでも現れるCopilot君は副操縦士なんだからお前が操縦するんだよは、それはそうというお気持ちでやっていこうぜ。

今回は個人的な気になりポイントは特に無い。 AzureやWindowsでAI以外の大きな話題が少なかったからだろう。 強いて言えばWindows Dev Homeが便利だといいですねーくらいかな。 とはいえ.NETやASP.NETは順当に進化してるので、いつも通りならまた11月をお楽しみにというところ。

教えてもらったクラウドスキルチャレンジは、おまけの試験を受けることはないだろうけど勉強になるのでゆるっとやってる。

www.microsoft.com

今のお仕事がもうちょいいいとこまで行けたら、この集まりに合うようなAzureや.NET周りのいい話ができそうな気がしなくもない。

前回で傾向が分かったので対策する。

yohfee.hatenadiary.org

ライブラリっぽい部分。

namespace Mackerel

open System.Net
open System.Text.Json.Serialization
open FsHttp

type Options = { BaseUri: string; ApiKey: string }

module HttpClient =
    let send<'t> request =
        async {
            let! response = request |> Request.sendAsync

            match response.statusCode with
            | HttpStatusCode.OK ->
                let! data = response |> Response.deserializeJsonAsync<'t>
                return Ok data
            | e -> return Error e
        }

    let get<'t> (path: string) param options =
        http {
            GET $"{options.BaseUri}{path}"
            header "X-Api-Key" options.ApiKey
            query param
        }
        |> send<'t>

    let post<'t> (path: string) param options =
        http {
            POST $"{options.BaseUri}{path}"
            header "X-Api-Key" options.ApiKey
            body
            jsonSerialize param
        }
        |> send<'t>

    let put<'t> (path: string) param options =
        http {
            PUT $"{options.BaseUri}{path}"
            header "X-Api-Key" options.ApiKey
            body
            jsonSerialize param
        }
        |> send<'t>

    let delete<'t> (path: string) options =
        http {
            DELETE $"{options.BaseUri}{path}"
            header "X-Api-Key" options.ApiKey
            body
            json ""
        }
        |> send<'t>

type Org =
    { [<JsonPropertyName("name")>]
      Name: string }

module Org =
    let get = HttpClient.get<Org> "/api/v0/org" []

type Service =
    { [<JsonPropertyName("name")>]
      Name: string

      [<JsonPropertyName("memo")>]
      Memo: string

      [<JsonPropertyName("roles")>]
      Roles: string list }

module Service =

    let findAll = HttpClient.get<{| services: Service list |}> "/api/v0/services" []

    type CreateParam =
        { [<JsonPropertyName("name")>]
          Name: string

          [<JsonPropertyName("memo")>]
          Memo: string }

    let create (param: CreateParam) =
        HttpClient.post<Service> "/api/v0/services" param

    let delete (serviceName: string) =
        HttpClient.delete<Service> $"/api/v0/services/{serviceName}"

    let listMetricNames (serviceName: string) =
        HttpClient.get<{| names: string list |}> $"/api/v0/services/{serviceName}/metric-names" []

type User =
    { [<JsonPropertyName("id")>]
      ID: string

      [<JsonPropertyName("screenName")>]
      ScreenName: string

      [<JsonPropertyName("email")>]
      Email: string

      [<JsonPropertyName("authority")>]
      Authority: string

      [<JsonPropertyName("isInRegistrationProcess")>]
      IsInRegistrationProcess: bool

      [<JsonPropertyName("isMFAEnabled")>]
      IsMFAEnabled: bool

      [<JsonPropertyName("authenticationMethods")>]
      AuthenticationMethods: string list

      [<JsonPropertyName("joinedAt")>]
      JoinedAt: int64 }

module User =

    let findAll = HttpClient.get<{| users: User list |}> "/api/v0/users" []

    let delete (userId: string) =
        HttpClient.delete<User> $"/api/v0/users/{userId}"

アプリケーションっぽい部分。

open Mackerel

let run t =
    t |> Async.RunSynchronously |> printfn "%A"

let options =
    { BaseUri = "https://api.mackerelio.com"
      ApiKey = "*********************************************" }

module Org =
    open Org

    get options |> run

module Service =
    open Service

    findAll options |> run

    ({ Name = "Service888"
       Memo = "Service 888" },
     options)
    ||> create
    |> run

    ("Service888", options) ||> listMetricNames |> run

    ("Service888", options) ||> delete |> run

module User =
    open User

    findAll options |> run

FsHttp 便利~。

自由にできる非公式なMackerelクライアントライブラリを持っとくと便利そうな予感を得たので、まずは素朴にちょいと素振りして傾向と対策の勘所を。

取り合えずサービスの上3つからお試し。

mackerel.io

open System
open System.Net
open System.Net.Http
open System.Text
open System.Text.Json

type NewService = { name: string; memo: string }

type Service =
    { name: string
      memo: string
      roles: string list }

type Services = { services: Service list }

let baseUrl = "https://api.mackerelio.com"
let key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

let run task =
    task
    |> Async.AwaitTask
    |> Async.RunSynchronously
    |> function
        | Ok res -> printfn "%A" res
        | Error err -> printfn "%s" err

let getServices (baseUrl: string) (key: string) =
    task {
        let uri = Uri $"{baseUrl}/api/v0/services"
        use request = new HttpRequestMessage(HttpMethod.Get, uri)
        request.Headers.Add("X-Api-Key", key)
        use httpClient = new HttpClient()
        let! response = httpClient.SendAsync request

        return!
            match response.StatusCode with
            | HttpStatusCode.OK ->
                task {
                    let! content = response.Content.ReadAsStreamAsync()
                    let! services = JsonSerializer.DeserializeAsync<Services>(content)
                    return Ok services
                }
            | e -> task { return Error $"Unexpected response: {e}" }
    }

getServices baseUrl key |> run

let createService (baseUrl: string) (key: string) (service: NewService) =
    task {
        let json = JsonSerializer.Serialize service
        use content = new StringContent(json, Encoding.UTF8, "application/json")
        let uri = Uri $"{baseUrl}/api/v0/services"
        use request = new HttpRequestMessage(HttpMethod.Post, uri, Content = content)
        request.Headers.Add("X-Api-Key", key)
        use httpClient = new HttpClient()
        let! response = httpClient.SendAsync request

        return!
            match response.StatusCode with
            | HttpStatusCode.OK ->
                task {
                    let! content = response.Content.ReadAsStreamAsync()
                    let! service = JsonSerializer.DeserializeAsync<Service>(content)
                    return Ok service
                }
            | HttpStatusCode.BadRequest -> task { return Error "Invalid service name" }
            | HttpStatusCode.Forbidden -> task { return Error "Unpermitted request" }
            | e -> task { return Error $"Unexpected response: {e}" }
    }

{ name = "Service99"; memo = "めも" }
|> createService baseUrl key
|> run

let deleteService (baseUrl: string) (key: string) (serviceName: string) =
    task {
        use content = new StringContent("", Encoding.UTF8, "application/json")
        let uri = Uri $"{baseUrl}/api/v0/services/{serviceName}"
        use request = new HttpRequestMessage(HttpMethod.Delete, uri, Content = content)
        request.Headers.Add("X-Api-Key", key)
        use httpClient = new HttpClient()
        let! response = httpClient.SendAsync request

        return!
            match response.StatusCode with
            | HttpStatusCode.OK ->
                task {
                    let! content = response.Content.ReadAsStreamAsync()
                    let! service = JsonSerializer.DeserializeAsync<Service>(content)
                    return Ok service
                }
            | HttpStatusCode.Forbidden -> task { return Error "Unpermitted request" }
            | HttpStatusCode.NotFound -> task { return Error "Service not found" }
            | e -> task { return Error $"Unexpected response: {e}" }
    }

"Service99" |> deleteService baseUrl key |> run

サービスの削除でボディ無しでもContent-Type: application/jsonが無いとBad Request返ってくるのにちょっとハマった。

なるほど把握した。ボチボチやってこ。

隣のビルの工事がうるさい

音楽を聴きながら仕事する

たまたまチップチューンがレコメンドされる

やってみたくなる

やり方を色々調べる

いきなりDAWはハードル高い

FamiStudioは感覚が合わないのとやっぱGitとかで管理したい

MMLに目をつける

ファミコンがどうやって音楽を扱ってるのか気になる

ファミコンのROMを作ればいいのではという気持ちになる

ファミコンサウンドシステムが知りたくなる

ファミコンエミュレータを作ればいいのではという気持ちになる

Rustやるか

Fable4 on Wails

Fable4が出てたので。

$ dotnet --version
7.0.100

$ go version
go version go1.20.3 windows/amd64

$ node -v
v19.9.0

今回はいきなりFelizでやってく。

$ dotnet new --install Feliz.Template

一応Wailsも。

$ go install github.com/wailsapp/wails/v2/cmd/wails@latest

前回同様まずはWailsアプリとして生成する。オプションは適当でテンプレートは無しで。

$ wails init -d hello -n hello -g -ide vscode
$ cd hello

生成されたフロントエンドを消してFelizで生成したものに置き換える。

$ cd frontend
$ rm -rf *
$ dotnet new feliz

Felizのテンプレは元々Viteでビルドするようになっているので前回より変更が少ない。

diff --git a/frontend/package.json b/frontend/package.json
index 7d6e90d..8748471 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,8 +1,9 @@
 {
   "private": true,
   "scripts": {
-    "start": "dotnet tool restore && dotnet fable watch src --runFast vite",
+    "dev": "dotnet tool restore && dotnet fable watch src --runFast vite",
     "build": "dotnet tool restore && dotnet fable src --run vite build",
+    "preview": "dotnet tool restore && dotnet fable src --run vite preview",
     "clean": "dotnet fable clean src --yes"
   },
   "dependencies": {

こんだけ。

$ npm install

楽~。

$ cd ..
$ wails dev