Rails7でSvelteを使えるようにする

Rails7でSvelteを使う方法ってないのかな？と探していたんですが、全く見つからなかったので自分で試行錯誤した記録をここに記します。

やったことの軌跡がわかるようにPRを作ってありまして、基本このコミットの通りにやれば使えるようになるはず。

https://github.com/yuki-noguchi/svelte-rails7/pull/1

# 手順

## rails newする
このとき、`-J`や`-j`オプションをつけて、javascript関連のファイルは生成されないようにしたほうが良いかもです。
後述しますが、`vite_rails`を使用するので、生成されるjavascript関連のファイルが邪魔になるかもです。
自分は`--minimal`をつけちゃいました。

## 'vite_rails'を追加する

Gemfileに以下を追記して、

```
gem 'vite_rails'
```

bundle install実行。

```shell
bundle install
```

さらに、初期化コマンドを実行。

```
bundle exec vite install
```

これでいろんなファイルが生成されます。

### vite_railsについて

そもそもVite Rubyなるものがありまして、ViteとRubyをいい感じに統合してくれるツールです。

https://vite-ruby.netlify.app/

そしてその中で、rails用の`vite_rails`というものがあるので、これをインストールしている感じです。

https://github.com/ElMassimo/vite_ruby/tree/main/vite_rails

このviteを使用して、svelteを利用できるようにしていきます。

## ホットリロードできるようにする

本筋からは外れますが、せっかくviteを入れたのだからホットリロードしてほしいっすよね。

package.jsonに`vite-plugin-full-reload`を追加して、

```diff_json
{
  "devDependencies": {
    "vite": "^3.0.5",
-    "vite-plugin-ruby": "^3.1.2"
+    "vite-plugin-ruby": "^3.1.2",
+    "vite-plugin-full-reload": "^1.0.3"
  }
}
```

プラグインをインストールします。

```
yarn install
```

このままだとなぜかホットリロードしてくれないので、config/vite.jsonを以下のように修正します。

```diff_json
{
  "all": {
    "sourceCodeDir": "app/frontend",
    "watchAdditionalPaths": []
  },
  "development": {
    "autoBuild": true,
    "publicOutputDir": "vite-dev",
-    "port": 3036
+    "port": 3036,
+    "host": "127.0.0.1"
  },
  "test": {
    "autoBuild": true,
    "publicOutputDir": "vite-test",
    "port": 3037
  }
}
```

これでホットリロードがきくようになるので、起動中にerbファイルを直したら勝手にブラウザ上も表示が変更されるようになります。

ちなみにホットリロードさせるときは以下のコマンドでrailsを起動するようにします。

```shell
foreman start -f Procfile.dev 
```

`Procfile.dev`の中身を見ればわかりますが、`bin/vite dev`もrailsサーバーと同時に起動しないといけないためです。
なので、事前にforemanのインストールは必要ですね。

## svelteを追加する

package.jsonを以下のように修正します。

```diff_json
{
+  "type": "module",
  "devDependencies": {
    "vite": "^3.0.5",
    "vite-plugin-ruby": "^3.1.2",
-    "vite-plugin-full-reload": "^1.0.3"
+    "vite-plugin-full-reload": "^1.0.3",
+    "@sveltejs/vite-plugin-svelte": "^1.0.1",
+    "@tsconfig/svelte": "^3.0.0",
+    "svelte": "^3.49.0",
+    "svelte-check": "^2.8.0",
+    "svelte-preprocess": "^4.10.7",
+    "tslib": "^2.4.0",
+    "typescript": "^4.6.4"
  }
}
```

そしてインストール実行。

```shell
yarn install
```

さらに以下のファイルたちをプロジェクト直下に配置します。

```svelte.config.js
import sveltePreprocess from 'svelte-preprocess'

export default {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: sveltePreprocess()
}
```

```tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Node"
  },
  "include": ["vite.config.ts"]
}
```

```tsconfig.json
{
  "extends": "@tsconfig/svelte/tsconfig.json",
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "resolveJsonModule": true,
    "baseUrl": ".",
    /**
     * Typecheck JS in `.svelte` and `.js` files by default.
     * Disable checkJs if you'd like to use dynamic types in JS.
     * Note that setting allowJs false does not prevent the use
     * of JS in `.svelte` files.
     */
    "allowJs": true,
    "checkJs": true,
    "isolatedModules": true
  },
  "include": ["./**/*.d.ts", "./**/*.ts", "./**/*.svelte"],
  "exclude": ["./public/**/*", "svelte.config.js",],
  "references": [{ "path": "./tsconfig.node.json" }]
}
```

そして、vite.config.tsを以下のように修正。

```diff_typescript
import { defineConfig } from "vite";
import RubyPlugin from "vite-plugin-ruby";
import FullReload from "vite-plugin-full-reload";
+import { svelte } from "@sveltejs/vite-plugin-svelte";

export default defineConfig({
  plugins: [RubyPlugin(), FullReload(["config/routes.rb", "app/views/**/*"])],
  plugins: [
    RubyPlugin(),
    FullReload(["config/routes.rb", "app/views/**/*"]),
+    svelte(),
  ],
});
```

これでSvelteを使う準備ができました。

※ここらへんのファイルたちは、`yarn create vite`をしてsvelteプロジェクトを生成したときに作られたファイルたちをコピってきてるだけです。
常に最新で試したい場合には、自らsvelteプロジェクトを生成して、作られたファイルを自分でコピってきたほうがいいです。

# Svelteを使ってみる

## とりあえずscaffold

```shell
bin/rails g scaffold Blog title:string article:string
```

とりあえず、`/blogs/1`とかにアクセスが来たらsvelteコンポーネントを表示できるようにしていきます。

## tsファイルとsvelteファイルを追加

まず、`app/frontend/components/BlogsShow.svelte`に以下のようなファイルを作成。

```BlogsShow.svelte
<script lang="ts">
</script>

<p>Hello Svelte</p>
```

そして、`app/frontend/entrypoints/blogs-show.ts`に以下のようなファイルを作成。

```blogs-show.ts
import BlogsShow from "../components/BlogsShow.svelte";

new BlogsShow({
  target: document.getElementById("blogs-show"),
});
```

## erbからentrypointsのtsファイルを参照する

vite_railsでは、erbファイルに`vite_typescript_tag`を用いると、`app/frontend/entrypoints`配下にあるtsファイルを参照できるようになります。

例えば以下のように書けば、

```
<%= vite_typescript_tag 'blogs-show'  %>
```

app/frontend/entrypoints/blogs-show.ts`ファイルを参照することができるようになるということです。

ということで、実際にやってみます。

`app/views/blogs/show.html.erb`を以下のように修正します。

```diff_erb
<p style="color: green"><%= notice %></p>

+<%= content_tag :div, "", id: "blogs-show" %>
+<%= vite_typescript_tag 'blogs-show'  %>
<%= render @blog %>
...
```

以下のようにrailsを起動して、

```shell
foreman start -f Procfile.dev
```

http://127.0.0.1:5100/blogs にアクセス。

適当に記事を作って http://127.0.0.1:5100/blogs/1 とかにアクセスすると、`Hello Svelte`の文字が！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177591/f718a3a8-6090-c0ba-5fe8-2c2724fcee60.png)


## propsを渡す

svelteコンポーネントにpropsを渡したいときには、dataにわたすようにします。

```diff_erb
<%= content_tag :div, "", id: "blogs-show" %>
<%= content_tag :div, "", id: "blogs-show", data: {title: @blog.title, article: @blog.article}%>
```

で、entrypointsのtsファイル側で、svelteをnewするときにpropsを渡します。

```diff_typescript
+const target = document.getElementById("blogs-show");
+
new BlogsShow({
-  target: document.getElementById("blogs-show"),
+  target: target,
+  props: {
+    blog: {
+      title: target.dataset["title"],
+      article: target.dataset["article"],
+    },
+  },
...
```

svelte側では普通にpropsを受け取る形で書けばOKです。

```diff_typescript
<script lang="ts">
+  export let blog: {
+    title: string;
+    article: string;
+  };
</script>

-<p>Hello Svelte</p>
+<h1>{blog.title}</h1>
+<p>{blog.article}</p>
```

http://127.0.0.1:5100/blogs/1 とかを見ると、svelteとerbの表示によってタイトルと本文が二重に表示されている！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177591/515149e8-915b-680a-bab5-f7c550fe0cfb.png)

# 以上
以上でございます。
最初にも記載したPRの内容をなぞって書いているだけなので、もし動かなかったりしたら、動いた実績のあるPR見たほうが早いかもです。

https://github.com/yuki-noguchi/svelte-rails7/pull/1

### ちなみに

自分はいつもjavaとreactばっか使ってるんですが、railsでreactやsvelte使うときってどうやって使うもんなんですか？
・railsプロジェクトには入れるけど、reactやsvelteはSPAとして作って、rails側はAPIとして動かし、erbファイルは1つくらいしか作らない
なのか
・あくまでMVCの形は崩さず、個々のerbからreactやsvelteを参照させるようにして、erbでビューは開発、ちょっと動きつけたいところにreactやsvelteを使う
なのか
・MVCの形は崩さず、個々のerbからreactやsvelteを参照させるようにするが、erbはdiv一個だけとかにして、ビューの実装は主にreactやsvelteで行うようにする
なのか。

誰かおしえてください〜。

### ほか
久しぶりにrails触るモチベとして、型のある開発に興味があったのですが、2022/08/10時点でrails7でやろうとすると無理っぽいですよね？
VSCodeまっかっかになってわけわからなくなりました。
↓にrails関連の型定義がバージョン7で作られたらできるようになるのかな？
https://github.com/ruby/gem_rbs_collection/tree/main/gems

これも誰か教えて下さい〜。