ruby_everywhere 0.1.10 → 0.1.11

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: 6e02deeb4f95a031a3a44bae0e87dbaece25f95a919ff817927e9eb5b611ffbb
4
- data.tar.gz: db779e7819a864517b0a44a388c56b835eb724a2d697af653857936cab505ac1
3
+ metadata.gz: b8f327ca168d52af14d2005995472056aa70359ed8219d85328603a41918b197
4
+ data.tar.gz: d4039cfb079920aa40385cb04681250b33ce95e63c33d31eb35037ff98ef7278
5
5
  SHA512:
6
- metadata.gz: a1567ed4df49616f1c05fe481812a62bc9f3eb64e816e4702756a0614e282027016a4e76ad62a3097fced1bde908a22fdba8ae6b6b6a46aa24cdc227bb8bf32e
7
- data.tar.gz: ed2ecc0ff275c958291986eb8ea23b0e9fde277b62dcffc614c61dd84f2d6c4c626e8c93d07a8563b4755cb036f56f4a0107e00cc6c46280c6e61c551e259c1e
6
+ metadata.gz: 92285595fa1315556057cf6520105c1f3226609666624ef8ecf0ba844f655a6bb899ca4ff29d39d52777d4f9bac5c29a0b18b6f136fe5563272c4ea1a340b636
7
+ data.tar.gz: 15ce68cd5b9dc8b066732dddd3649dd994b8a74d7abca5e2fa57df269c16f6fc995fbee36b3cd8c545eea16453d34f908d0185d5dba255a3f87ae7555af824cb
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Andrea Fomera
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
13
+ all 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
21
+ THE SOFTWARE.
data/bridge/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Andrea Fomera
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
13
+ all 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
21
+ THE SOFTWARE.
data/bridge/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "main": "everywhere/bridge.js",
7
7
  "exports": { ".": "./everywhere/bridge.js" },
8
- "files": ["everywhere/bridge.js", "README.md"],
8
+ "files": ["everywhere/bridge.js", "README.md", "LICENSE"],
9
9
  "sideEffects": false,
10
10
  "keywords": ["rubyeverywhere", "tauri", "hotwire", "turbo", "rails", "desktop", "native", "bridge"],
11
11
  "homepage": "https://rubyeverywhere.com",
@@ -83,6 +83,10 @@ module Everywhere
83
83
  # (Rack::Files sends an empty body), so static assets are served from a
84
84
  # real-disk copy in app-data instead. The everywhere initializer points
85
85
  # Rails.public_path here via NATIVE_PUBLIC_DIR.
86
+ #
87
+ # The copy is boot-time-visible work (rm_rf + cp_r of every asset), so it
88
+ # only happens when the packaged public/ actually changed since the last
89
+ # boot — a stamp of the source file list survives in app-data.
86
90
  def extract_public_dir
87
91
  return unless ENV["NATIVE_PACKAGED"] == "1"
88
92
 
@@ -90,11 +94,33 @@ module Everywhere
90
94
  return unless File.directory?(source)
91
95
 
92
96
  target = File.join(app_data, "public")
93
- FileUtils.rm_rf(target)
94
- FileUtils.cp_r(source, target)
97
+ stamp_file = File.join(app_data, "public.stamp")
98
+ stamp = public_stamp(source)
99
+
100
+ unless File.directory?(target) && File.exist?(stamp_file) && File.read(stamp_file) == stamp
101
+ FileUtils.rm_rf(target)
102
+ FileUtils.cp_r(source, target)
103
+ File.write(stamp_file, stamp)
104
+ end
95
105
  ENV["NATIVE_PUBLIC_DIR"] = target
96
106
  end
97
107
 
108
+ # Path + size + mtime for every file under public/. Fingerprinted assets
109
+ # change path, everything else changes size or mtime (memfs mtimes are
110
+ # fixed at build time, so any rebuild moves them). Metadata only — cheap
111
+ # enough to run every boot.
112
+ def public_stamp(source)
113
+ require "digest"
114
+ entries = Dir.glob("**/*", File::FNM_DOTMATCH, base: source).sort.filter_map do |rel|
115
+ path = File.join(source, rel)
116
+ next unless File.file?(path)
117
+
118
+ stat = File.stat(path)
119
+ "#{rel}:#{stat.size}:#{stat.mtime.to_i}"
120
+ end
121
+ Digest::SHA256.hexdigest(entries.join("\n"))
122
+ end
123
+
98
124
  def ensure_secret_key_base
99
125
  secret_file = File.join(app_data, "secret_key_base")
100
126
  unless File.exist?(secret_file)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Everywhere
4
- VERSION = "0.1.10"
4
+ VERSION = "0.1.11"
5
5
 
6
6
  # Version of the @rubyeverywhere/bridge JS this gem ships. bridge/ in the
7
7
  # gem IS the npm package (served to Rails apps by Everywhere::Engine,
@@ -3,8 +3,11 @@
3
3
  // Boot sequence (packaged / repo-dev):
4
4
  // 1. load app config (NATIVE_CONFIG env JSON, or Resources/everywhere.json in a .app)
5
5
  // 2. pick a free localhost port
6
- // 3. spawn the tebako-packaged Rails binary with NATIVE_PORT / NATIVE_APPDATA
7
- // 4. show the splash page while polling until the server accepts connections
6
+ // 3. spawn the tebako-packaged Rails binary with NATIVE_PORT / NATIVE_APPDATA,
7
+ // its output captured to <app_data>/log/server.log
8
+ // 4. show the splash page while polling until the server accepts connections —
9
+ // for as long as the sidecar is alive; if it dies first, show an error
10
+ // page with the log tail instead
8
11
  // 5. navigate the webview to http://127.0.0.1:{port}{entry_path}
9
12
  // 6. kill the sidecar when the app exits
10
13
  //
@@ -13,7 +16,8 @@
13
16
  #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
14
17
 
15
18
  use std::net::{TcpListener, TcpStream};
16
- use std::process::{Child, Command};
19
+ use std::path::{Path, PathBuf};
20
+ use std::process::{Child, Command, ExitStatus};
17
21
  use std::sync::Mutex;
18
22
  use std::time::{Duration, Instant};
19
23
 
@@ -126,6 +130,60 @@ fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
126
130
  }
127
131
  }
128
132
 
133
+ fn sidecar_log_path(app_data: &Path) -> PathBuf {
134
+ let dir = app_data.join("log");
135
+ let _ = std::fs::create_dir_all(&dir);
136
+ dir.join("server.log")
137
+ }
138
+
139
+ // Non-blocking liveness check for the sidecar. The child stays managed so
140
+ // exit cleanup can still reap the process group.
141
+ fn sidecar_exit_status(app: &tauri::AppHandle) -> Option<ExitStatus> {
142
+ let sidecar = app.try_state::<Sidecar>()?;
143
+ let mut guard = sidecar.0.lock().unwrap();
144
+ guard.as_mut()?.try_wait().ok().flatten()
145
+ }
146
+
147
+ // Error page for a sidecar that died before opening its port. A data: URL —
148
+ // there is no server to serve anything — carrying the log tail so "it never
149
+ // loaded" comes with something to paste into a bug report.
150
+ fn boot_failure_url(log_path: &Path, status: ExitStatus) -> tauri::Url {
151
+ let tail = log_tail(log_path, 4096);
152
+ let tail = if tail.trim().is_empty() { "(no output captured)".into() } else { tail };
153
+ let html = format!(
154
+ r#"<!doctype html><html><head><meta charset="utf-8"><title>App failed to start</title><style>
155
+ :root {{ color-scheme: light dark; }}
156
+ body {{ margin: 0; min-height: 100vh; display: grid; place-items: center;
157
+ font-family: -apple-system, system-ui, sans-serif;
158
+ background: light-dark(#faf9f7, #1c1b1a); color: light-dark(#333, #ddd); }}
159
+ main {{ max-width: 640px; padding: 32px; text-align: center; }}
160
+ h1 {{ font-size: 17px; }} p {{ font-size: 13px; opacity: 0.7; }}
161
+ pre {{ font-size: 11px; text-align: left; background: light-dark(#eee, #111);
162
+ padding: 12px; border-radius: 8px; overflow: auto; max-height: 45vh; white-space: pre-wrap; }}
163
+ </style></head><body><main>
164
+ <div style="font-size:40px">💎</div>
165
+ <h1>The app&#8217;s server failed to start</h1>
166
+ <p>It exited ({status}) before it was ready. Recent server output:</p>
167
+ <pre>{log}</pre>
168
+ <p>Full log: {path}</p>
169
+ </main></body></html>"#,
170
+ status = html_escape(&status.to_string()),
171
+ log = html_escape(&tail),
172
+ path = html_escape(&log_path.display().to_string()),
173
+ );
174
+ format!("data:text/html;base64,{}", base64(html.as_bytes())).parse().unwrap()
175
+ }
176
+
177
+ fn log_tail(path: &Path, max_bytes: usize) -> String {
178
+ let bytes = std::fs::read(path).unwrap_or_default();
179
+ let start = bytes.len().saturating_sub(max_bytes);
180
+ String::from_utf8_lossy(&bytes[start..]).into_owned()
181
+ }
182
+
183
+ fn html_escape(s: &str) -> String {
184
+ s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
185
+ }
186
+
129
187
  fn free_port() -> u16 {
130
188
  TcpListener::bind("127.0.0.1:0")
131
189
  .expect("could not bind to a local port")
@@ -256,6 +314,18 @@ fn main() {
256
314
  // The sidecar watches this pid and shuts itself down if the
257
315
  // shell dies without running cleanup (crash, SIGKILL, pkill).
258
316
  .env("NATIVE_SHELL_PID", std::process::id().to_string());
317
+ // A Finder-launched .app has no terminal — this file is the only
318
+ // evidence when the server fails to boot. Truncated each launch so
319
+ // it always shows the current boot.
320
+ let log_path = sidecar_log_path(&app_data);
321
+ match std::fs::File::create(&log_path) {
322
+ Ok(f) => {
323
+ if let Ok(out) = f.try_clone() {
324
+ sidecar_cmd.stdout(out).stderr(f);
325
+ }
326
+ }
327
+ Err(e) => eprintln!("could not create {}: {e}", log_path.display()),
328
+ }
259
329
  // Own process group so exit cleanup can reap the WHOLE tree —
260
330
  // Solid Queue forks workers that would otherwise outlive Puma.
261
331
  #[cfg(unix)]
@@ -276,7 +346,13 @@ fn main() {
276
346
  )?;
277
347
 
278
348
  // Wait for the server, then swap the splash for the real app.
349
+ // No fixed deadline: first launches are legitimately slow
350
+ // (Gatekeeper scanning the binary, first db:prepare), and as long
351
+ // as the sidecar is alive the port can still open. The exit
352
+ // condition is the sidecar dying — that's surfaced as an error
353
+ // page instead of an eternal splash.
279
354
  let entry_path = config.entry_path.clone();
355
+ let handle = app.handle().clone();
280
356
  std::thread::spawn(move || {
281
357
  let addr = format!("127.0.0.1:{port}");
282
358
  let started = Instant::now();
@@ -289,8 +365,9 @@ fn main() {
289
365
  {
290
366
  break;
291
367
  }
292
- if started.elapsed() > Duration::from_secs(60) {
293
- eprintln!("sidecar never came up on {addr}");
368
+ if let Some(status) = sidecar_exit_status(&handle) {
369
+ eprintln!("sidecar exited before opening {addr}: {status}");
370
+ let _ = window.navigate(boot_failure_url(&log_path, status));
294
371
  return;
295
372
  }
296
373
  std::thread::sleep(Duration::from_millis(150));
@@ -503,8 +580,11 @@ const INIT_SCRIPT: &str = r#"
503
580
  window.__EVERYWHERE_CONFIG__ = /*__CONFIG_JSON__*/ null || {};
504
581
  const cfg = window.__EVERYWHERE_CONFIG__;
505
582
 
583
+ // .catch matters: on data: pages (splash, boot-failure) the relative URL
584
+ // can't resolve, and an unhandled rejection here would re-trigger report()
585
+ // from the unhandledrejection listener forever.
506
586
  const report = (kind, msg) => {
507
- try { fetch('/?' + kind + '=' + encodeURIComponent(msg)) } catch (_) {}
587
+ try { fetch('/?' + kind + '=' + encodeURIComponent(msg)).catch(() => {}) } catch (_) {}
508
588
  };
509
589
  window.addEventListener('error', e =>
510
590
  report('jserror', e.message + ' @ ' + (e.filename || '?') + ':' + (e.lineno || '?')));
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_everywhere
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrea Fomera
@@ -75,6 +75,8 @@ executables:
75
75
  extensions: []
76
76
  extra_rdoc_files: []
77
77
  files:
78
+ - LICENSE.txt
79
+ - bridge/LICENSE
78
80
  - bridge/README.md
79
81
  - bridge/everywhere/bridge.js
80
82
  - bridge/package.json