salvia 0.2.0

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: d47620b2511642dc1d75545d11cfda1f52071059a320bf1b181735af2bca2f41
4
+ data.tar.gz: 0a7322cbf4477fa2bff9ad6664e658a056a24a9cec83b6943e17f1ffbaf00bac
5
+ SHA512:
6
+ metadata.gz: 8b677e392d348bb21b8e6bcdb02e90ece13021e3c710fdfa394395174097ff81ee19fb3513ec09e7f752a392a79c1b63593747ec57d697cc4675c9730d755072
7
+ data.tar.gz: f55abe0fd41e6a76c5d39a728e56193ade7d986e32df1c49b05f9bfc0ba88b495f1b0483b686333dfbfca33c31b473f0953780472a81a09971f36fd3c7990dfd
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,152 @@
1
+ # Salvia 🌿
2
+
3
+ > **The Future of Rails View Layer**
4
+
5
+ Salvia is a next-generation **Server-Side Rendering (SSR) engine** designed to replace ERB with **JSX/TSX** in Ruby on Rails. It brings the **Islands Architecture** and **True HTML First** philosophy to the Rails ecosystem.
6
+
7
+ <img src="https://img.shields.io/gem/v/salvia?style=flat-square&color=ff6347" alt="Gem">
8
+
9
+ ## Vision: The Road to Sage
10
+
11
+ Salvia is the core engine for a future MVC framework called **Sage**.
12
+ While Sage will be a complete standalone framework, Salvia is available *today* as a drop-in replacement for the View layer in **Ruby on Rails**.
13
+
14
+ ## Features
15
+
16
+ * 🏝️ **Islands Architecture**: Render interactive components (Preact/React) only where needed. Zero JS for static content.
17
+ * 🚀 **True HTML First**: Replace `app/views/**/*.erb` with `app/pages/**/*.tsx`.
18
+ * ⚡ **JIT Compilation**: No build steps during development. Just run `rails s`.
19
+ * 💎 **Rails Native**: Seamless integration with Controllers, Routes, and Models.
20
+ * 🦕 **Deno Powered**: Uses Deno for lightning-fast TypeScript compilation and formatting.
21
+
22
+ ## Installation
23
+
24
+ Add this line to your Rails application's Gemfile:
25
+
26
+ ```ruby
27
+ gem 'salvia'
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ ```bash
33
+ $ bundle install
34
+ ```
35
+
36
+ ## Getting Started
37
+
38
+ ### 1. Install Salvia
39
+
40
+ Run the interactive installer to set up Salvia for your Rails project:
41
+
42
+ ```bash
43
+ $ bundle exec salvia install
44
+ ```
45
+
46
+ This creates the `salvia/` directory structure and configures your app with a **Zero Config** setup (Preact + Signals).
47
+
48
+ ### 2. Create a Page (Server Component)
49
+
50
+ Delete `app/views/home/index.html.erb` and create `salvia/app/pages/home/Index.tsx`:
51
+
52
+ ```tsx
53
+ import { h } from 'preact';
54
+
55
+ export default function Home({ title }) {
56
+ return (
57
+ <div class="p-10">
58
+ <h1 class="text-3xl font-bold">{title}</h1>
59
+ <p>This is rendered on the server with 0kb JavaScript sent to the client.</p>
60
+ </div>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ### 3. Render in Controller
66
+
67
+ In your Rails controller:
68
+
69
+ ```ruby
70
+ class HomeController < ApplicationController
71
+ def index
72
+ # Renders salvia/app/pages/home/Index.tsx
73
+ render html: ssr("home/Index", title: "Hello Salvia")
74
+ end
75
+ end
76
+ ```
77
+
78
+ ### 4. Add Interactivity (Islands)
79
+
80
+ Create an interactive component in `salvia/app/islands/Counter.tsx`:
81
+
82
+ ```tsx
83
+ import { h } from 'preact';
84
+ import { useState } from 'preact/hooks';
85
+
86
+ export default function Counter() {
87
+ const [count, setCount] = useState(0);
88
+ return (
89
+ <button onClick={() => setCount(count + 1)} class="btn">
90
+ Count: {count}
91
+ </button>
92
+ );
93
+ }
94
+ ```
95
+
96
+ Use it in your Page:
97
+
98
+ ```tsx
99
+ import Counter from '../../islands/Counter.tsx';
100
+
101
+ export default function Home() {
102
+ return (
103
+ <div>
104
+ <h1>Interactive Island</h1>
105
+ <Counter />
106
+ </div>
107
+ );
108
+ }
109
+ ```
110
+
111
+ ### 4. Turbo Drive (Optional)
112
+
113
+ Salvia works seamlessly with Turbo Drive for SPA-like navigation.
114
+
115
+ Add Turbo to your layout file (e.g., `app/pages/layouts/Main.tsx`):
116
+
117
+ ```tsx
118
+ <head>
119
+ {/* ... */}
120
+ <script type="module">
121
+ import * as Turbo from "https://esm.sh/@hotwired/turbo@8.0.0";
122
+ Turbo.start();
123
+ </script>
124
+ </head>
125
+ ```
126
+
127
+ This approach leverages Import Maps and browser-native modules, keeping your bundle size small and your architecture transparent.
128
+
129
+ ## Documentation
130
+
131
+ * **English**:
132
+ * [**Wisdom for Salvia**](docs/en/DESIGN.md): Deep dive into the architecture, directory structure, and "True HTML First" philosophy.
133
+ * [**Architecture**](docs/en/ARCHITECTURE.md): Internal design of the gem.
134
+ * **Japanese (日本語)**:
135
+ * [**Salviaの知恵**](docs/ja/DESIGN.md): アーキテクチャ、ディレクトリ構造、「真のHTMLファースト」哲学についての詳細。
136
+ * [**アーキテクチャ**](docs/ja/ARCHITECTURE.md): Gemの内部設計。
137
+
138
+ ## Framework Support
139
+
140
+ Salvia is primarily designed for **Ruby on Rails** to pave the way for the **Sage** framework.
141
+
142
+ * **Ruby on Rails**: First-class support.
143
+
144
+ ## Requirements
145
+
146
+ * Ruby 3.1+
147
+ * Rails 7.0+ (Recommended)
148
+ * Deno (for JIT compilation and tooling)
149
+
150
+ ## License
151
+
152
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,15 @@
1
+ import { h, ComponentType } from "preact";
2
+
3
+ interface IslandProps {
4
+ name: string;
5
+ component: ComponentType<any>;
6
+ [key: string]: any;
7
+ }
8
+
9
+ export default function Island({ name, component: Component, ...props }: IslandProps) {
10
+ return (
11
+ <div data-island={name} data-props={JSON.stringify(props)}>
12
+ <Component {...props} />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,17 @@
1
+ import { h } from "preact";
2
+ import { useState } from "preact/hooks";
3
+
4
+ export default function Counter() {
5
+ const [count, setCount] = useState(0);
6
+ return (
7
+ <div class="p-4 border rounded-lg">
8
+ <p class="text-lg mb-2">Count: {count}</p>
9
+ <button
10
+ onClick={() => setCount(count + 1)}
11
+ class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
12
+ >
13
+ Increment
14
+ </button>
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,55 @@
1
+ // Salvia Islands - Client-side Hydration
2
+ // Framework-agnostic island loader
3
+ // Each island component must export a mount(element, props) function
4
+
5
+ import { h, hydrate, render } from "preact";
6
+
7
+ async function hydrateIsland(island) {
8
+ if (island.dataset.hydrated) return;
9
+ island.dataset.hydrated = "true";
10
+
11
+ const name = island.dataset.island;
12
+ const props = JSON.parse(island.dataset.props || '{}');
13
+ const hasSSR = island.innerHTML.trim().length > 0;
14
+
15
+ try {
16
+ // Use the import map alias which handles dev/prod paths
17
+ const module = await import(`@/islands/${name}.js`);
18
+
19
+ if (typeof module.mount === 'function') {
20
+ module.mount(island, props, { hydrate: hasSSR });
21
+ console.log(`🏝️ Island ${hasSSR ? 'hydrated' : 'mounted'}: ${name}`);
22
+ } else if (module.default) {
23
+ // Auto-hydration for Preact components
24
+ if (hasSSR) {
25
+ hydrate(h(module.default, props), island);
26
+ } else {
27
+ render(h(module.default, props), island);
28
+ }
29
+ console.log(`🏝️ Island ${hasSSR ? 'hydrated' : 'mounted'} (auto): ${name}`);
30
+ } else {
31
+ console.error(`Island ${name} must export a mount() function or be a Preact component.`);
32
+ }
33
+ } catch (error) {
34
+ console.error(`Failed to load island: ${name}`, error);
35
+ island.removeAttribute('data-hydrated'); // Retry on next pass if failed?
36
+ }
37
+ }
38
+
39
+ async function hydrateAll() {
40
+ const islands = document.querySelectorAll('[data-island]');
41
+ for (const island of islands) {
42
+ hydrateIsland(island);
43
+ }
44
+ }
45
+
46
+ // Initial hydration
47
+ document.addEventListener('DOMContentLoaded', hydrateAll);
48
+
49
+ // Support for Turbo Drive / Turbolinks
50
+ document.addEventListener('turbo:load', hydrateAll);
51
+ document.addEventListener('turbolinks:load', hydrateAll);
52
+
53
+ // Expose for manual hydration (e.g. HTMX)
54
+ globalThis.Salvia = globalThis.Salvia || {};
55
+ globalThis.Salvia.hydrateAll = hydrateAll;
@@ -0,0 +1,19 @@
1
+ import { h } from "preact";
2
+ import Island from "../components/Island.tsx";
3
+ import Counter from "../islands/Counter.tsx";
4
+
5
+ export default function Home({ title }: { title: string }) {
6
+ return (
7
+ <html>
8
+ <head>
9
+ <title>{title}</title>
10
+ <script type="module" src="/assets/javascripts/islands.js"></script>
11
+ </head>
12
+ <body class="p-8">
13
+ <h1 class="text-3xl font-bold mb-4">{title}</h1>
14
+ <p class="mb-4">This is a Server Component (Page).</p>
15
+ <Island name="Counter" component={Counter} />
16
+ </body>
17
+ </html>
18
+ );
19
+ }
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env -S deno run --allow-all
2
+ // deno-lint-ignore-file
3
+ /**
4
+ * Salvia Build Script
5
+ *
6
+ * Build SSR Islands + Tailwind CSS
7
+ *
8
+ * Usage:
9
+ * deno run --allow-all build_ssr.ts
10
+ * deno run --allow-all build_ssr.ts --watch
11
+ */
12
+
13
+ import * as esbuild from "https://deno.land/x/esbuild@v0.24.2/mod.js";
14
+ import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11";
15
+
16
+ // Resolve deno.json relative to this script
17
+ let CONFIG_PATH = new URL("./deno.json", import.meta.url).pathname;
18
+
19
+ // Check for user config in project root
20
+ const USER_CONFIG_PATH = `${Deno.cwd()}/salvia/deno.json`;
21
+ try {
22
+ await Deno.stat(USER_CONFIG_PATH);
23
+ CONFIG_PATH = USER_CONFIG_PATH;
24
+ } catch {
25
+ // User config not found, use internal one
26
+ }
27
+
28
+ // When running via `deno task --config salvia/deno.json`, CWD is usually the project root.
29
+ // But if running from inside salvia/, it's different.
30
+ const ROOT_DIR = Deno.cwd().endsWith("/salvia") ? "." : "salvia";
31
+ const ISLANDS_DIR = `${ROOT_DIR}/app/islands`;
32
+ const PAGES_DIR = `${ROOT_DIR}/app/pages`;
33
+ const COMPONENTS_DIR = `${ROOT_DIR}/app/components`;
34
+ const SSR_OUTPUT_DIR = `${ROOT_DIR}/server`;
35
+ const CLIENT_OUTPUT_DIR = `${ROOT_DIR}/../public/assets/islands`;
36
+ const WATCH_MODE = Deno.args.includes("--watch");
37
+ const VERBOSE = Deno.args.includes("--verbose");
38
+
39
+ // ============================================
40
+ // SSR Islands Build
41
+ // ============================================
42
+
43
+ interface IslandFile {
44
+ path: string;
45
+ name: string;
46
+ clientOnly: boolean;
47
+ isPage: boolean;
48
+ }
49
+
50
+ async function findIslandFiles(): Promise<IslandFile[]> {
51
+ const files: IslandFile[] = [];
52
+
53
+ const scan = async (dir: string, isPage: boolean) => {
54
+ try {
55
+ for await (const entry of Deno.readDir(dir)) {
56
+ if (entry.isFile && (entry.name.endsWith(".tsx") || entry.name.endsWith(".jsx") || entry.name.endsWith(".js"))) {
57
+ if (entry.name.startsWith("_")) continue; // Skip internal files
58
+ const path = `${dir}/${entry.name}`;
59
+ const content = await Deno.readTextFile(path);
60
+ const clientOnly = content.trimStart().startsWith('"client only"') ||
61
+ content.trimStart().startsWith("'client only'");
62
+ const name = entry.name.replace(/\.(tsx|jsx|js)$/, "");
63
+ files.push({ path, name, clientOnly, isPage });
64
+ }
65
+ }
66
+ } catch {
67
+ // Directory might not exist
68
+ }
69
+ };
70
+
71
+ await scan(ISLANDS_DIR, false);
72
+ await scan(PAGES_DIR, true);
73
+
74
+ if (files.length === 0) {
75
+ console.log("📁 No components found in app/islands or app/pages.");
76
+ }
77
+
78
+ return files;
79
+ }
80
+
81
+ async function buildSSR() {
82
+ const islandFiles = await findIslandFiles();
83
+
84
+ if (islandFiles.length === 0) {
85
+ console.log("⚠️ No Island components found. Skipping SSR build.");
86
+ return;
87
+ }
88
+
89
+ const ssrFiles = islandFiles.filter(f => !f.clientOnly);
90
+ const clientFiles = islandFiles.filter(f => !f.isPage);
91
+
92
+ if (VERBOSE) {
93
+ console.log("🔍 SSR targets:", ssrFiles.map(f => f.name));
94
+ console.log("🔍 Client targets:", clientFiles.map(f => f.name));
95
+ }
96
+
97
+ // Create output directories
98
+ await Deno.mkdir(SSR_OUTPUT_DIR, { recursive: true });
99
+ await Deno.mkdir(CLIENT_OUTPUT_DIR, { recursive: true });
100
+
101
+ try {
102
+ // SSR bundle (for QuickJS) - single bundle with all components
103
+ if (ssrFiles.length > 0) {
104
+ // Create a virtual entry point that exports all components
105
+ // Get just the filename from the path
106
+ const entryCode = ssrFiles.map(f => {
107
+ let importPath = f.path;
108
+ // f.path is like "../app/islands/Counter.tsx" or "../app/pages/Home.tsx"
109
+ // We are writing _ssr_entry.js to ISLANDS_DIR ("../app/islands")
110
+
111
+ if (f.path.startsWith(ISLANDS_DIR)) {
112
+ importPath = `./${f.path.split("/").pop()}`;
113
+ } else if (f.path.startsWith(PAGES_DIR)) {
114
+ // From ../app/islands to ../app/pages is ../pages
115
+ importPath = `../pages/${f.path.split("/").pop()}`;
116
+ }
117
+ return `import ${f.name} from "${importPath}";`;
118
+ }).join("\n") + `
119
+ import { h } from "preact";
120
+ import renderToString from "preact-render-to-string";
121
+
122
+ // Salvia SSR Runtime
123
+ const components = {
124
+ ${ssrFiles.map(f => ` "${f.name}": ${f.name}`).join(",\n")}
125
+ };
126
+
127
+ globalThis.SalviaSSR = {
128
+ render: function(name, props) {
129
+ const Component = components[name];
130
+ if (!Component) {
131
+ throw new Error("Component not found: " + name);
132
+ }
133
+ const vnode = h(Component, props);
134
+ return renderToString(vnode);
135
+ }
136
+ };
137
+ export default {}; // Ensure it's a module
138
+ `;
139
+ const entryPath = `${ISLANDS_DIR}/_ssr_entry.js`;
140
+ await Deno.writeTextFile(entryPath, entryCode);
141
+
142
+ await esbuild.build({
143
+ entryPoints: [entryPath],
144
+ bundle: true,
145
+ format: "iife",
146
+ outfile: `${SSR_OUTPUT_DIR}/ssr_bundle.js`,
147
+ platform: "neutral",
148
+ plugins: [...denoPlugins({ configPath: CONFIG_PATH })],
149
+ external: [],
150
+ jsx: "automatic",
151
+ jsxImportSource: "preact",
152
+ banner: {
153
+ js: `// Salvia SSR Bundle - Generated at ${new Date().toISOString()}`,
154
+ },
155
+ });
156
+
157
+ // Clean up temp file
158
+ await Deno.remove(entryPath);
159
+
160
+ console.log(`✅ SSR bundle built: ${SSR_OUTPUT_DIR}/ssr_bundle.js (${ssrFiles.map(f => f.name).join(", ")})`);
161
+ }
162
+
163
+ // Client bundle (for hydration) - all files
164
+ // We need to wrap each component with a mount function
165
+ const clientEntryPoints = [];
166
+
167
+ for (const file of clientFiles) {
168
+ const filename = file.path.split("/").pop();
169
+ const wrapperCode = `
170
+ import Component from "./${filename}";
171
+ import { h, hydrate, render } from "preact";
172
+
173
+ export function mount(element, props, options) {
174
+ const vnode = h(Component, props);
175
+ if (options && options.hydrate) {
176
+ hydrate(vnode, element);
177
+ } else {
178
+ render(vnode, element);
179
+ }
180
+ }
181
+ `;
182
+ const wrapperPath = `${ISLANDS_DIR}/_client_${file.name}.js`;
183
+ await Deno.writeTextFile(wrapperPath, wrapperCode);
184
+ clientEntryPoints.push({ in: wrapperPath, out: file.name });
185
+ }
186
+
187
+ try {
188
+ await esbuild.build({
189
+ entryPoints: clientEntryPoints,
190
+ bundle: true,
191
+ format: "esm",
192
+ outdir: CLIENT_OUTPUT_DIR,
193
+ platform: "browser",
194
+ plugins: [...denoPlugins({ configPath: CONFIG_PATH })],
195
+ external: ["preact", "preact/hooks", "preact/jsx-runtime"],
196
+ jsx: "automatic",
197
+ jsxImportSource: "preact",
198
+ minify: true,
199
+ banner: {
200
+ js: `// Salvia Client Islands - Generated at ${new Date().toISOString()}`,
201
+ },
202
+ });
203
+ } finally {
204
+ // Clean up temp files
205
+ for (const entry of clientEntryPoints) {
206
+ try {
207
+ await Deno.remove(entry.in);
208
+ } catch {
209
+ // Ignore if file doesn't exist
210
+ }
211
+ }
212
+ }
213
+
214
+ console.log(`✅ Client Islands built: ${CLIENT_OUTPUT_DIR}/ (${clientFiles.map(f => f.name).join(", ")})`);
215
+
216
+ // Generate manifest (which Islands are client only)
217
+ const manifest = Object.fromEntries(
218
+ islandFiles.map(f => [f.name, { clientOnly: f.clientOnly, serverOnly: f.isPage }])
219
+ );
220
+ await Deno.writeTextFile(
221
+ `${SSR_OUTPUT_DIR}/manifest.json`,
222
+ JSON.stringify(manifest, null, 2)
223
+ );
224
+ console.log(`✅ Manifest generated: ${SSR_OUTPUT_DIR}/manifest.json`);
225
+
226
+ // Copy islands.js loader
227
+ try {
228
+ const islandsJsPath = new URL("../javascripts/islands.js", import.meta.url).pathname;
229
+ const islandsJsOutput = `${ROOT_DIR}/../public/assets/javascripts/islands.js`;
230
+ await Deno.mkdir(`${ROOT_DIR}/../public/assets/javascripts`, { recursive: true });
231
+ await Deno.copyFile(islandsJsPath, islandsJsOutput);
232
+ console.log(`✅ Loader copied: ${islandsJsOutput}`);
233
+ } catch (e) {
234
+ console.warn("⚠️ Failed to copy islands.js loader:", e);
235
+ }
236
+
237
+ } catch (error) {
238
+ const e = error as Error;
239
+ console.error("❌ SSR build error:", e.message || error);
240
+ }
241
+ }
242
+
243
+ // ============================================
244
+ // Main Build
245
+ // ============================================
246
+
247
+ async function build() {
248
+ await Promise.all([
249
+ buildSSR(),
250
+ ]);
251
+ }
252
+
253
+ async function watch() {
254
+ console.log("👀 Watching for file changes...");
255
+
256
+ // Watch Islands source
257
+ const watchDirs = [ISLANDS_DIR, PAGES_DIR, COMPONENTS_DIR, "./app/views"];
258
+
259
+ for (const dir of watchDirs) {
260
+ (async () => {
261
+ try {
262
+ const watcher = Deno.watchFs(dir);
263
+ let debounceTimer: number | undefined;
264
+
265
+ for await (const event of watcher) {
266
+ // Ignore generated files
267
+ if (event.paths.some(p => p.includes("_ssr_entry.js") || p.includes("_client_"))) {
268
+ continue;
269
+ }
270
+
271
+ if (event.kind === "modify" || event.kind === "create" || event.kind === "remove") {
272
+ clearTimeout(debounceTimer);
273
+ debounceTimer = setTimeout(async () => {
274
+ console.log(`🔄 Changes detected in ${dir}, rebuilding...`);
275
+ await build();
276
+ }, 100);
277
+ }
278
+ }
279
+ } catch {
280
+ // Skip if directory doesn't exist
281
+ }
282
+ })();
283
+ }
284
+
285
+ // Wait indefinitely
286
+ await new Promise(() => {});
287
+ }
288
+
289
+ // Main execution
290
+ console.log("🌿 Salvia Build (SSR + Tailwind)");
291
+ console.log("================================");
292
+ await build();
293
+
294
+ if (WATCH_MODE) {
295
+ await watch();
296
+ } else {
297
+ await esbuild.stop();
298
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "imports": {
3
+ "preact": "https://esm.sh/preact@10.19.3",
4
+ "preact/hooks": "https://esm.sh/preact@10.19.3/hooks",
5
+ "preact/jsx-runtime": "https://esm.sh/preact@10.19.3/jsx-runtime",
6
+ "preact-render-to-string": "https://esm.sh/preact-render-to-string@6.3.1?external=preact",
7
+ "@preact/signals": "https://esm.sh/@preact/signals@1.2.2?external=preact",
8
+ "@hotwired/turbo": "https://esm.sh/@hotwired/turbo@8.0.0",
9
+ "@/": "./app/"
10
+ },
11
+ "compilerOptions": {
12
+ "jsx": "react-jsx",
13
+ "jsxImportSource": "preact"
14
+ }
15
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "version": "5",
3
+ "specifiers": {
4
+ "jsr:@luca/esbuild-deno-loader@0.11": "0.11.1",
5
+ "jsr:@std/bytes@^1.0.2": "1.0.6",
6
+ "jsr:@std/encoding@^1.0.5": "1.0.10",
7
+ "jsr:@std/internal@^1.0.12": "1.0.12",
8
+ "jsr:@std/path@^1.0.6": "1.1.3"
9
+ },
10
+ "jsr": {
11
+ "@luca/esbuild-deno-loader@0.11.1": {
12
+ "integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
13
+ "dependencies": [
14
+ "jsr:@std/bytes",
15
+ "jsr:@std/encoding",
16
+ "jsr:@std/path"
17
+ ]
18
+ },
19
+ "@std/bytes@1.0.6": {
20
+ "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
21
+ },
22
+ "@std/encoding@1.0.10": {
23
+ "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
24
+ },
25
+ "@std/internal@1.0.12": {
26
+ "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
27
+ },
28
+ "@std/path@1.1.3": {
29
+ "integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3",
30
+ "dependencies": [
31
+ "jsr:@std/internal"
32
+ ]
33
+ }
34
+ },
35
+ "redirects": {
36
+ "https://esm.sh/@preact/signals-core@^1.4.0?target=denonext": "https://esm.sh/@preact/signals-core@1.12.1?target=denonext"
37
+ },
38
+ "remote": {
39
+ "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6",
40
+ "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4",
41
+ "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d",
42
+ "https://deno.land/x/esbuild@v0.24.2/mod.js": "8d1e46a6494585235b0514d37743ee48a4f6f0b8e00fca9d0a2e371914b1df0e",
43
+ "https://esm.sh/@preact/signals-core@1.12.1/denonext/signals-core.mjs": "7ec7397ff664d18002330248c74ad39a8f65ad2db3a2ecdfdc9874d3585cd32a",
44
+ "https://esm.sh/@preact/signals-core@1.12.1?target=denonext": "cc0c788b17ca5e38bc693aceccad467cdcd2c576974444d0bf65309619b4bf20",
45
+ "https://esm.sh/@preact/signals@1.2.2/X-ZXByZWFjdA/denonext/signals.mjs": "b588b595eda1161dbb91eab1170c293d89194faa41a4dfc50c04986869f79f6a",
46
+ "https://esm.sh/@preact/signals@1.2.2?external=preact": "fc9fb49e945d8e44b00d8fbed96594355ad0cefc580a5da9f6488f617dca8773",
47
+ "https://esm.sh/preact-render-to-string@6.3.1/X-ZXByZWFjdA/denonext/preact-render-to-string.mjs": "859e9f3dc137a53c17542bbed4cf8accdca81a2895d7a52d1fca38e6c5c00c90",
48
+ "https://esm.sh/preact-render-to-string@6.3.1?external=preact": "3bc8562bab5e16dd3c984d996abdc37d25880319bb9eac3169e97a9c7fa51243",
49
+ "https://esm.sh/preact@10.19.3": "e665fa7cfbac33461e7ecf79f8b361a8f831b3e7edc8089d1f95e05fed6a8be1",
50
+ "https://esm.sh/preact@10.19.3/denonext/hooks.mjs": "1e6dcdc31a6d4c4fecf1665538f892bb33bbee8fee0f3c664b9db5362871849f",
51
+ "https://esm.sh/preact@10.19.3/denonext/jsx-runtime.mjs": "a151a1f662b5e3023385100c46e2c4142489d3b982cb094a08af8a3ca7b30c1d",
52
+ "https://esm.sh/preact@10.19.3/denonext/preact.mjs": "6233d303372951bb69428bcd7fd4a6617fc721edee334f228ae8b2e5ab6a2e7f",
53
+ "https://esm.sh/preact@10.19.3/hooks": "ec6d1409b12bce994ec7f0711b17e41424e339727af83fb471acc63952e35278",
54
+ "https://esm.sh/preact@10.19.3/jsx-runtime": "c0a489946b2a492ed590365ce1dbb2606adef27635722687b7d6320f57faf435"
55
+ }
56
+ }