https://github.com/takapi327/melt
https://github.com/takapi327/melt
Last synced: 2 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/takapi327/melt
- Owner: takapi327
- License: other
- Created: 2026-04-05T08:23:02.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-14T15:35:46.000Z (3 months ago)
- Last Synced: 2026-04-14T17:25:21.233Z (3 months ago)
- Language: Scala
- Size: 832 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# Melt
> Scala を溶かして JS にするコンパイラ
Melt は Scala.js 向けのシングルファイルコンポーネント(SFC)フレームワークです。Svelte にインスパイアされた `.melt` ファイルに Scala・HTML・CSS を1ファイルで記述し、コンパイラが素の DOM 操作コードへ変換します。
```html
val count = Var(0)
count += 1}>Count: {count}
button { font-size: 1.5rem; cursor: pointer; }
```
## コンセプト
- **コンパイラがフレームワーク** — Svelte と同様、ランタイムフレームワーク不要。コンパイラが DOM 操作コードを直接生成
- **Scala の型システムを完全保持** — テンプレート内の式も含め、型チェックはすべて scalac が行う
- **ランタイムは最小限** — `Var` / `Signal` / `Bind` を提供する小さなランタイムのみ
- **SSR 対応** — 同じ `.melt` ファイルを JVM 側で HTML 文字列として出力可能(`CompileMode.SSR`)
## Status
| フェーズ | 内容 | 状態 |
|---------|------|------|
| Phase 0 | モノレポスケルトン | ✅ 完了 |
| Phase 1 | `melt-runtime` コア(`Var` / `Signal`) | ✅ 完了 |
| Phase 2 | `meltc` パーサー | ✅ 完了 |
| Phase 3 | コード生成 + sbt プラグイン | ✅ 完了 |
| Phase 4 | リアクティブバインディング | ✅ 完了 |
| Phase 5 | コンポーネントシステム + CSS スコーピング | ✅ 完了 |
| Phase 6 | テンプレート完全対応 | ✅ 完了 |
| Phase 7 | ライフサイクル & 状態管理 | ✅ 完了 |
| Phase 8 | 高度な機能 | ✅ 完了 |
| Phase 9 | トランジション & アニメーション | ✅ 完了 |
| Phase 10 | テストキット(`melt-testkit`) | 🚧 開発中 |
| Phase 11 | IDE サポート / LSP | 🚧 開発中 |
| Phase 12+ | フォームライブラリ・ドキュメント・リリース | 📋 予定 |
---
## モジュール構成
| モジュール | 説明 | プラットフォーム |
|-----------|------|----------------|
| `meltc` | `.melt` → `.scala` コンパイラ | JVM / JS / Native |
| `sbt-meltc` | sbt プラグイン(`.melt` 自動コンパイル) | JVM (Scala 2.12) |
| `melt-runtime` | リアクティブランタイム(`Var` / `Signal` / `Bind`) | JVM / JS |
| `melt-testkit` | コンポーネントテストユーティリティ | JS |
| `melt-language-server` | LSP サーバー(IDE 統合) | JVM |
---
## `.melt` ファイル構文
`.melt` ファイルは 3 つのセクションで構成されます。
```html
case class Props(title: String = "Hello", count: Var[Int])
val doubled = count.map(_ * 2)
{title}
Count: {count} / Doubled: {doubled}
count += 1}>+1
h1 { color: #ff3e00; }
```
### テンプレート構文
#### 式展開
```html
{message}
{count * 2}
{if isActive then "active" else "inactive"}
```
#### 生 HTML の挿入(XSS 注意)
`TrustedHtml.unsafe` でマークした文字列のみ raw HTML として挿入できます。コメントアンカー方式で挿入されるため、ラッパー要素は生成されません。
```html
{TrustedHtml.unsafe("Bold")}
{if show then TrustedHtml.unsafe("yes") else TrustedHtml.unsafe("no")}
```
#### イベントハンドラー
```html
count += 1}>+1
name.set(e.target.value)} />
handleSubmit()}>...
```
#### データバインディング
```html
A
B
```
#### クラス・スタイルディレクティブ
```html
...
...
```
#### スニペット(コンテンツ投影)
親コンポーネントでスニペットを定義し、子コンポーネントへ渡せます。
```html
{#snippet renderItem(item: Todo)}
{item.text}
{/snippet}
case class Props(items: Var[List[Todo]], renderItem: Snippet[Todo])
- {@render props.renderItem(item)}
{props.items.map((item: Todo) =>
)}
```
#### トランジション・アニメーション
```html
```
利用可能なトランジション: `fade`, `slide`, `fly`, `scale`, `blur`, `draw`, `crossfade`
イージング関数(31種): `linear`, `cubicIn/Out/InOut`, `quadIn/Out/InOut`, `quartIn/Out/InOut`, `quintIn/Out/InOut`, `sineIn/Out/InOut`, `backIn/Out/InOut`, `elasticIn/Out/InOut`, `bounceIn/Out/InOut`, `circIn/Out/InOut`, `expoIn/Out/InOut`
#### アクション
```html
...
```
#### 特殊要素
```html
{pageTitle}
...
{#pending}
Loading…
{/pending}
{#failed error}
Error: {error.getMessage()}
{/failed}
```
#### ジェネリックコンポーネント
```html
case class Props[T](items: Var[List[T]], render: Snippet[T])
```
---
## Runtime API
### リアクティブプリミティブ
#### `Var[A]` — ミュータブルなリアクティブ変数
```scala
val count = Var(0)
// 読み取り
val current: Int = count.value // または count.now()
// 更新
count.set(5)
count.update(_ + 1)
// 算術拡張メソッド
count += 1 // Int / Long / Double / String
count -= 1
count *= 2
// コレクション拡張(Var[List[A]])
items.append(newItem)
items.prepend(first)
items.removeWhere(_.id == id)
items.removeAt(0)
items.updateWhere(_.id == id)(_.copy(done = true))
items.clear()
items.sortBy(_.name)
// 派生
val doubled: Signal[Int] = count.map(_ * 2)
// 購読
val unsubscribe = count.subscribe(n => println(s"Changed to: $n"))
unsubscribe()
```
#### `Signal[A]` — 読み取り専用のリアクティブ値
```scala
val doubled: Signal[Int] = count.map(_ * 2)
val frozen: Signal[Int] = Signal.pure(42)
doubled.value
doubled.subscribe(n => println(n))
```
### エフェクト・メモ化
```scala
// エフェクト(依存値変化時に実行)
effect(count) { n =>
dom.document.title = s"Count: $n"
}
// 2変数
effect(a, b) { (va, vb) => ... }
// レイアウトエフェクト(DOM 更新前に実行)
layoutEffect(count) { n =>
el.getBoundingClientRect().height
}
// メモ化(依存値が変化したときのみ再計算)
val isEven = memo(count)(_ % 2 == 0)
```
### ライフサイクル
```scala
onMount { () =>
fetchData()
() => cleanup() // アンマウント時のクリーンアップを返せる
}
onCleanup { () =>
subscription.cancel()
}
// 次の DOM 更新を待つ
tick().foreach { _ => ... }
```
### コンテキスト API
```scala
val ThemeCtx = Context.create("light")
// 提供側
ThemeCtx.provide("dark")
// 消費側
val theme = ThemeCtx.inject() // "dark"
```
### バッチ更新
```scala
batch {
firstName.set("John")
lastName.set("Doe")
// DOM 更新は1回にまとめられる
}
```
### Tween / Spring
```scala
val pos = Tween(0.0, TweenOptions(duration = 400, easing = Easing.cubicOut))
val scale = Spring(1.0, SpringOptions(stiffness = 0.1, damping = 0.25))
pos.set(100.0) // アニメーション付きで更新
scale.set(1.5)
```
### セキュリティユーティリティ
```scala
// HTML エスケープ
Escape.html("alert(1)") // "<script>..."
// 属性エスケープ
Escape.attr(userInput)
// URL 検証(javascript:, vbscript:, file: をブロック)
Escape.url(hrefValue)
// CSS 値エスケープ
Escape.cssValue(styleValue)
// 信頼済み HTML(エスケープをバイパス)
val safe = TrustedHtml.unsafe("validated")
```
---
## セキュリティ機能
コンパイル時・ランタイムの2段階でセキュリティを担保します。
### コンパイル時チェック(`SecurityChecker`)
| パターン | 種別 | 説明 |
|---------|------|------|
| `` | **エラー**(コンパイル失敗) | 任意 HTML の XSS 防止 |
| `` | 警告 | URL バリデーション推奨 |
| `` / `` | 警告 | プラグイン実行リスク |
| `` | 警告 | 動的フォームターゲット |
| `` | 警告 | 動的フォームターゲット |
| `` | 警告 | オープンリダイレクト |
| `` without `rel="noopener"` | 警告 | タブナッビング |
### ランタイムエスケープ
- `Escape.html` — `&`, `<`, `>` をエスケープ
- `Escape.attr` — `&`, `<`, `>`, `"`, `\n`, `\r`, `\t` をエスケープ
- `Escape.url` — `javascript:`, `vbscript:`, `file:` をブロック。`data:` URL は `data:image/*`(SVG 除く)のみ許可
- `Escape.cssValue` — `expression(`, `@import`, `javascript:` 等をブロック
---
## 使い方
### 1. sbt セットアップ
```scala
// project/plugins.sbt
addSbtPlugin("io.github.takapi327" % "sbt-meltc" % "0.1.0-SNAPSHOT")
// build.sbt
enablePlugins(ScalaJSPlugin, MeltcPlugin)
scalaVersion := "3.3.7"
libraryDependencies += "io.github.takapi327" %%% "melt-runtime" % "0.1.0-SNAPSHOT"
meltcPackage := "components" // 生成コードのパッケージ
meltcHydration := false // SSR+Hydration を使う場合は true
```
### 2. コンポーネント作成
```
src/main/scala/components/App.melt
src/main/scala/components/Counter.melt
src/main/scala/components/TodoList.melt
```
### 3. エントリーポイント
```scala
// Main.scala
import org.scalajs.dom
object Main:
def main(args: Array[String]): Unit =
val root = dom.document.getElementById("app")
components.App.mount(root)
```
### 4. ビルド
```bash
sbt fastLinkJS # 開発用(ウォッチモード: sbt ~fastLinkJS)
sbt fullLinkJS # 本番用
```
### SSR(サーバーサイドレンダリング)
```scala
// build.sbt — JVM ターゲット側で
libraryDependencies += "io.github.takapi327" %% "melt-runtime" % "0.1.0-SNAPSHOT"
meltcMode := "ssr"
```
```scala
// サーバー側(http4s など)
import components.Home
val html = Home(Home.Props(userName = "Alice", count = 42))
Ok(html.body)
```
---
## サンプル
| サンプル | 説明 |
|---------|------|
| [hello-world](examples/hello-world) | 最小構成 |
| [counter](examples/counter) | リアクティブ状態・双方向バインディング・コンテキスト |
| [todo-app](examples/todo-app) | コンポーネント合成・スニペット・リスト操作 |
| [transitions](examples/transitions) | トランジション・アニメーション・FLIP |
| [special-elements](examples/special-elements) | `melt:head` / `melt:window` / `melt:body` / `melt:element` / `melt:key` |
| [boundary](examples/boundary) | Error Boundary・非同期レンダリング |
| [layout-effect](examples/layout-effect) | DOM 計測・レイアウトエフェクト |
| [reactive-scope](examples/reactive-scope) | リソース管理パターン |
| [dynamic-element](examples/dynamic-element) | 動的タグ名 |
| [media-binding](examples/media-binding) | メディア要素バインディング |
| [dimension-binding](examples/dimension-binding) | 要素寸法バインディング |
| [select-textarea-bind](examples/select-textarea-bind) | セレクト・テキストエリアバインディング |
| [trusted-html](examples/trusted-html) | 生 HTML 挿入(`TrustedHtml`) |
| [http4s-spa](examples/http4s-spa) | SPA + API(http4s) |
| [http4s-ssr](examples/http4s-ssr) | SSR + Hydration + API(http4s) |
---
## 開発
```bash
# コンパイル
sbt compile
# テスト(全プラットフォーム)
sbt test
# JVM のみ
sbt meltcJVM/test runtimeJVM/test
# コードフォーマット
sbt scalafmtAll
# ヘッダーチェック
sbt headerCheckAll
```
### Scala バージョン
| Scala | 用途 |
|-------|------|
| 3.3.7 | メイン(LTS) |
| 3.8.3 | 追加テスト |
| 2.12.21 | `sbt-meltc` プラグインのみ |
### Java バージョン
Java 17 / 21 / 25(Corretto)で CI テストを実施しています。
---
## ライセンス
Apache 2.0 — 詳細は [LICENSE](LICENSE) を参照してください。