ruby_everywhere 0.1.9 → 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 +4 -4
- data/LICENSE.txt +21 -0
- data/bridge/LICENSE +21 -0
- data/bridge/README.md +77 -0
- data/bridge/package.json +14 -0
- data/config/importmap.rb +3 -0
- data/lib/everywhere/boot.rb +28 -2
- data/lib/everywhere/commands/install.rb +20 -16
- data/lib/everywhere/engine.rb +30 -0
- data/lib/everywhere/receipt.rb +11 -3
- data/lib/everywhere/version.rb +9 -6
- data/lib/everywhere.rb +3 -0
- data/support/shell/src-tauri/src/main.rs +86 -6
- metadata +8 -2
- /data/{lib/everywhere/javascript → bridge/everywhere}/bridge.js +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8f327ca168d52af14d2005995472056aa70359ed8219d85328603a41918b197
|
|
4
|
+
data.tar.gz: d4039cfb079920aa40385cb04681250b33ce95e63c33d31eb35037ff98ef7278
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @rubyeverywhere/bridge
|
|
2
|
+
|
|
3
|
+
One API for the browser, the RubyEverywhere desktop shell, and (soon) Hotwire
|
|
4
|
+
Native mobile apps — the JavaScript half of [RubyEverywhere](https://rubyeverywhere.com).
|
|
5
|
+
|
|
6
|
+
App code writes to this surface only; platform differences live in adapters
|
|
7
|
+
inside the package. Everything degrades gracefully: the same page works in a
|
|
8
|
+
plain browser tab and gains native powers inside a RubyEverywhere app.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @rubyeverywhere/bridge
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Using RubyEverywhere? You don't need npm at all: the `ruby_everywhere` gem
|
|
17
|
+
ships this exact package. Rails apps get it served and importmap-pinned by the
|
|
18
|
+
gem's engine (updates arrive with `bundle update ruby_everywhere`); Sinatra and
|
|
19
|
+
Hanami apps get it vendored to `public/bridge.js` by `every install`.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import Everywhere from "@rubyeverywhere/bridge"
|
|
25
|
+
|
|
26
|
+
Everywhere.platform // "desktop" | "mobile" | "browser"
|
|
27
|
+
Everywhere.os // "macos" | "windows" | "linux" | "ios" | "android" | "chromeos" | "unknown"
|
|
28
|
+
Everywhere.native // true inside a RubyEverywhere app
|
|
29
|
+
Everywhere.version // app version; null in a plain browser tab
|
|
30
|
+
|
|
31
|
+
Everywhere.notify({ title, body }) // native notification / web Notification / console
|
|
32
|
+
Everywhere.confirm("Sure?") // Promise<boolean>, native dialog when possible
|
|
33
|
+
Everywhere.on("menu", handler) // shell events; returns an unsubscribe fn
|
|
34
|
+
Everywhere.visit("/settings") // Turbo.visit with location fallback
|
|
35
|
+
|
|
36
|
+
Everywhere.clipboard.write(text) // Promise<void>
|
|
37
|
+
Everywhere.clipboard.read() // Promise<string | null>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Auto-updates
|
|
41
|
+
|
|
42
|
+
Inside the desktop shell, apps with an update feed configured can check,
|
|
43
|
+
install, and switch channels at runtime:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
Everywhere.updates.supported // true when the shell has an update feed
|
|
47
|
+
Everywhere.updates.channel // effective channel ("stable", "beta", …)
|
|
48
|
+
|
|
49
|
+
const result = await Everywhere.updates.check()
|
|
50
|
+
// { available: true, version, notes, notesHtml } or { available: false }
|
|
51
|
+
|
|
52
|
+
Everywhere.updates.install() // download / verify / swap / relaunch
|
|
53
|
+
Everywhere.updates.setChannel("beta") // Promise<{ channel }>, persisted by the shell
|
|
54
|
+
|
|
55
|
+
Everywhere.updates.on("available", handler)
|
|
56
|
+
// events: "available" | "none" | "progress" | "ready" | "error" | "channel"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`notes` is the release-notes markdown source; `notesHtml` is the same notes
|
|
60
|
+
pre-rendered to HTML from your signed update feed, ready for a changelog
|
|
61
|
+
modal: `el.innerHTML = notesHtml`.
|
|
62
|
+
|
|
63
|
+
In a plain browser tab the updates API is inert: `check()` resolves
|
|
64
|
+
`{ available: false, unsupported: true }` and nothing throws.
|
|
65
|
+
|
|
66
|
+
## Platforms
|
|
67
|
+
|
|
68
|
+
- **desktop** — the Tauri-based RubyEverywhere shell. Notifications, dialogs,
|
|
69
|
+
clipboard, shell events, and auto-updates are native.
|
|
70
|
+
- **browser** — honest web fallbacks: the Notification API, `window.confirm`,
|
|
71
|
+
`navigator.clipboard`, DOM CustomEvents.
|
|
72
|
+
- **mobile** — Hotwire Native detection is live; behavior currently falls back
|
|
73
|
+
to the browser adapter until the bridge-component adapter lands.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
data/bridge/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rubyeverywhere/bridge",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "One API for browser, desktop, and mobile — the JS half of RubyEverywhere",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "everywhere/bridge.js",
|
|
7
|
+
"exports": { ".": "./everywhere/bridge.js" },
|
|
8
|
+
"files": ["everywhere/bridge.js", "README.md", "LICENSE"],
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"keywords": ["rubyeverywhere", "tauri", "hotwire", "turbo", "rails", "desktop", "native", "bridge"],
|
|
11
|
+
"homepage": "https://rubyeverywhere.com",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"publishConfig": { "access": "public" }
|
|
14
|
+
}
|
data/config/importmap.rb
ADDED
data/lib/everywhere/boot.rb
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
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)
|
|
@@ -66,7 +66,7 @@ module Everywhere
|
|
|
66
66
|
# Rack frameworks vary too much to auto-edit their boot files safely, so we
|
|
67
67
|
# vendor the bridge and print the remaining wiring rather than guess.
|
|
68
68
|
def vendor_bridge_to_public
|
|
69
|
-
source = File.expand_path("
|
|
69
|
+
source = File.expand_path("../../../bridge/everywhere/bridge.js", __dir__)
|
|
70
70
|
target_dir = app_file("public")
|
|
71
71
|
FileUtils.mkdir_p(target_dir)
|
|
72
72
|
target = File.join(target_dir, "bridge.js")
|
|
@@ -219,32 +219,36 @@ module Everywhere
|
|
|
219
219
|
change("disable force_ssl for localhost", made)
|
|
220
220
|
end
|
|
221
221
|
|
|
222
|
-
#
|
|
223
|
-
# and
|
|
224
|
-
#
|
|
222
|
+
# @rubyeverywhere/bridge (one JS API across browser/desktop/mobile) is
|
|
223
|
+
# served and pinned by Everywhere::Engine straight from the gem, so the
|
|
224
|
+
# bridge updates with `bundle update ruby_everywhere`. Nothing to vendor —
|
|
225
|
+
# just migrate apps installed before the engine existed (their vendored
|
|
226
|
+
# copy and pin would shadow the engine's, freezing them on an old bridge)
|
|
227
|
+
# and wire the import.
|
|
225
228
|
def install_bridge
|
|
226
|
-
|
|
227
|
-
target_dir = app_file("vendor", "javascript")
|
|
228
|
-
target = File.join(target_dir, "@rubyeverywhere--bridge.js")
|
|
229
|
-
FileUtils.mkdir_p(target_dir)
|
|
230
|
-
# Stamp a machine-readable version marker on the vendored copy so the
|
|
231
|
-
# build receipt (Receipt#bridge_version) can record which bridge shipped.
|
|
232
|
-
# Comparing against the fully-stamped content keeps re-runs idempotent.
|
|
233
|
-
desired = "// @rubyeverywhere/bridge version: #{Everywhere::BRIDGE_VERSION} (vendored by `every install`)\n#{File.read(source)}"
|
|
234
|
-
fresh = !File.exist?(target) || File.read(target) != desired
|
|
235
|
-
File.write(target, desired) if fresh
|
|
229
|
+
fresh = remove_vendored_bridge
|
|
236
230
|
|
|
237
231
|
importmap = app_file("config", "importmap.rb")
|
|
238
232
|
if File.exist?(importmap)
|
|
239
233
|
contents = File.read(importmap)
|
|
240
|
-
|
|
241
|
-
|
|
234
|
+
stale_pin = /^pin ["']@rubyeverywhere\/bridge["'],\s*to:\s*["']@rubyeverywhere--bridge\.js["'].*\n/
|
|
235
|
+
if contents.match?(stale_pin)
|
|
236
|
+
File.write(importmap, contents.gsub(stale_pin, ""))
|
|
242
237
|
fresh = true
|
|
243
238
|
end
|
|
244
239
|
end
|
|
245
240
|
wire_application_js(fresh)
|
|
246
241
|
end
|
|
247
242
|
|
|
243
|
+
def remove_vendored_bridge
|
|
244
|
+
target = app_file("vendor", "javascript", "@rubyeverywhere--bridge.js")
|
|
245
|
+
return false unless File.exist?(target)
|
|
246
|
+
|
|
247
|
+
FileUtils.rm(target)
|
|
248
|
+
change("remove vendored bridge (now served by the ruby_everywhere engine)", true)
|
|
249
|
+
true
|
|
250
|
+
end
|
|
251
|
+
|
|
248
252
|
def wire_application_js(fresh)
|
|
249
253
|
app_js = app_file("app", "javascript", "application.js")
|
|
250
254
|
if File.exist?(app_js)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# Rails integration: serves @rubyeverywhere/bridge straight from the gem's
|
|
5
|
+
# bridge/ directory (the same files published to npm — single source) and
|
|
6
|
+
# contributes the importmap pin, so apps get bridge updates with
|
|
7
|
+
# `bundle update ruby_everywhere` — no vendored copy to refresh. Loaded only
|
|
8
|
+
# when Rails is present; Sinatra/Hanami apps keep the vendored
|
|
9
|
+
# public/bridge.js from `every install`.
|
|
10
|
+
class Engine < ::Rails::Engine
|
|
11
|
+
# bridge/ becomes an asset root, so bridge/everywhere/bridge.js gets the
|
|
12
|
+
# namespaced logical path "everywhere/bridge.js" (Propshaft and Sprockets
|
|
13
|
+
# both read config.assets.paths); Sprockets additionally needs the file
|
|
14
|
+
# declared precompilable.
|
|
15
|
+
initializer "everywhere.assets" do |app|
|
|
16
|
+
if app.config.respond_to?(:assets)
|
|
17
|
+
app.config.assets.paths << Engine.root.join("bridge").to_s
|
|
18
|
+
app.config.assets.precompile += %w[everywhere/bridge.js] if app.config.assets.respond_to?(:precompile)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Runs before importmap-rails adds the app's config/importmap.rb, so an
|
|
23
|
+
# app-level pin (e.g. a legacy vendored copy) still overrides ours.
|
|
24
|
+
initializer "everywhere.importmap", before: "importmap" do |app|
|
|
25
|
+
if app.config.respond_to?(:importmap)
|
|
26
|
+
app.config.importmap.paths << Engine.root.join("config/importmap.rb")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/everywhere/receipt.rb
CHANGED
|
@@ -116,10 +116,18 @@ module Everywhere
|
|
|
116
116
|
File.join(@root, "vendor", "javascript", "@rubyeverywhere", "bridge", "package.json")
|
|
117
117
|
].each { |p| (v = json_version(p)) and return v }
|
|
118
118
|
|
|
119
|
-
#
|
|
120
|
-
#
|
|
119
|
+
# Legacy single-file vendored install: read the `// version: x.y.z`
|
|
120
|
+
# marker `every install` stamped on the copy.
|
|
121
121
|
flat = File.join(@root, "vendor", "javascript", "@rubyeverywhere--bridge.js")
|
|
122
|
-
|
|
122
|
+
return File.foreach(flat).first(6).join[/version:\s*([\d.]+)/i, 1] if File.exist?(flat)
|
|
123
|
+
|
|
124
|
+
# Engine-served (Rails apps bundling ruby_everywhere): the bridge ships
|
|
125
|
+
# inside the gem, so it carries the gem's BRIDGE_VERSION. Best-effort —
|
|
126
|
+
# assumes the app bundles the same gem release that's running this build.
|
|
127
|
+
lock = File.join(@root, "Gemfile.lock")
|
|
128
|
+
return Everywhere::BRIDGE_VERSION if File.exist?(lock) && File.read(lock).match?(/^\s+ruby_everywhere\s/)
|
|
129
|
+
|
|
130
|
+
nil
|
|
123
131
|
end
|
|
124
132
|
|
|
125
133
|
def shell_version
|
data/lib/everywhere/version.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Everywhere
|
|
4
|
-
VERSION = "0.1.
|
|
4
|
+
VERSION = "0.1.11"
|
|
5
5
|
|
|
6
|
-
# Version of the
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
6
|
+
# Version of the @rubyeverywhere/bridge JS this gem ships. bridge/ in the
|
|
7
|
+
# gem IS the npm package (served to Rails apps by Everywhere::Engine,
|
|
8
|
+
# vendored to public/ for Sinatra/Hanami), so its package.json is the single
|
|
9
|
+
# source of truth. The build receipt records it; it versions independently
|
|
10
|
+
# of the CLI.
|
|
11
|
+
BRIDGE_VERSION = File.read(
|
|
12
|
+
File.expand_path("../../bridge/package.json", __dir__)
|
|
13
|
+
)[/"version":\s*"([^"]+)"/, 1]
|
|
11
14
|
end
|
data/lib/everywhere.rb
CHANGED
|
@@ -7,6 +7,9 @@ require_relative "everywhere/config"
|
|
|
7
7
|
require_relative "everywhere/framework"
|
|
8
8
|
require_relative "everywhere/database"
|
|
9
9
|
require_relative "everywhere/boot"
|
|
10
|
+
# Rails apps get the bridge served + pinned by the engine (Sinatra/Hanami
|
|
11
|
+
# vendor it to public/ instead — see `every install`).
|
|
12
|
+
require_relative "everywhere/engine" if defined?(::Rails::Engine)
|
|
10
13
|
|
|
11
14
|
module Everywhere
|
|
12
15
|
class Error < StandardError; end
|
|
@@ -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
|
-
//
|
|
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::
|
|
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’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('&', "&").replace('<', "<").replace('>', ">")
|
|
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
|
|
293
|
-
eprintln!("sidecar
|
|
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.
|
|
4
|
+
version: 0.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrea Fomera
|
|
@@ -75,6 +75,12 @@ executables:
|
|
|
75
75
|
extensions: []
|
|
76
76
|
extra_rdoc_files: []
|
|
77
77
|
files:
|
|
78
|
+
- LICENSE.txt
|
|
79
|
+
- bridge/LICENSE
|
|
80
|
+
- bridge/README.md
|
|
81
|
+
- bridge/everywhere/bridge.js
|
|
82
|
+
- bridge/package.json
|
|
83
|
+
- config/importmap.rb
|
|
78
84
|
- exe/every
|
|
79
85
|
- exe/rbe
|
|
80
86
|
- lib/everywhere.rb
|
|
@@ -98,10 +104,10 @@ files:
|
|
|
98
104
|
- lib/everywhere/commands/updates_keygen.rb
|
|
99
105
|
- lib/everywhere/config.rb
|
|
100
106
|
- lib/everywhere/database.rb
|
|
107
|
+
- lib/everywhere/engine.rb
|
|
101
108
|
- lib/everywhere/framework.rb
|
|
102
109
|
- lib/everywhere/icon.rb
|
|
103
110
|
- lib/everywhere/ignore.rb
|
|
104
|
-
- lib/everywhere/javascript/bridge.js
|
|
105
111
|
- lib/everywhere/log_filter.rb
|
|
106
112
|
- lib/everywhere/minisign.rb
|
|
107
113
|
- lib/everywhere/paths.rb
|
|
File without changes
|