最新技術！Freestyleを使ってみた

## Freestyleの紹介
Freestyle.sh San Francisco, CA, USA
https://www.freestyle.sh/
2024年創業のスタートアップ
創業者　Benjamin Swerdlow, Jacob Zwang

Y CombinatorのWebサイトでの紹介文（翻訳）
> 私たちは、JavaScript エンジニア向けの抽象クラウドを構築しています。データベース、メッセージ キュー、ストリーミング インフラストラクチャ、認証などを、開発者が世界クラスのソフトウェアに組み込むことができるオープンソースの JavaScript パッケージに置き換えます。この機能を JavaScript でモジュール化するために、クラウドでホストされる新しい分散ランタイムを開発しました。

Webサイトでの紹介

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/9150c9ef-05dd-7188-904f-0b32f8326073.png)

ということで、TypeScriptに特化したクラウドサービスを提供し、効率的に実行できるインフラを提供していると思われる。

イメージとしては、TypeScript専用CodePenといった感じだが、GUIが無く、全てコマンドラインから実行するものなので、利用するハードルは高い。まだまだβ版のような感じ。（ドキュメントにも(beta)となっていた）

## アプリケーションを作ってみた
生成AIと組み合わせて、Freestyleをインフラとして誕生日占いを作ってみた。

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">誕生日占いのアプリを作ってみた。<br>誕生日を入力すると、今月の運勢を占ってくれます。<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://twitter.com/hashtag/%E8%AA%95%E7%94%9F%E6%97%A5%E5%8D%A0%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#誕生日占い</a> <a href="https://twitter.com/hashtag/typescript?src=hash&amp;ref_src=twsrc%5Etfw">#typescript</a> <a href="https://twitter.com/hashtag/%E7%94%9F%E6%88%90AI?src=hash&amp;ref_src=twsrc%5Etfw">#生成AI</a> <a href="https://twitter.com/hashtag/miibo?src=hash&amp;ref_src=twsrc%5Etfw">#miibo</a> <a href="https://t.co/TId3IPuR2L">pic.twitter.com/TId3IPuR2L</a></p>&mdash; ただし#オムクエ (@tadokuno) <a href="https://twitter.com/tadokuno/status/1821058969340932252?ref_src=twsrc%5Etfw">August 7, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 制作過程
今回は、インフラであるFreestyleにスポットを当てている。

思い付きで、占いでも出来ないかな、と思ってChatGPTに占いが出来るかどうか聞いてみたら、雑誌の記事程度の内容は出てきたので、生成AIを使って占いをさせる。

生成AIへのインターフェースとしては、miiboを使用し、AIのモデルはGPT-4oを使う。

Freestyleを使って、ネット上で公開する前提で開発を進める。

## Freestyleのセットアップ

開発の流れとしては、
- ローカルのNode.jsで環境を構築し、動作するプログラムを作成する。
- ビルドして、デプロイすると、公開用のURLの設定ができる。
URLは、freestyle.dev のサブドメインを定義する形で設定される。サブドメインは自分で定義できるが、使用されていないものにする必要がある。

### GitHubとの連携を取る

Freestyle.shのWebサイトから、右上のStart Buildingをクリックし、ドキュメントに飛ぶ。

![スクリーンショット 2024-08-07 145507.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/b573d963-c436-506b-0c2b-986faf0adf71.jpeg)

Getting Started With Astro のサイトに飛ぶので、Astro Templateをクリックして、GitHubのリポジトリに飛ぶ。

https://github.com/freestyle-sh/freestyle-astro-template

GitHubにはログインしておくこと。
Use this template のドロップダウンから、Create new repositoryを選択すると、自分のリポジトリとして、コピーされる。（フォークではない）

この操作をすると、アカウントが連携されるので、後でデプロイする前にログインするためにはこの作業が必須となる。

## ローカル環境の構築
Node.jsは、v19以降が必須で、LinuxかMacOSのみサポートされている。
自分はWindowsなので、WSLを使用してローカル環境を構築した。

Node.jsのデフォルトが、v12なので、以下のドキュメントを参考にアップグレードする。

https://learn.microsoft.com/ja-jp/windows/dev-environment/javascript/nodejs-on-wsl

先ほど、自分のGitHubにコピーしたリポジトリのクローンを作ってデプロイする。

```
git clone https://github.com/my-id/freestyle-template.git
npm install
npx freestyle dev
```
少し時間は掛かるが、以下のメッセージが出て、localhostでアクセス可能となる。
```
created proxy from http://localhost:8910 to http://localhost:4321
15:09:19 [WARN] The currently selected adapter `freestyle-deno-astro-adapter` is not compatible with the image service "Sharp".
15:09:19 [types] Generated 1ms

 astro  v4.13.1 ready in 393 ms

┃ Local    http://localhost:8910/
┃ Network  use --host to expose

15:09:19 watching for file changes...
```

## 公開アプリケーションとしてデプロイ
クラウドにアップできる形にビルドする。
```
npx freestyle build
```
クラウドにログインする。以下のコマンドを入力すると、Webページが開いてそこで認証プロセスが走るため、デフォルトブラウザはGitHubにログインしておく。
```
npx freestyle login
```
ローカルにあるプログラムをクラウドにデプロイする。
```
npx freestyle deploy
```

一度この手順をしておけば、次からはビルドしてデプロイすればよい。

以上の操作は必ず必要だと思われる。

## 実際のプログラムを作成
自分はReact Nativeを使いたかったので、別のサンプルプログラムを使用した。

https://github.com/freestyle-sh/freestyle-react-native-template.git

このサンプルを修正して、誕生日占いのアプリを作成する。
また、占いの結果は履歴として永続オブジェクトとして、クラウドに保存できるようにする。

追加するファイルは、履歴管理のためのモジュールで、cloudstate/history.tsとなる。また処理の中心は、app/(tabs)/index.tsx にある。
それ以外のファイルは、サンプルそのままとする。

ChatGPTに元のソースと、修正したい内容を伝えてコードを作る。
生成されたコードは以下の通り。

```typescript
// cloudstate/history.ts

import { cloudstate } from 'freestyle-sh';

@cloudstate
class History {
  static id = "history";
  entries: Array<{ number: string, fact: string }> = [];

  addEntry(entry: { number: string, fact: string }) {
    this.entries.push(entry);
console.log(entry);
  }

  getEntries() {
    return this.entries;
  }
}

export const HistoryCS = History;
```

app/(tabs)/index.tsx
```typescript
import React, { useState } from 'react';
import {
  StyleSheet,
  TextInput,
  View,
  Text,
  Button,
} from "react-native";
import axios from 'axios';
import { useCloud } from "freestyle-sh";
import { HistoryCS } from "@/cloudstate/history";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

export default function HomeScreen() {
  const history = useCloud<typeof HistoryCS>("history");
  const client = useQueryClient();
  const [date, setDate] = useState('');
  const [response, setResponse] = useState('');

  const fetchFortune = async () => {
    try {
      const apiRequest = {
        api_key: "MIIBO_API_KEY",
        agent_id: "MIIBO_AGENT_ID",
        utterance: date,
        uid: "MIIBO_UID"
      };
      const response = await axios.post('https://api-mebo.dev/api', apiRequest, {
        headers: { 'Content-Type': 'application/json' }
      });
      const fortune = response.data.bestResponse.utterance;
      setResponse(fortune);
      await history.addEntry({ date, response: fortune });
      client.invalidateQueries({
        queryKey: [history.getEntries],
        exact: true,
      });
    } catch (error) {
      console.error('Error fetching data from miibo API:', error);
      setResponse('Error fetching data.');
    }
  };

  const { data: historyEntries } = useQuery({
    queryKey: [history.getEntries],
    queryFn: () => history.getEntries(),
  });

  return (
    <View style={styles.container}>
      <Text style={styles.title}>誕生日占い</Text>

      <TextInput
        style={styles.input}
        placeholder="Enter a date (e.g., 3月3日)"
        value={date}
        onChangeText={setDate}
      />
      <Button
        onPress={fetchFortune}
        title="Get Fortune"
      />
      {response ? <Text style={styles.response}>{response}</Text> : null}

      <View style={styles.historyContainer}>
        <Text style={styles.historyTitle}>History</Text>
        {historyEntries && historyEntries.map((item, index) => (
          <View key={index} style={styles.historyItem}>
            <Text>{item.date}</Text>
            <Text>{item.response}</Text>
          </View>
        ))}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 16,
  },
  title: {
    fontSize: 32,  // H1のサイズに相当
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 20,
  },
  input: {
    height: 40,
    borderColor: 'gray',
    borderWidth: 1,
    marginBottom: 12,
    paddingHorizontal: 8,
  },
  response: {
    marginTop: 12,
    fontSize: 16,
  },
  historyContainer: {
    marginTop: 16,
  },
  historyTitle: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  historyItem: {
    marginBottom: 8,
  },
});
```

あとは、デプロイまでの手順を踏んで、アプリケーションは公開となる。

## 考察と感想
使ってみて一番困ったのが、ローカルで実行するとエラーが出るが、デプロイしたら出ない、という挙動である。
最初はバグかと思って途方にくれて、しばらくデバッグをしていたが、よくよくマニュアルを見ると、デプロイしないと動作しない処理があって、それを丁度使っていたため、エラーが出ていたことが分かった。このあたりはもっとはっきりと、ローカルで使えない機能がある点をドキュメントに記載しておいてほしかった。

コマンドラインからの操作は単純なので特に問題は無いが、実際使うには機能が不足している。例えば追加削除などは、デプロイしたプロジェクトを指定して出来るようにした欲しい。競合となる先行プロダクトは、全てGUIで豊富な機能があるので、それを使わないで、Freestyleを使うメリット等が全然分からなかった。

ただ、TypescriptやReactなどの理解が進んだので、次は皆がよく使っているインフラを使ってみて、それらと比較してこの最新技術がいかに優れているかを調べてみたい。

以上