salvia 0.2.0 → 0.2.1
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 +4 -4
- data/.vscode/settings.json +4 -0
- data/Gemfile +8 -0
- data/Rakefile +12 -0
- data/assets/islands/Counter.tsx +2 -2
- data/assets/javascripts/islands.js +2 -1
- data/assets/scripts/build.ts +91 -41
- data/assets/scripts/deno.json +3 -3
- data/assets/scripts/sidecar.ts +47 -23
- data/assets/scripts/vendor_setup.ts +16 -6
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/salvia/cli.rb +40 -9
- data/lib/salvia/core/import_map.rb +120 -20
- data/lib/salvia/helpers/island.rb +10 -50
- data/lib/salvia/server/dev_server.rb +22 -8
- data/lib/salvia/server/sidecar.rb +72 -35
- data/lib/salvia/ssr/dom_mock.rb +11 -3
- data/lib/salvia/ssr/quickjs.rb +97 -34
- data/lib/salvia/ssr.rb +41 -5
- data/lib/salvia/version.rb +1 -1
- metadata +11 -6
- data/README.md +0 -152
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c51e30591df5e8347faf21b95bf57e6d324feb29f10c5c82070ff62d23f09cb
|
|
4
|
+
data.tar.gz: 696e105566fc056cefa6b947923d3c0e60527c9b38d9740b6b9c79b90cf1fd1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a90257f44c7730110d0e94138b00b0d9e8607f9a40736d8cb29b64a7bd5f47a895ffeece690f6b9a7426a3a38f942ee5417297819ade4bfacbd20ba290bd856a
|
|
7
|
+
data.tar.gz: 3bb8812e772f2fa8a27696863c5711c564257d87048468921e9e5f2883121900d4e306e4a080080f54e3bfc6604bb5a1c8d6dc413afcae565e3ea2ea08d27270
|
data/Gemfile
ADDED
data/Rakefile
ADDED
data/assets/islands/Counter.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { h } from "preact";
|
|
2
2
|
import { useState } from "preact/hooks";
|
|
3
3
|
|
|
4
|
-
export default function Counter() {
|
|
5
|
-
const [count, setCount] = useState(
|
|
4
|
+
export default function Counter({ count: initialCount = 0 }: { count?: number }) {
|
|
5
|
+
const [count, setCount] = useState(initialCount);
|
|
6
6
|
return (
|
|
7
7
|
<div class="p-4 border rounded-lg">
|
|
8
8
|
<p class="text-lg mb-2">Count: {count}</p>
|
|
@@ -14,7 +14,8 @@ async function hydrateIsland(island) {
|
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
// Use the import map alias which handles dev/prod paths
|
|
17
|
-
|
|
17
|
+
// We import without extension to let Import Map handle resolution (including hashing in prod)
|
|
18
|
+
const module = await import(`@/islands/${name}`);
|
|
18
19
|
|
|
19
20
|
if (typeof module.mount === 'function') {
|
|
20
21
|
module.mount(island, props, { hydrate: hasSSR });
|
data/assets/scripts/build.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env -S deno run --allow-
|
|
1
|
+
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --allow-run
|
|
2
2
|
// deno-lint-ignore-file
|
|
3
3
|
/**
|
|
4
4
|
* Salvia Build Script
|
|
@@ -13,11 +13,16 @@
|
|
|
13
13
|
import * as esbuild from "https://deno.land/x/esbuild@v0.24.2/mod.js";
|
|
14
14
|
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11";
|
|
15
15
|
|
|
16
|
+
const WATCH_MODE = Deno.args.includes("--watch");
|
|
17
|
+
const VERBOSE = Deno.args.includes("--verbose");
|
|
18
|
+
|
|
19
|
+
import * as path from "https://deno.land/std@0.224.0/path/mod.ts";
|
|
20
|
+
|
|
16
21
|
// Resolve deno.json relative to this script
|
|
17
22
|
let CONFIG_PATH = new URL("./deno.json", import.meta.url).pathname;
|
|
18
23
|
|
|
19
24
|
// Check for user config in project root
|
|
20
|
-
const USER_CONFIG_PATH =
|
|
25
|
+
const USER_CONFIG_PATH = path.join(Deno.cwd(), "salvia", "deno.json");
|
|
21
26
|
try {
|
|
22
27
|
await Deno.stat(USER_CONFIG_PATH);
|
|
23
28
|
CONFIG_PATH = USER_CONFIG_PATH;
|
|
@@ -25,16 +30,30 @@ try {
|
|
|
25
30
|
// User config not found, use internal one
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
// Load externals from deno.json
|
|
34
|
+
let externals = ["preact", "preact/hooks", "preact/jsx-runtime"];
|
|
35
|
+
try {
|
|
36
|
+
const configContent = await Deno.readTextFile(CONFIG_PATH);
|
|
37
|
+
const config = JSON.parse(configContent);
|
|
38
|
+
if (config.imports) {
|
|
39
|
+
const importKeys = Object.keys(config.imports);
|
|
40
|
+
externals = [...new Set([...externals, ...importKeys])];
|
|
41
|
+
}
|
|
42
|
+
if (VERBOSE) {
|
|
43
|
+
console.log("📦 Externals loaded from deno.json:", externals);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn("⚠️ Failed to load externals from deno.json:", e);
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
// When running via `deno task --config salvia/deno.json`, CWD is usually the project root.
|
|
29
50
|
// But if running from inside salvia/, it's different.
|
|
30
51
|
const ROOT_DIR = Deno.cwd().endsWith("/salvia") ? "." : "salvia";
|
|
31
|
-
const ISLANDS_DIR =
|
|
32
|
-
const PAGES_DIR =
|
|
33
|
-
const COMPONENTS_DIR =
|
|
34
|
-
const SSR_OUTPUT_DIR =
|
|
35
|
-
const CLIENT_OUTPUT_DIR =
|
|
36
|
-
const WATCH_MODE = Deno.args.includes("--watch");
|
|
37
|
-
const VERBOSE = Deno.args.includes("--verbose");
|
|
52
|
+
const ISLANDS_DIR = path.join(ROOT_DIR, "app", "islands");
|
|
53
|
+
const PAGES_DIR = path.join(ROOT_DIR, "app", "pages");
|
|
54
|
+
const COMPONENTS_DIR = path.join(ROOT_DIR, "app", "components");
|
|
55
|
+
const SSR_OUTPUT_DIR = path.join(ROOT_DIR, "server");
|
|
56
|
+
const CLIENT_OUTPUT_DIR = path.join(ROOT_DIR, "..", "public", "assets", "islands");
|
|
38
57
|
|
|
39
58
|
// ============================================
|
|
40
59
|
// SSR Islands Build
|
|
@@ -55,12 +74,12 @@ async function findIslandFiles(): Promise<IslandFile[]> {
|
|
|
55
74
|
for await (const entry of Deno.readDir(dir)) {
|
|
56
75
|
if (entry.isFile && (entry.name.endsWith(".tsx") || entry.name.endsWith(".jsx") || entry.name.endsWith(".js"))) {
|
|
57
76
|
if (entry.name.startsWith("_")) continue; // Skip internal files
|
|
58
|
-
const
|
|
59
|
-
const content = await Deno.readTextFile(
|
|
77
|
+
const filePath = path.join(dir, entry.name);
|
|
78
|
+
const content = await Deno.readTextFile(filePath);
|
|
60
79
|
const clientOnly = content.trimStart().startsWith('"client only"') ||
|
|
61
80
|
content.trimStart().startsWith("'client only'");
|
|
62
81
|
const name = entry.name.replace(/\.(tsx|jsx|js)$/, "");
|
|
63
|
-
files.push({ path, name, clientOnly, isPage });
|
|
82
|
+
files.push({ path: filePath, name, clientOnly, isPage });
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
85
|
} catch {
|
|
@@ -104,16 +123,11 @@ async function buildSSR() {
|
|
|
104
123
|
// Create a virtual entry point that exports all components
|
|
105
124
|
// Get just the filename from the path
|
|
106
125
|
const entryCode = ssrFiles.map(f => {
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
}
|
|
126
|
+
// Calculate relative path from ISLANDS_DIR (where _ssr_entry.js will be) to the component file
|
|
127
|
+
// This handles nested directories and different source roots correctly
|
|
128
|
+
const relativePath = path.relative(ISLANDS_DIR, f.path);
|
|
129
|
+
// Ensure path starts with ./ or ../ for import
|
|
130
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
117
131
|
return `import ${f.name} from "${importPath}";`;
|
|
118
132
|
}).join("\n") + `
|
|
119
133
|
import { h } from "preact";
|
|
@@ -130,20 +144,22 @@ globalThis.SalviaSSR = {
|
|
|
130
144
|
if (!Component) {
|
|
131
145
|
throw new Error("Component not found: " + name);
|
|
132
146
|
}
|
|
133
|
-
|
|
147
|
+
// Handle default export if necessary
|
|
148
|
+
const Comp = Component.default || Component;
|
|
149
|
+
const vnode = h(Comp, props);
|
|
134
150
|
return renderToString(vnode);
|
|
135
151
|
}
|
|
136
152
|
};
|
|
137
153
|
export default {}; // Ensure it's a module
|
|
138
154
|
`;
|
|
139
|
-
const entryPath =
|
|
155
|
+
const entryPath = path.join(ISLANDS_DIR, "_ssr_entry.js");
|
|
140
156
|
await Deno.writeTextFile(entryPath, entryCode);
|
|
141
157
|
|
|
142
158
|
await esbuild.build({
|
|
143
159
|
entryPoints: [entryPath],
|
|
144
160
|
bundle: true,
|
|
145
161
|
format: "iife",
|
|
146
|
-
outfile:
|
|
162
|
+
outfile: path.join(SSR_OUTPUT_DIR, "ssr_bundle.js"),
|
|
147
163
|
platform: "neutral",
|
|
148
164
|
plugins: [...denoPlugins({ configPath: CONFIG_PATH })],
|
|
149
165
|
external: [],
|
|
@@ -157,7 +173,7 @@ export default {}; // Ensure it's a module
|
|
|
157
173
|
// Clean up temp file
|
|
158
174
|
await Deno.remove(entryPath);
|
|
159
175
|
|
|
160
|
-
console.log(`✅ SSR bundle built: ${SSR_OUTPUT_DIR
|
|
176
|
+
console.log(`✅ SSR bundle built: ${path.join(SSR_OUTPUT_DIR, "ssr_bundle.js")} (${ssrFiles.map(f => f.name).join(", ")})`);
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
// Client bundle (for hydration) - all files
|
|
@@ -165,9 +181,12 @@ export default {}; // Ensure it's a module
|
|
|
165
181
|
const clientEntryPoints = [];
|
|
166
182
|
|
|
167
183
|
for (const file of clientFiles) {
|
|
168
|
-
const filename =
|
|
184
|
+
const filename = path.basename(file.path);
|
|
185
|
+
const relativePath = path.relative(ISLANDS_DIR, file.path);
|
|
186
|
+
const importPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
187
|
+
|
|
169
188
|
const wrapperCode = `
|
|
170
|
-
import Component from "
|
|
189
|
+
import Component from "${importPath}";
|
|
171
190
|
import { h, hydrate, render } from "preact";
|
|
172
191
|
|
|
173
192
|
export function mount(element, props, options) {
|
|
@@ -179,27 +198,68 @@ export function mount(element, props, options) {
|
|
|
179
198
|
}
|
|
180
199
|
}
|
|
181
200
|
`;
|
|
182
|
-
const wrapperPath =
|
|
201
|
+
const wrapperPath = path.join(ISLANDS_DIR, `_client_${file.name}.js`);
|
|
183
202
|
await Deno.writeTextFile(wrapperPath, wrapperCode);
|
|
184
203
|
clientEntryPoints.push({ in: wrapperPath, out: file.name });
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
try {
|
|
188
|
-
await esbuild.build({
|
|
207
|
+
const result = await esbuild.build({
|
|
189
208
|
entryPoints: clientEntryPoints,
|
|
190
209
|
bundle: true,
|
|
191
210
|
format: "esm",
|
|
192
211
|
outdir: CLIENT_OUTPUT_DIR,
|
|
193
212
|
platform: "browser",
|
|
194
213
|
plugins: [...denoPlugins({ configPath: CONFIG_PATH })],
|
|
195
|
-
external:
|
|
214
|
+
external: externals,
|
|
196
215
|
jsx: "automatic",
|
|
197
216
|
jsxImportSource: "preact",
|
|
198
217
|
minify: true,
|
|
218
|
+
entryNames: "[name]-[hash]",
|
|
219
|
+
metafile: true,
|
|
199
220
|
banner: {
|
|
200
221
|
js: `// Salvia Client Islands - Generated at ${new Date().toISOString()}`,
|
|
201
222
|
},
|
|
202
223
|
});
|
|
224
|
+
|
|
225
|
+
// Parse metafile to map original names to hashed filenames
|
|
226
|
+
const outputs = result.metafile?.outputs || {};
|
|
227
|
+
const fileMap: Record<string, string> = {};
|
|
228
|
+
|
|
229
|
+
for (const [path, _info] of Object.entries(outputs)) {
|
|
230
|
+
// path is like "salvia/../public/assets/islands/Counter-HASH.js"
|
|
231
|
+
// We need to extract "Counter" and the filename "Counter-HASH.js"
|
|
232
|
+
const filename = path.split("/").pop();
|
|
233
|
+
if (!filename) continue;
|
|
234
|
+
|
|
235
|
+
// Check if this is one of our entry points
|
|
236
|
+
for (const entry of clientEntryPoints) {
|
|
237
|
+
// entry.out is "Counter"
|
|
238
|
+
// filename starts with "Counter-" and ends with ".js"
|
|
239
|
+
if (filename.startsWith(`${entry.out}-`) && filename.endsWith(".js")) {
|
|
240
|
+
fileMap[entry.out] = filename;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Generate manifest (which Islands are client only + file paths)
|
|
247
|
+
const manifest = Object.fromEntries(
|
|
248
|
+
islandFiles.map(f => [
|
|
249
|
+
f.name,
|
|
250
|
+
{
|
|
251
|
+
clientOnly: f.clientOnly,
|
|
252
|
+
serverOnly: f.isPage,
|
|
253
|
+
file: fileMap[f.name] // Add hashed filename if available
|
|
254
|
+
}
|
|
255
|
+
])
|
|
256
|
+
);
|
|
257
|
+
await Deno.writeTextFile(
|
|
258
|
+
`${SSR_OUTPUT_DIR}/manifest.json`,
|
|
259
|
+
JSON.stringify(manifest, null, 2)
|
|
260
|
+
);
|
|
261
|
+
console.log(`✅ Manifest generated: ${SSR_OUTPUT_DIR}/manifest.json`);
|
|
262
|
+
|
|
203
263
|
} finally {
|
|
204
264
|
// Clean up temp files
|
|
205
265
|
for (const entry of clientEntryPoints) {
|
|
@@ -213,16 +273,6 @@ export function mount(element, props, options) {
|
|
|
213
273
|
|
|
214
274
|
console.log(`✅ Client Islands built: ${CLIENT_OUTPUT_DIR}/ (${clientFiles.map(f => f.name).join(", ")})`);
|
|
215
275
|
|
|
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
276
|
// Copy islands.js loader
|
|
227
277
|
try {
|
|
228
278
|
const islandsJsPath = new URL("../javascripts/islands.js", import.meta.url).pathname;
|
data/assets/scripts/deno.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"imports": {
|
|
3
|
-
"preact": "https://esm.sh/preact@10.19.
|
|
4
|
-
"preact/hooks": "https://esm.sh/preact@10.19.
|
|
5
|
-
"preact/jsx-runtime": "https://esm.sh/preact@10.19.
|
|
3
|
+
"preact": "https://esm.sh/preact@10.19.6",
|
|
4
|
+
"preact/hooks": "https://esm.sh/preact@10.19.6/hooks",
|
|
5
|
+
"preact/jsx-runtime": "https://esm.sh/preact@10.19.6/jsx-runtime",
|
|
6
6
|
"preact-render-to-string": "https://esm.sh/preact-render-to-string@6.3.1?external=preact",
|
|
7
7
|
"@preact/signals": "https://esm.sh/@preact/signals@1.2.2?external=preact",
|
|
8
8
|
"@hotwired/turbo": "https://esm.sh/@hotwired/turbo@8.0.0",
|
data/assets/scripts/sidecar.ts
CHANGED
|
@@ -18,6 +18,28 @@ const handler = async (request: Request): Promise<Response> => {
|
|
|
18
18
|
if (command === "bundle") {
|
|
19
19
|
const { entryPoint, externals, format, globalName, configPath } = params;
|
|
20
20
|
|
|
21
|
+
// Load globals from deno.json
|
|
22
|
+
const defaultGlobals: Record<string, string> = {
|
|
23
|
+
"preact": "globalThis.Preact",
|
|
24
|
+
"preact/hooks": "globalThis.PreactHooks",
|
|
25
|
+
"@preact/signals": "globalThis.PreactSignals",
|
|
26
|
+
"preact/jsx-runtime": "globalThis.PreactJsxRuntime",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let userGlobals: Record<string, string> = {};
|
|
30
|
+
try {
|
|
31
|
+
const cfgPath = configPath || `${Deno.cwd()}/salvia/deno.json`;
|
|
32
|
+
const configText = await Deno.readTextFile(cfgPath);
|
|
33
|
+
const config = JSON.parse(configText);
|
|
34
|
+
if (config.salvia && config.salvia.globals) {
|
|
35
|
+
userGlobals = config.salvia.globals;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore if config not found or invalid
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const allGlobals = { ...defaultGlobals, ...userGlobals };
|
|
42
|
+
|
|
21
43
|
// If format is IIFE, we need to handle externals by mapping them to globals
|
|
22
44
|
// But esbuild doesn't support this out of the box for IIFE with externals.
|
|
23
45
|
// We can use a plugin to rewrite imports to globals if they are in externals list.
|
|
@@ -26,28 +48,24 @@ const handler = async (request: Request): Promise<Response> => {
|
|
|
26
48
|
const globalExternalsPlugin = {
|
|
27
49
|
name: "global-externals",
|
|
28
50
|
setup(build: any) {
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return { path: args.path, namespace: "global-external" };
|
|
40
|
-
});
|
|
41
|
-
// JSX Runtime
|
|
42
|
-
build.onResolve({ filter: /^preact\/jsx-runtime$/ }, (args: any) => {
|
|
43
|
-
return { path: args.path, namespace: "global-external" };
|
|
44
|
-
});
|
|
51
|
+
// Register resolvers for all globals
|
|
52
|
+
for (const pkg of Object.keys(allGlobals)) {
|
|
53
|
+
// Escape regex special characters
|
|
54
|
+
const escapedPkg = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
55
|
+
const filter = new RegExp(`^${escapedPkg}$`);
|
|
56
|
+
|
|
57
|
+
build.onResolve({ filter }, (args: any) => {
|
|
58
|
+
return { path: args.path, namespace: "global-external" };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
45
61
|
|
|
46
62
|
build.onLoad({ filter: /.*/, namespace: "global-external" }, (args: any) => {
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
const globalVar = allGlobals[args.path];
|
|
64
|
+
if (globalVar) {
|
|
65
|
+
// Use module.exports to support both default and named imports via esbuild's CommonJS interop.
|
|
66
|
+
// This requires a minimal CommonJS shim (module.exports) in the execution environment (vendor_setup.ts).
|
|
67
|
+
return { contents: `module.exports = ${globalVar};`, loader: "js" };
|
|
68
|
+
}
|
|
51
69
|
return null;
|
|
52
70
|
});
|
|
53
71
|
},
|
|
@@ -152,9 +170,15 @@ const handler = async (request: Request): Promise<Response> => {
|
|
|
152
170
|
}
|
|
153
171
|
};
|
|
154
172
|
|
|
155
|
-
const server = Deno.serve({
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
const server = Deno.serve({
|
|
174
|
+
port: PORT,
|
|
175
|
+
onListen: ({ port, hostname }) => {
|
|
176
|
+
// Output JSON handshake for reliable parsing
|
|
177
|
+
const msg = JSON.stringify({ port, status: "ready" });
|
|
178
|
+
console.log(msg);
|
|
179
|
+
console.log(`[Deno Init] Listening on http://${hostname}:${port}/`);
|
|
180
|
+
}
|
|
181
|
+
}, handler);
|
|
158
182
|
|
|
159
183
|
// Handle cleanup on exit
|
|
160
184
|
const cleanup = () => {
|
|
@@ -14,12 +14,22 @@ import { renderToString } from "preact-render-to-string";
|
|
|
14
14
|
(globalThis as any).h = h;
|
|
15
15
|
(globalThis as any).Fragment = Fragment;
|
|
16
16
|
|
|
17
|
+
// Module registry for require shim
|
|
18
|
+
const moduleRegistry: Record<string, any> = {
|
|
19
|
+
"preact": preact,
|
|
20
|
+
"preact/hooks": hooks,
|
|
21
|
+
"@preact/signals": signals,
|
|
22
|
+
"preact/jsx-runtime": jsxRuntime,
|
|
23
|
+
"preact-render-to-string": { default: renderToString, renderToString }
|
|
24
|
+
};
|
|
25
|
+
|
|
17
26
|
// Simple require shim for JIT bundles
|
|
18
27
|
(globalThis as any).require = function(moduleName: string) {
|
|
19
|
-
if (moduleName
|
|
20
|
-
|
|
21
|
-
if (moduleName === "@preact/signals") return signals;
|
|
22
|
-
if (moduleName === "preact/jsx-runtime") return jsxRuntime;
|
|
23
|
-
if (moduleName === "preact-render-to-string") return { default: renderToString, renderToString };
|
|
24
|
-
throw new Error("Module not found: " + moduleName);
|
|
28
|
+
if (moduleRegistry[moduleName]) return moduleRegistry[moduleName];
|
|
29
|
+
throw new Error(`[Salvia SSR] Module not found: "${moduleName}". If this is an external dependency, ensure it is registered in vendor_setup.ts.`);
|
|
25
30
|
};
|
|
31
|
+
|
|
32
|
+
// Shim for module.exports (used by sidecar global-externals)
|
|
33
|
+
if (typeof (globalThis as any).module === 'undefined') {
|
|
34
|
+
(globalThis as any).module = { exports: {} };
|
|
35
|
+
}
|
data/bin/console
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "salvia"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
require "irb"
|
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/salvia/cli.rb
CHANGED
|
@@ -46,7 +46,11 @@ module Salvia
|
|
|
46
46
|
copy_file "assets/islands/Counter.tsx", "salvia/app/islands/Counter.tsx"
|
|
47
47
|
copy_file "assets/pages/Home.tsx", "salvia/app/pages/Home.tsx"
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
if File.exist?("salvia/.gitignore")
|
|
50
|
+
append_to_file "salvia/.gitignore", "\n/server/\n"
|
|
51
|
+
else
|
|
52
|
+
create_file "salvia/.gitignore", "/server/\n"
|
|
53
|
+
end
|
|
50
54
|
|
|
51
55
|
# Backend Setup
|
|
52
56
|
case backend
|
|
@@ -58,12 +62,6 @@ module Salvia
|
|
|
58
62
|
config.build_dir = Rails.root.join("public/assets")
|
|
59
63
|
config.ssr_bundle_path = Rails.root.join("salvia/server/ssr_bundle.js")
|
|
60
64
|
end
|
|
61
|
-
|
|
62
|
-
# Initialize SSR Engine
|
|
63
|
-
Salvia::SSR.configure(
|
|
64
|
-
bundle_path: Salvia.config.ssr_bundle_path,
|
|
65
|
-
development: Rails.env.development?
|
|
66
|
-
)
|
|
67
65
|
RUBY
|
|
68
66
|
end
|
|
69
67
|
say " - Created config/initializers/salvia.rb"
|
|
@@ -98,6 +96,25 @@ module Salvia
|
|
|
98
96
|
say " - app/assets/stylesheets/ : Tailwind CSS entry point created (v4)"
|
|
99
97
|
end
|
|
100
98
|
|
|
99
|
+
# Cache Deno dependencies
|
|
100
|
+
say ""
|
|
101
|
+
say "📦 Caching Deno dependencies...", :green
|
|
102
|
+
sidecar_script = File.expand_path("../../../assets/scripts/sidecar.ts", __FILE__)
|
|
103
|
+
|
|
104
|
+
# Use user's deno.json if available, otherwise fallback to internal
|
|
105
|
+
user_config = File.expand_path("salvia/deno.json")
|
|
106
|
+
config_path = if File.exist?(user_config)
|
|
107
|
+
user_config
|
|
108
|
+
else
|
|
109
|
+
File.expand_path("../../../assets/scripts/deno.json", __FILE__)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if system("deno cache --config #{config_path} #{sidecar_script}")
|
|
113
|
+
say " - Dependencies cached successfully"
|
|
114
|
+
else
|
|
115
|
+
say " - Warning: Failed to cache dependencies (non-fatal)", :yellow
|
|
116
|
+
end
|
|
117
|
+
|
|
101
118
|
say ""
|
|
102
119
|
say "✅ Salvia SSR installed!", :green
|
|
103
120
|
say " - salvia/app/islands/ : Put your interactive Island components here"
|
|
@@ -133,7 +150,7 @@ module Salvia
|
|
|
133
150
|
File.expand_path("../../../assets/scripts/deno.json", __FILE__)
|
|
134
151
|
end
|
|
135
152
|
|
|
136
|
-
cmd = "deno run --allow-
|
|
153
|
+
cmd = "deno run --allow-read --allow-write --allow-net --allow-env --allow-run --config #{config_path} #{build_script}"
|
|
137
154
|
cmd += " --verbose" if options[:verbose]
|
|
138
155
|
|
|
139
156
|
success = system(cmd)
|
|
@@ -144,6 +161,20 @@ module Salvia
|
|
|
144
161
|
say "❌ SSR build failed", :red
|
|
145
162
|
exit 1
|
|
146
163
|
end
|
|
164
|
+
|
|
165
|
+
# Build Tailwind CSS if available
|
|
166
|
+
if File.exist?("bin/rails")
|
|
167
|
+
say "🎨 Building Tailwind CSS...", :green
|
|
168
|
+
# Check if tailwindcss:build task exists
|
|
169
|
+
if system("bin/rails -T | grep tailwindcss:build > /dev/null 2>&1")
|
|
170
|
+
if system("bin/rails tailwindcss:build")
|
|
171
|
+
say "✅ Tailwind CSS build completed!", :green
|
|
172
|
+
else
|
|
173
|
+
say "❌ Tailwind CSS build failed", :red
|
|
174
|
+
# Don't exit here, as SSR build succeeded
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
147
178
|
end
|
|
148
179
|
|
|
149
180
|
desc "watch", "Watch and rebuild Island components"
|
|
@@ -164,7 +195,7 @@ module Salvia
|
|
|
164
195
|
File.expand_path("../../../assets/scripts/deno.json", __FILE__)
|
|
165
196
|
end
|
|
166
197
|
|
|
167
|
-
cmd = "deno run --allow-
|
|
198
|
+
cmd = "deno run --allow-read --allow-write --allow-net --allow-env --allow-run --config #{config_path} #{build_script} --watch"
|
|
168
199
|
cmd += " --verbose" if options[:verbose]
|
|
169
200
|
|
|
170
201
|
exec cmd
|