salvia_rb 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 111770f5d7b2a6e7d086e0a12d568f24eef9011aed420f4257498f3ad442838a
4
+ data.tar.gz: 641a090af7f584e5d8d35d0c6dcf93c3529860869c3130049a3acd2fc12c99ee
5
+ SHA512:
6
+ metadata.gz: acb426d04b64edbeda0788b13b462f0d5d12144f4717a8e5375ae9ada8deebefdc7725b8472a724900d45b521d04ba83111446d718286f580de86ab3819d5d40
7
+ data.tar.gz: dad6635b64693e7bdcd86dc4f7fbea89438ec6a6267e8a907b99b16602d6b1108060c18173ef454d85809f67618528ca6e8d8ca83ecd02b05c9af617ab936394
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hiroto Furugen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,235 @@
1
+ <p align="center">
2
+ <img src="https://img.shields.io/badge/Ruby-3.1+-CC342D?style=flat-square&logo=ruby" alt="Ruby">
3
+ <img src="https://img.shields.io/badge/License-MIT-blue?style=flat-square" alt="License">
4
+ <img src="https://img.shields.io/badge/Version-0.1.0-6A5ACD?style=flat-square" alt="Version">
5
+ <img src="https://img.shields.io/gem/v/salvia_rb?style=flat-square&color=ff6347" alt="Gem">
6
+ </p>
7
+
8
+ # 🌿 Salvia.rb
9
+
10
+ > **"Wisdom for Rubyists."**
11
+ >
12
+ > A small, understandable Ruby MVC framework
13
+
14
+ **SSR Islands Architecture** × **Tailwind** × **ActiveRecord** combined into a simple and clear Ruby Web framework.
15
+
16
+ ## Features
17
+
18
+ - **🚀 Zero Configuration** - Works out of the box, customizable when needed
19
+ - **Server-Rendered (HTML) First** - Return HTML, not JSON APIs
20
+ - **🏝️ SSR Islands Architecture** - Server-side render Preact components with QuickJS
21
+ - **Rails-like DSL** - Familiar `resources`, `root to:` routing
22
+ - **ActiveRecord Integration** - Use models like Rails
23
+ - **🐳 Docker Ready** - Auto-generated Dockerfile and docker-compose.yml
24
+ - **No Node.js Required** - QuickJS for SSR, Deno for build
25
+
26
+ ## Installation
27
+
28
+ ```ruby
29
+ gem "salvia_rb"
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # Install the gem
36
+ gem install salvia_rb
37
+
38
+ # Create a new app
39
+ salvia new myapp
40
+ cd myapp
41
+
42
+ # Setup and start
43
+ bundle install
44
+ salvia ssr:build # Build SSR bundle (requires Deno)
45
+ salvia server # Start server (Puma in dev, Falcon in prod)
46
+ ```
47
+
48
+ Open http://localhost:9292 in your browser.
49
+
50
+ ### Zero Configuration
51
+
52
+ Salvia works with minimal setup:
53
+
54
+ ```ruby
55
+ # config.ru (3 lines!)
56
+ require "salvia_rb"
57
+ Salvia.configure { |c| c.root = __dir__ }
58
+ run Salvia::Application.new
59
+ ```
60
+
61
+ ### One-liner Mode
62
+
63
+ ```ruby
64
+ # app.rb
65
+ require "salvia_rb"
66
+ Salvia.run! # Auto-selects server: Puma (dev) or Falcon (prod)
67
+ ```
68
+
69
+ ## Directory Structure
70
+
71
+ ```
72
+ myapp/
73
+ ├── app/
74
+ │ ├── controllers/
75
+ │ │ ├── application_controller.rb
76
+ │ │ └── home_controller.rb
77
+ │ ├── models/
78
+ │ │ └── application_record.rb
79
+ │ ├── views/
80
+ │ │ ├── layouts/
81
+ │ │ │ └── application.html.erb
82
+ │ │ └── home/
83
+ │ │ └── index.html.erb
84
+ │ └── islands/ # 🏝️ Island components
85
+ │ └── Counter.js
86
+ ├── vendor/server/
87
+ │ └── ssr_bundle.js # SSR bundle (generated)
88
+ ├── config/
89
+ │ ├── database.yml
90
+ │ ├── environment.rb
91
+ │ └── routes.rb
92
+ ├── db/
93
+ │ └── migrate/
94
+ ├── public/
95
+ │ └── assets/
96
+ ├── config.ru # 3 lines!
97
+ ├── Gemfile
98
+ ├── Dockerfile # Auto-generated
99
+ └── docker-compose.yml # Auto-generated
100
+ ```
101
+
102
+ ## Routing
103
+
104
+ ```ruby
105
+ # config/routes.rb
106
+ Salvia::Router.draw do
107
+ root to: "home#index"
108
+
109
+ get "/about", to: "pages#about"
110
+
111
+ resources :posts, only: [:index, :show, :create]
112
+ end
113
+ ```
114
+
115
+ ## Controller
116
+
117
+ ```ruby
118
+ class PostsController < ApplicationController
119
+ def index
120
+ @posts = Post.order(created_at: :desc)
121
+ end
122
+
123
+ def create
124
+ @post = Post.create!(title: params["title"])
125
+ render "posts/_post", locals: { post: @post }
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## 🏝️ SSR Islands
131
+
132
+ Salvia's Islands Architecture supports server-side rendering (SSR).
133
+
134
+ ### Create an Island Component
135
+
136
+ ```jsx
137
+ // app/islands/Counter.jsx
138
+ import { h } from "preact";
139
+ import { useState } from "preact/hooks";
140
+
141
+ export function Counter({ initialCount = 0 }) {
142
+ const [count, setCount] = useState(initialCount);
143
+
144
+ return (
145
+ <div className="p-4 border rounded">
146
+ <p className="text-2xl font-bold">{count}</p>
147
+ <button
148
+ onClick={() => setCount(count + 1)}
149
+ className="px-4 py-2 bg-blue-500 text-white rounded"
150
+ >
151
+ +1
152
+ </button>
153
+ </div>
154
+ );
155
+ }
156
+ ```
157
+
158
+ ### Use in ERB
159
+
160
+ ```erb
161
+ <!-- app/views/home/index.html.erb -->
162
+ <h1>Counter Demo</h1>
163
+
164
+ <%# SSR + Client Hydration %>
165
+ <%= island "Counter", { initialCount: 10 } %>
166
+ ```
167
+
168
+ ### Build SSR Bundle
169
+
170
+ ```bash
171
+ salvia ssr:build
172
+ ```
173
+
174
+ ### How It Works
175
+
176
+ ```
177
+ 1. SSR: Render Preact components with QuickJS (0.3ms/render)
178
+ 2. HTML: Embed rendered result in ERB
179
+ 3. Hydrate: Client-side Preact hydrate()
180
+ 4. Interactive: Clicks and inputs work
181
+ ```
182
+
183
+ ## CLI Commands
184
+
185
+ | Command | Description |
186
+ |---------|-------------|
187
+ | `salvia new APP_NAME` | Create a new application |
188
+ | `salvia server` / `salvia s` | Start server (Puma dev / Falcon prod) |
189
+ | `salvia dev` | Start server + CSS watch + SSR watch |
190
+ | `salvia console` / `salvia c` | Start IRB console |
191
+ | `salvia db:create` | Create database |
192
+ | `salvia db:migrate` | Run migrations |
193
+ | `salvia db:rollback` | Rollback last migration |
194
+ | `salvia db:setup` | Create database and run migrations |
195
+ | `salvia css:build` | Build Tailwind CSS |
196
+ | `salvia css:watch` | Watch and rebuild CSS |
197
+ | `salvia ssr:build` | Build SSR bundle |
198
+ | `salvia ssr:watch` | Watch and rebuild SSR |
199
+ | `salvia routes` | Display routes |
200
+ | `salvia g controller NAME` | Generate controller |
201
+ | `salvia g model NAME` | Generate model |
202
+
203
+ ## Docker
204
+
205
+ Generated apps include Docker support:
206
+
207
+ ```bash
208
+ # Development
209
+ docker compose up
210
+
211
+ # Production
212
+ docker build -t myapp .
213
+ docker run -p 9292:9292 -e RACK_ENV=production myapp
214
+ ```
215
+
216
+ ## Requirements
217
+
218
+ - Ruby 3.1+
219
+ - Deno (for SSR build)
220
+ - SQLite3 (default) or PostgreSQL/MySQL
221
+
222
+ ## License
223
+
224
+ MIT License - See [LICENSE](LICENSE.txt) for details.
225
+
226
+ ## Contributing
227
+
228
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/genfuru011/Salvia).
229
+
230
+ ---
231
+
232
+ <p align="center">
233
+ <strong>🌿 Salvia.rb</strong><br>
234
+ <em>"Simple, like a flower. Solid, like a gem."</em>
235
+ </p>
@@ -0,0 +1,26 @@
1
+ // Salvia Islands - Client-side Hydration
2
+ // Framework-agnostic island loader
3
+ // Each island component must export a mount(element, props) function
4
+
5
+ document.addEventListener('DOMContentLoaded', async () => {
6
+ const islands = document.querySelectorAll('[data-island]');
7
+
8
+ for (const island of islands) {
9
+ const name = island.dataset.island;
10
+ const props = JSON.parse(island.dataset.props || '{}');
11
+ const hasSSR = island.innerHTML.trim().length > 0;
12
+
13
+ try {
14
+ const module = await import(`/client/${name}.js`);
15
+
16
+ if (typeof module.mount === 'function') {
17
+ module.mount(island, props, { hydrate: hasSSR });
18
+ console.log(`🏝️ Island ${hasSSR ? 'hydrated' : 'mounted'}: ${name}`);
19
+ } else {
20
+ console.error(`Island ${name} must export a mount() function`);
21
+ }
22
+ } catch (error) {
23
+ console.error(`Failed to load island: ${name}`, error);
24
+ }
25
+ }
26
+ });
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env -S deno run --allow-all
2
+ /**
3
+ * Salvia Build Script
4
+ *
5
+ * Build SSR Islands + Tailwind CSS
6
+ *
7
+ * Usage:
8
+ * deno run --allow-all build_ssr.ts
9
+ * deno run --allow-all build_ssr.ts --watch
10
+ */
11
+
12
+ import * as esbuild from "https://deno.land/x/esbuild@v0.24.2/mod.js";
13
+ import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11";
14
+
15
+ // Tailwind CSS (Deno)
16
+ import postcss from "npm:postcss@8";
17
+ import tailwindcss from "npm:tailwindcss@3";
18
+ import autoprefixer from "npm:autoprefixer@10";
19
+
20
+ const ISLANDS_DIR = "./app/islands";
21
+ const SSR_OUTPUT_DIR = "./vendor/server";
22
+ const CLIENT_OUTPUT_DIR = "./vendor/client";
23
+ const CSS_INPUT = "./app/assets/stylesheets/application.tailwind.css";
24
+ const CSS_OUTPUT = "./public/assets/stylesheets/tailwind.css";
25
+ const WATCH_MODE = Deno.args.includes("--watch");
26
+ const VERBOSE = Deno.args.includes("--verbose");
27
+
28
+ // ============================================
29
+ // Tailwind CSS Build
30
+ // ============================================
31
+
32
+ async function buildCSS() {
33
+ try {
34
+ const css = await Deno.readTextFile(CSS_INPUT);
35
+
36
+ // Load Tailwind config
37
+ const config = {
38
+ content: [
39
+ "./app/views/**/*.erb",
40
+ "./app/islands/**/*.{js,jsx,tsx}",
41
+ "./public/assets/javascripts/**/*.js"
42
+ ],
43
+ theme: {
44
+ extend: {
45
+ colors: {
46
+ 'salvia': {
47
+ 50: '#f0f0ff',
48
+ 100: '#e4e4ff',
49
+ 200: '#cdcdff',
50
+ 300: '#a8a8ff',
51
+ 400: '#7c7cff',
52
+ 500: '#6A5ACD',
53
+ 600: '#5a4ab8',
54
+ 700: '#4B0082',
55
+ 800: '#3d006b',
56
+ 900: '#2d0050',
57
+ }
58
+ }
59
+ },
60
+ },
61
+ plugins: [],
62
+ };
63
+
64
+ const result = await postcss([
65
+ tailwindcss(config),
66
+ autoprefixer(),
67
+ ]).process(css, { from: CSS_INPUT, to: CSS_OUTPUT });
68
+
69
+ await Deno.writeTextFile(CSS_OUTPUT, result.css);
70
+ console.log(`✅ Tailwind CSS built: ${CSS_OUTPUT}`);
71
+ } catch (error) {
72
+ console.error("❌ CSS build error:", error);
73
+ }
74
+ }
75
+
76
+ // ============================================
77
+ // SSR Islands Build
78
+ // ============================================
79
+
80
+ interface IslandFile {
81
+ path: string;
82
+ name: string;
83
+ clientOnly: boolean;
84
+ }
85
+
86
+ async function findIslandFiles(): Promise<IslandFile[]> {
87
+ const files: IslandFile[] = [];
88
+ try {
89
+ for await (const entry of Deno.readDir(ISLANDS_DIR)) {
90
+ if (entry.isFile && (entry.name.endsWith(".tsx") || entry.name.endsWith(".jsx") || entry.name.endsWith(".js"))) {
91
+ const path = `${ISLANDS_DIR}/${entry.name}`;
92
+ const content = await Deno.readTextFile(path);
93
+ const clientOnly = content.trimStart().startsWith('"client only"') ||
94
+ content.trimStart().startsWith("'client only'");
95
+ const name = entry.name.replace(/\.(tsx|jsx|js)$/, "");
96
+ files.push({ path, name, clientOnly });
97
+ }
98
+ }
99
+ } catch {
100
+ console.log("📁 app/islands directory not found. Skipping.");
101
+ }
102
+ return files;
103
+ }
104
+
105
+ async function buildSSR() {
106
+ const islandFiles = await findIslandFiles();
107
+
108
+ if (islandFiles.length === 0) {
109
+ console.log("⚠️ No Island components found. Skipping SSR build.");
110
+ return;
111
+ }
112
+
113
+ const ssrFiles = islandFiles.filter(f => !f.clientOnly);
114
+ const clientFiles = islandFiles; // All files go to client
115
+
116
+ if (VERBOSE) {
117
+ console.log("🔍 SSR targets:", ssrFiles.map(f => f.name));
118
+ console.log("🔍 Client targets:", clientFiles.map(f => f.name));
119
+ }
120
+
121
+ // Create output directories
122
+ await Deno.mkdir(SSR_OUTPUT_DIR, { recursive: true });
123
+ await Deno.mkdir(CLIENT_OUTPUT_DIR, { recursive: true });
124
+
125
+ try {
126
+ // SSR bundle (for QuickJS) - single bundle with all components
127
+ if (ssrFiles.length > 0) {
128
+ // Create a virtual entry point that exports all components
129
+ // Get just the filename from the path
130
+ const entryCode = ssrFiles.map(f => {
131
+ const filename = f.path.split("/").pop();
132
+ return `import ${f.name} from "./${filename}";`;
133
+ }).join("\n") + `
134
+ import { h } from "https://esm.sh/preact@10.19.3";
135
+ import renderToString from "https://esm.sh/preact-render-to-string@6.3.1?deps=preact@10.19.3";
136
+
137
+ // Salvia SSR Runtime
138
+ const components = {
139
+ ${ssrFiles.map(f => ` "${f.name}": ${f.name}`).join(",\n")}
140
+ };
141
+
142
+ globalThis.SalviaSSR = {
143
+ render: function(name, props) {
144
+ const Component = components[name];
145
+ if (!Component) {
146
+ throw new Error("Component not found: " + name);
147
+ }
148
+ const vnode = h(Component, props);
149
+ return renderToString(vnode);
150
+ }
151
+ };
152
+ `;
153
+ const entryPath = `${ISLANDS_DIR}/_ssr_entry.js`;
154
+ await Deno.writeTextFile(entryPath, entryCode);
155
+
156
+ await esbuild.build({
157
+ entryPoints: [entryPath],
158
+ bundle: true,
159
+ format: "iife",
160
+ outfile: `${SSR_OUTPUT_DIR}/ssr_bundle.js`,
161
+ platform: "neutral",
162
+ plugins: [...denoPlugins()],
163
+ external: [],
164
+ jsx: "automatic",
165
+ jsxImportSource: "preact",
166
+ banner: {
167
+ js: `// Salvia SSR Bundle - Generated at ${new Date().toISOString()}`,
168
+ },
169
+ });
170
+
171
+ // Clean up temp file
172
+ await Deno.remove(entryPath);
173
+
174
+ console.log(`✅ SSR bundle built: ${SSR_OUTPUT_DIR}/ssr_bundle.js (${ssrFiles.map(f => f.name).join(", ")})`);
175
+ }
176
+
177
+ // Client bundle (for hydration) - all files
178
+ await esbuild.build({
179
+ entryPoints: clientFiles.map(f => f.path),
180
+ bundle: true,
181
+ format: "esm",
182
+ outdir: CLIENT_OUTPUT_DIR,
183
+ platform: "browser",
184
+ plugins: [...denoPlugins()],
185
+ external: [],
186
+ jsx: "automatic",
187
+ jsxImportSource: "preact",
188
+ minify: true,
189
+ banner: {
190
+ js: `// Salvia Client Islands - Generated at ${new Date().toISOString()}`,
191
+ },
192
+ });
193
+ console.log(`✅ Client Islands built: ${CLIENT_OUTPUT_DIR}/ (${clientFiles.map(f => f.name).join(", ")})`);
194
+
195
+ // Generate manifest (which Islands are client only)
196
+ const manifest = Object.fromEntries(
197
+ islandFiles.map(f => [f.name, { clientOnly: f.clientOnly }])
198
+ );
199
+ await Deno.writeTextFile(
200
+ `${SSR_OUTPUT_DIR}/manifest.json`,
201
+ JSON.stringify(manifest, null, 2)
202
+ );
203
+ console.log(`✅ Manifest generated: ${SSR_OUTPUT_DIR}/manifest.json`);
204
+
205
+ } catch (error) {
206
+ console.error("❌ SSR build error:", error.message);
207
+ }
208
+ }
209
+
210
+ // ============================================
211
+ // Main Build
212
+ // ============================================
213
+
214
+ async function build() {
215
+ await Promise.all([
216
+ buildCSS(),
217
+ buildSSR(),
218
+ ]);
219
+ }
220
+
221
+ async function watch() {
222
+ console.log("👀 Watching for file changes...");
223
+
224
+ // Watch Islands and CSS source
225
+ const watchDirs = [ISLANDS_DIR, "./app/views", "./app/assets/stylesheets"];
226
+
227
+ for (const dir of watchDirs) {
228
+ (async () => {
229
+ try {
230
+ const watcher = Deno.watchFs(dir);
231
+ let debounceTimer: number | undefined;
232
+
233
+ for await (const event of watcher) {
234
+ if (event.kind === "modify" || event.kind === "create") {
235
+ clearTimeout(debounceTimer);
236
+ debounceTimer = setTimeout(async () => {
237
+ console.log(`🔄 Changes detected in ${dir}, rebuilding...`);
238
+ await build();
239
+ }, 100);
240
+ }
241
+ }
242
+ } catch {
243
+ // Skip if directory doesn't exist
244
+ }
245
+ })();
246
+ }
247
+
248
+ // Wait indefinitely
249
+ await new Promise(() => {});
250
+ }
251
+
252
+ // Main execution
253
+ console.log("🌿 Salvia Build (SSR + Tailwind)");
254
+ console.log("================================");
255
+ await build();
256
+
257
+ if (WATCH_MODE) {
258
+ await watch();
259
+ } else {
260
+ await esbuild.stop();
261
+ }
data/exe/salvia ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "salvia_rb/cli"
5
+
6
+ Salvia::CLI.start(ARGV)
7
+