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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d47620b2511642dc1d75545d11cfda1f52071059a320bf1b181735af2bca2f41
4
- data.tar.gz: 0a7322cbf4477fa2bff9ad6664e658a056a24a9cec83b6943e17f1ffbaf00bac
3
+ metadata.gz: 7c51e30591df5e8347faf21b95bf57e6d324feb29f10c5c82070ff62d23f09cb
4
+ data.tar.gz: 696e105566fc056cefa6b947923d3c0e60527c9b38d9740b6b9c79b90cf1fd1a
5
5
  SHA512:
6
- metadata.gz: 8b677e392d348bb21b8e6bcdb02e90ece13021e3c710fdfa394395174097ff81ee19fb3513ec09e7f752a392a79c1b63593747ec57d697cc4675c9730d755072
7
- data.tar.gz: f55abe0fd41e6a76c5d39a728e56193ade7d986e32df1c49b05f9bfc0ba88b495f1b0483b686333dfbfca33c31b473f0953780472a81a09971f36fd3c7990dfd
6
+ metadata.gz: a90257f44c7730110d0e94138b00b0d9e8607f9a40736d8cb29b64a7bd5f47a895ffeece690f6b9a7426a3a38f942ee5417297819ade4bfacbd20ba290bd856a
7
+ data.tar.gz: 3bb8812e772f2fa8a27696863c5711c564257d87048468921e9e5f2883121900d4e306e4a080080f54e3bfc6604bb5a1c8d6dc413afcae565e3ea2ea08d27270
@@ -0,0 +1,4 @@
1
+ {
2
+ "deno.enable": false,
3
+ "deno.lint": false
4
+ }
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in salvia.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -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(0);
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
- const module = await import(`@/islands/${name}.js`);
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 });
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S deno run --allow-all
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 = `${Deno.cwd()}/salvia/deno.json`;
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 = `${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");
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 path = `${dir}/${entry.name}`;
59
- const content = await Deno.readTextFile(path);
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
- 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
- }
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
- const vnode = h(Component, props);
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 = `${ISLANDS_DIR}/_ssr_entry.js`;
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: `${SSR_OUTPUT_DIR}/ssr_bundle.js`,
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}/ssr_bundle.js (${ssrFiles.map(f => f.name).join(", ")})`);
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 = file.path.split("/").pop();
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 "./${filename}";
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 = `${ISLANDS_DIR}/_client_${file.name}.js`;
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: ["preact", "preact/hooks", "preact/jsx-runtime"],
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;
@@ -1,8 +1,8 @@
1
1
  {
2
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",
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",
@@ -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
- // Preact
30
- build.onResolve({ filter: /^preact$/ }, (args: any) => {
31
- return { path: args.path, namespace: "global-external" };
32
- });
33
- // Hooks
34
- build.onResolve({ filter: /^preact\/hooks$/ }, (args: any) => {
35
- return { path: args.path, namespace: "global-external" };
36
- });
37
- // Signals
38
- build.onResolve({ filter: /^@preact\/signals$/ }, (args: any) => {
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
- if (args.path === "preact") return { contents: "module.exports = globalThis.Preact;", loader: "js" };
48
- if (args.path === "preact/hooks") return { contents: "module.exports = globalThis.PreactHooks;", loader: "js" };
49
- if (args.path === "@preact/signals") return { contents: "module.exports = globalThis.PreactSignals;", loader: "js" };
50
- if (args.path === "preact/jsx-runtime") return { contents: "module.exports = globalThis.PreactJsxRuntime;", loader: "js" };
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({ port: PORT }, handler);
156
- // Output the assigned port so Ruby can read it
157
- console.log(`[Deno Init] Listening on http://localhost:${server.addr.port}/`);
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 === "preact") return preact;
20
- if (moduleName === "preact/hooks") return hooks;
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
- create_file "salvia/.gitignore", "/server/\n"
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-all --config #{config_path} #{build_script}"
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-all --config #{config_path} #{build_script} --watch"
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