ruby_everywhere 0.6.0 → 0.7.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 +4 -4
- data/exe/every +12 -1
- data/exe/rbe +12 -1
- data/lib/everywhere/agents_guide.rb +3 -1
- data/lib/everywhere/blake2b.rb +17 -1
- data/lib/everywhere/boot.rb +21 -4
- data/lib/everywhere/builders/android.rb +144 -30
- data/lib/everywhere/builders/desktop.rb +25 -17
- data/lib/everywhere/builders/ios.rb +244 -9
- data/lib/everywhere/cli.rb +2 -0
- data/lib/everywhere/commands/build.rb +113 -58
- data/lib/everywhere/commands/clean.rb +8 -3
- data/lib/everywhere/commands/dev.rb +85 -11
- data/lib/everywhere/commands/doctor.rb +138 -21
- data/lib/everywhere/commands/install.rb +48 -8
- data/lib/everywhere/commands/platform/build.rb +136 -27
- data/lib/everywhere/commands/platform/login.rb +16 -2
- data/lib/everywhere/commands/platform/runner.rb +113 -23
- data/lib/everywhere/commands/preview.rb +359 -0
- data/lib/everywhere/commands/publish.rb +20 -2
- data/lib/everywhere/commands/release.rb +145 -28
- data/lib/everywhere/commands/shell_dir.rb +2 -2
- data/lib/everywhere/config.rb +88 -3
- data/lib/everywhere/console.rb +2 -1
- data/lib/everywhere/engine.rb +24 -0
- data/lib/everywhere/framework.rb +21 -4
- data/lib/everywhere/ignore.rb +3 -1
- data/lib/everywhere/jump.rb +121 -0
- data/lib/everywhere/mobile_config_endpoint.rb +47 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/paths.rb +13 -6
- data/lib/everywhere/platform/client.rb +27 -6
- data/lib/everywhere/platform/credentials.rb +18 -8
- data/lib/everywhere/platform/snapshot.rb +10 -0
- data/lib/everywhere/receipt.rb +55 -10
- data/lib/everywhere/shellout.rb +19 -0
- data/lib/everywhere/simulator.rb +12 -1
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/android/app/build.gradle.kts +29 -1
- data/support/mobile/ios/App/EverywhereConfig.swift +17 -5
- data/support/mobile/ios/App/SceneDelegate.swift +75 -8
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +2 -2
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
- data/support/mobile/ios/README.md +13 -6
- metadata +17 -1
|
@@ -4,6 +4,7 @@ require "rubygems/package"
|
|
|
4
4
|
require "zlib"
|
|
5
5
|
require "digest"
|
|
6
6
|
require_relative "../ignore"
|
|
7
|
+
require_relative "../ui"
|
|
7
8
|
|
|
8
9
|
module Everywhere
|
|
9
10
|
module Platform
|
|
@@ -38,10 +39,19 @@ module Everywhere
|
|
|
38
39
|
|
|
39
40
|
def add(tar, root, rel)
|
|
40
41
|
abs = File.join(root, rel)
|
|
42
|
+
# lstat first: File.stat follows the link, so a dangling symlink (a
|
|
43
|
+
# deleted bundle path, a checked-in link to an absent sibling repo)
|
|
44
|
+
# would raise Errno::ENOENT and abort the whole snapshot.
|
|
45
|
+
return Everywhere::UI.warn("skipping dangling symlink #{rel}") if File.lstat(abs).symlink? && !File.exist?(abs)
|
|
46
|
+
|
|
41
47
|
stat = File.stat(abs)
|
|
42
48
|
tar.add_file_simple(rel, stat.mode & 0o777, stat.size) do |io|
|
|
43
49
|
File.open(abs, "rb") { |f| IO.copy_stream(f, io) }
|
|
44
50
|
end
|
|
51
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
52
|
+
# The listing is a snapshot in time; a dev server or watcher can delete a
|
|
53
|
+
# tmp/ file between the glob and here. Losing it beats losing the build.
|
|
54
|
+
Everywhere::UI.warn("skipping #{rel} (#{e.class.name.split("::").last})")
|
|
45
55
|
end
|
|
46
56
|
end
|
|
47
57
|
end
|
data/lib/everywhere/receipt.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "digest"
|
|
|
5
5
|
require_relative "version"
|
|
6
6
|
require_relative "shellout"
|
|
7
7
|
require_relative "ignore"
|
|
8
|
+
require_relative "paths"
|
|
8
9
|
|
|
9
10
|
module Everywhere
|
|
10
11
|
# Builds the machine-readable build receipt (release.json) — the same shape
|
|
@@ -22,14 +23,20 @@ module Everywhere
|
|
|
22
23
|
class Receipt
|
|
23
24
|
SCHEMA = 1
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
# source_checksum/lockfile_checksum are optional so a caller building one
|
|
27
|
+
# receipt per target hashes the tree ONCE and threads the digest through the
|
|
28
|
+
# rest — they describe the source, which is identical for every target.
|
|
29
|
+
def initialize(root:, config:, framework:, ruby:, target:, channel:, shell_dir: nil,
|
|
30
|
+
source_checksum: nil, lockfile_checksum: nil)
|
|
26
31
|
@root = File.expand_path(root)
|
|
27
32
|
@config = config
|
|
28
33
|
@framework = framework
|
|
29
34
|
@ruby = ruby
|
|
30
|
-
@target_str = target #
|
|
35
|
+
@target_str = target # bare "os-arch" — selects platforms: overrides
|
|
31
36
|
@target = parse_target(target, channel)
|
|
32
37
|
@shell_dir = shell_dir
|
|
38
|
+
@source_checksum = source_checksum
|
|
39
|
+
@lockfile_checksum = lockfile_checksum
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
# The deterministic build request.
|
|
@@ -48,7 +55,9 @@ module Everywhere
|
|
|
48
55
|
"lockfile_checksum" => lockfile_checksum
|
|
49
56
|
},
|
|
50
57
|
"target" => @target,
|
|
51
|
-
"permissions" => @config.permissions
|
|
58
|
+
"permissions" => @config.permissions,
|
|
59
|
+
"mode" => @config.mode,
|
|
60
|
+
"remote" => { "url" => @config.remote_url }.compact
|
|
52
61
|
}
|
|
53
62
|
end
|
|
54
63
|
|
|
@@ -105,10 +114,15 @@ module Everywhere
|
|
|
105
114
|
"cli" => Everywhere::VERSION,
|
|
106
115
|
"bridge" => bridge_version,
|
|
107
116
|
"shell" => shell_version,
|
|
108
|
-
|
|
117
|
+
# Only macOS targets are tebako-pressed; spawning rbe-tebako for a
|
|
118
|
+
# mobile receipt would cost a process to learn nothing.
|
|
119
|
+
"tebako" => macos? ? tebako_version : nil,
|
|
120
|
+
"hotwire_native" => hotwire_native_version
|
|
109
121
|
}
|
|
110
122
|
end
|
|
111
123
|
|
|
124
|
+
def macos? = @target["os"] == "macos"
|
|
125
|
+
|
|
112
126
|
def bridge_version
|
|
113
127
|
# Package installs: read the version straight from package.json.
|
|
114
128
|
[
|
|
@@ -145,11 +159,40 @@ module Everywhere
|
|
|
145
159
|
out[/version\s+([\d.]+)/i, 1]
|
|
146
160
|
end
|
|
147
161
|
|
|
162
|
+
# Which Hotwire Native the bundled mobile template pins. Read straight off
|
|
163
|
+
# the frozen templates in the gem (no spawn): iOS from the SPM lockfile,
|
|
164
|
+
# Android from the Gradle dependency line. Best-effort — a receipt is worth
|
|
165
|
+
# writing even when the template is missing or reshaped.
|
|
166
|
+
def hotwire_native_version
|
|
167
|
+
case @target["os"]
|
|
168
|
+
when "ios" then hotwire_native_ios_revision
|
|
169
|
+
when "android" then hotwire_native_android_version
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def hotwire_native_ios_revision
|
|
174
|
+
dir = Paths.ios_dir or return nil
|
|
175
|
+
resolved = File.join(dir, "App.xcodeproj", "project.xcworkspace", "xcshareddata", "swiftpm", "Package.resolved")
|
|
176
|
+
pin = JSON.parse(File.read(resolved))["pins"].find { |p| p["identity"].to_s.include?("hotwire-native-ios") }
|
|
177
|
+
pin&.dig("state", "revision")&.slice(0, 12)
|
|
178
|
+
rescue StandardError
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def hotwire_native_android_version
|
|
183
|
+
dir = Paths.android_dir or return nil
|
|
184
|
+
File.read(File.join(dir, "app", "build.gradle.kts"))[/dev\.hotwire:core:([\d.]+)/, 1]
|
|
185
|
+
rescue StandardError
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
|
|
148
189
|
# ---- checksums -----------------------------------------------------------
|
|
149
190
|
|
|
150
191
|
def lockfile_checksum
|
|
151
|
-
|
|
152
|
-
|
|
192
|
+
@lockfile_checksum ||= begin
|
|
193
|
+
lock = File.join(@root, "Gemfile.lock")
|
|
194
|
+
File.exist?(lock) ? "sha256:#{Digest::SHA256.file(lock).hexdigest}" : nil
|
|
195
|
+
end
|
|
153
196
|
end
|
|
154
197
|
|
|
155
198
|
def lockfile_gem_version(gem_name)
|
|
@@ -164,11 +207,13 @@ module Everywhere
|
|
|
164
207
|
# Uses the SAME ignore rules as the (future) snapshot, so the checksum
|
|
165
208
|
# describes exactly what ships.
|
|
166
209
|
def source_checksum
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
@source_checksum ||= begin
|
|
211
|
+
digest = Digest::SHA256.new
|
|
212
|
+
Ignore.for(@root).files(@root).each do |rel|
|
|
213
|
+
digest << rel << "\0" << Digest::SHA256.file(File.join(@root, rel)).hexdigest << "\n"
|
|
214
|
+
end
|
|
215
|
+
"sha256:#{digest.hexdigest}"
|
|
170
216
|
end
|
|
171
|
-
"sha256:#{digest.hexdigest}"
|
|
172
217
|
end
|
|
173
218
|
|
|
174
219
|
# ---- helpers -------------------------------------------------------------
|
data/lib/everywhere/shellout.rb
CHANGED
|
@@ -40,9 +40,27 @@ module Everywhere
|
|
|
40
40
|
|
|
41
41
|
def run!(env, *cmd, chdir: Dir.pwd)
|
|
42
42
|
success = system(child_env(env), *cmd, chdir: chdir)
|
|
43
|
+
# nil means the command never ran (missing binary), false that it failed.
|
|
44
|
+
UI.die!("#{cmd.first} not found — is it installed and on your PATH?") if success.nil?
|
|
43
45
|
UI.die!("command failed: #{cmd.join(" ")}") unless success
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
# Whether `name` resolves as an executable on the PATH children get (the
|
|
49
|
+
# unbundled one — a Bundler binstub visible to the CLI doesn't count).
|
|
50
|
+
def tool?(name)
|
|
51
|
+
return File.executable?(name) && !File.directory?(name) if name.include?("/")
|
|
52
|
+
|
|
53
|
+
path = (unbundled_base["PATH"] || ENV["PATH"]).to_s
|
|
54
|
+
exts = Gem.win_platform? ? [".exe", ".bat", ".cmd", ""] : [""]
|
|
55
|
+
path.split(File::PATH_SEPARATOR).any? do |dir|
|
|
56
|
+
exts.any? { |ext| File.executable?(File.join(dir, name + ext)) && !File.directory?(File.join(dir, name + ext)) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ensure_tool!(name, hint)
|
|
61
|
+
tool?(name) or UI.die!("#{name} not found — #{hint}")
|
|
62
|
+
end
|
|
63
|
+
|
|
46
64
|
# quiet also detaches stdin. A quiet command is by definition one that has
|
|
47
65
|
# nothing to say to the terminal, and `every dev` reads single keystrokes
|
|
48
66
|
# from that same terminal — a child left holding stdin silently eats the
|
|
@@ -78,6 +96,7 @@ module Everywhere
|
|
|
78
96
|
# ChildProcesses instead.
|
|
79
97
|
def run_logged!(env, cmd, log:, filter: nil)
|
|
80
98
|
require "open3"
|
|
99
|
+
ensure_tool!(cmd.first, "is it installed and on your PATH?")
|
|
81
100
|
success = false
|
|
82
101
|
File.open(log, "w") do |logf|
|
|
83
102
|
# Inside an `every dev` worker the build leads its own process group,
|
data/lib/everywhere/simulator.rb
CHANGED
|
@@ -56,11 +56,22 @@ module Everywhere
|
|
|
56
56
|
out, status = Shellout.capture("xcrun", "simctl", "list", "devices", "available", "-j")
|
|
57
57
|
return [] unless status&.success?
|
|
58
58
|
|
|
59
|
-
JSON.parse(out).fetch("devices", {})
|
|
59
|
+
JSON.parse(out).fetch("devices", {})
|
|
60
|
+
.sort_by { |runtime, _list| runtime_order(runtime) }
|
|
61
|
+
.flat_map { |_runtime, list| list }
|
|
60
62
|
rescue JSON::ParserError
|
|
61
63
|
[]
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# Runtimes are keyed by identifier ("com.apple.CoreSimulator.SimRuntime.iOS-17-0"),
|
|
67
|
+
# so a plain string sort puts iOS-9-3 AFTER iOS-17-0 and "newest last" hands
|
|
68
|
+
# back the oldest runtime installed. Compare the version numerically, with
|
|
69
|
+
# the family (iOS/watchOS/…) as the primary key so the grouping is stable.
|
|
70
|
+
def runtime_order(runtime)
|
|
71
|
+
family, *version = runtime.to_s.split(".").last.to_s.split("-")
|
|
72
|
+
[family.to_s.downcase, version.map(&:to_i)]
|
|
73
|
+
end
|
|
74
|
+
|
|
64
75
|
def wait_until_booted(udid, timeout: 60)
|
|
65
76
|
deadline = Time.now + timeout
|
|
66
77
|
until Time.now > deadline
|
data/lib/everywhere/version.rb
CHANGED
|
@@ -18,6 +18,15 @@ val everywhere = Properties().apply {
|
|
|
18
18
|
fun stamped(key: String, default: String): String =
|
|
19
19
|
everywhere.getProperty("everywhere.$key")?.takeIf { it.isNotBlank() } ?: default
|
|
20
20
|
|
|
21
|
+
// Release signing and the versionCode override arrive as environment, never as
|
|
22
|
+
// stamped properties: everywhere.properties lives in the work dir for the life
|
|
23
|
+
// of the app, and a keystore password written there outlives the build that
|
|
24
|
+
// needed it. `every build` resolves and passes these; a direct Gradle
|
|
25
|
+
// invocation can export them itself.
|
|
26
|
+
fun env(name: String): String? = System.getenv("EVERY_ANDROID_$name")?.takeIf { it.isNotBlank() }
|
|
27
|
+
|
|
28
|
+
val releaseKeystore = env("KEYSTORE")?.let { file(it) }
|
|
29
|
+
|
|
21
30
|
android {
|
|
22
31
|
// The namespace is the R class / BuildConfig package and is frozen. It is
|
|
23
32
|
// deliberately NOT the applicationId: apps rename their bundle id freely,
|
|
@@ -28,7 +37,7 @@ android {
|
|
|
28
37
|
defaultConfig {
|
|
29
38
|
applicationId = stamped("applicationId", "com.rubyeverywhere.app")
|
|
30
39
|
versionName = stamped("versionName", "0.1.0")
|
|
31
|
-
versionCode = stamped("versionCode", "1").toInt()
|
|
40
|
+
versionCode = (env("VERSION_CODE") ?: stamped("versionCode", "1")).toInt()
|
|
32
41
|
|
|
33
42
|
// minSdk 28 is Hotwire Native Android's floor and is not negotiable.
|
|
34
43
|
// It also clears API 26 for Typeface.Builder.setFontVariationSettings,
|
|
@@ -63,11 +72,30 @@ android {
|
|
|
63
72
|
getByName("main") { res.srcDir("src/stamped/res") }
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
signingConfigs {
|
|
76
|
+
// Created only when a keystore is in the environment: the default build
|
|
77
|
+
// is an unsigned release, and a signingConfig with a null storeFile is
|
|
78
|
+
// a configuration-time failure rather than an unsigned APK.
|
|
79
|
+
releaseKeystore?.let { keystore ->
|
|
80
|
+
create("release") {
|
|
81
|
+
storeFile = keystore
|
|
82
|
+
storePassword = env("KEYSTORE_PASSWORD")
|
|
83
|
+
keyAlias = env("KEY_ALIAS")
|
|
84
|
+
// keytool -genkeypair without -keypass gives the key the store's
|
|
85
|
+
// password; the CLI resolves the same fallback before it gets here.
|
|
86
|
+
keyPassword = env("KEY_PASSWORD") ?: env("KEYSTORE_PASSWORD")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
66
91
|
buildTypes {
|
|
67
92
|
getByName("debug") {
|
|
68
93
|
isDebuggable = true
|
|
69
94
|
}
|
|
70
95
|
release {
|
|
96
|
+
if (releaseKeystore != null) {
|
|
97
|
+
signingConfig = signingConfigs.getByName("release")
|
|
98
|
+
}
|
|
71
99
|
isMinifyEnabled = false
|
|
72
100
|
proguardFiles(
|
|
73
101
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
@@ -145,20 +145,31 @@ struct EverywhereConfig: Decodable {
|
|
|
145
145
|
/// relaunches; release builds never read it.
|
|
146
146
|
private static let devOverrideURL: URL? = {
|
|
147
147
|
let key = "EverywhereDevURL"
|
|
148
|
-
if let dev = ProcessInfo.processInfo.environment["EVERYWHERE_DEV_URL"], let url =
|
|
148
|
+
if let dev = ProcessInfo.processInfo.environment["EVERYWHERE_DEV_URL"], let url = rootWorthy(dev) {
|
|
149
149
|
#if DEBUG
|
|
150
|
-
UserDefaults.standard.set(
|
|
150
|
+
UserDefaults.standard.set(url.absoluteString, forKey: key)
|
|
151
151
|
#endif
|
|
152
152
|
return url
|
|
153
153
|
}
|
|
154
154
|
#if DEBUG
|
|
155
|
-
if let saved = UserDefaults.standard.string(forKey: key), let url =
|
|
155
|
+
if let saved = UserDefaults.standard.string(forKey: key), let url = rootWorthy(saved) {
|
|
156
156
|
return url
|
|
157
157
|
}
|
|
158
158
|
#endif
|
|
159
159
|
return nil
|
|
160
160
|
}()
|
|
161
161
|
|
|
162
|
+
/// A string as an app ROOT: everything derived from a root (path config,
|
|
163
|
+
/// tab URLs) is built by appending paths, so a root carrying a query or
|
|
164
|
+
/// fragment would smear them onto every derived URL. Paths are kept —
|
|
165
|
+
/// dev URLs legitimately carry entry_path.
|
|
166
|
+
private static func rootWorthy(_ string: String) -> URL? {
|
|
167
|
+
guard var components = URLComponents(string: string) else { return nil }
|
|
168
|
+
components.query = nil
|
|
169
|
+
components.fragment = nil
|
|
170
|
+
return components.url
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
// MARK: Instance override (multi-instance apps)
|
|
163
174
|
|
|
164
175
|
/// UserDefaults key holding the user-chosen instance root. Multi-instance
|
|
@@ -186,9 +197,10 @@ struct EverywhereConfig: Decodable {
|
|
|
186
197
|
return true
|
|
187
198
|
}
|
|
188
199
|
guard let scheme = url.scheme?.lowercased(), ["https", "http"].contains(scheme),
|
|
189
|
-
url.host != nil
|
|
200
|
+
url.host != nil,
|
|
201
|
+
let root = rootWorthy(url.absoluteString)
|
|
190
202
|
else { return false }
|
|
191
|
-
UserDefaults.standard.set(
|
|
203
|
+
UserDefaults.standard.set(root.absoluteString, forKey: instanceKey)
|
|
192
204
|
return true
|
|
193
205
|
}
|
|
194
206
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AuthenticationServices
|
|
2
2
|
import HotwireNative
|
|
3
|
+
import SwiftUI
|
|
3
4
|
import UIKit
|
|
4
5
|
import WebKit
|
|
5
6
|
|
|
@@ -369,7 +370,15 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
|
369
370
|
nav.start()
|
|
370
371
|
}
|
|
371
372
|
} else {
|
|
372
|
-
let controller =
|
|
373
|
+
let controller = EverywhereTabBarController(navigatorDelegate: self, lazyLoadTabs: config.lazyLoadsTabs)
|
|
374
|
+
if config.remoteInstances == true {
|
|
375
|
+
controller.leaveTabIndex = entries.firstIndex { $0.path == EverywhereTabBarController.jumpLeavePath }
|
|
376
|
+
controller.onLeaveSelected = { [weak self] in
|
|
377
|
+
guard let self else { return }
|
|
378
|
+
EverywhereConfig.setInstanceURL(nil)
|
|
379
|
+
self.resetApp(to: self.config.startURL)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
373
382
|
tabBarController = controller
|
|
374
383
|
navigator = nil
|
|
375
384
|
window?.rootViewController = controller
|
|
@@ -543,13 +552,23 @@ extension SceneDelegate: NavigatorDelegate {
|
|
|
543
552
|
// App-provided native screens: a path rule's `view_controller`
|
|
544
553
|
// identifier resolves through the extension registry (everywhere.yml
|
|
545
554
|
// native.ios.screens). Unknown identifiers fall through to the web.
|
|
546
|
-
if let identifier = proposal.properties["view_controller"] as? String
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
555
|
+
if let identifier = proposal.properties["view_controller"] as? String {
|
|
556
|
+
if let screen = EverywhereExtensions.screen(for: identifier, url: proposal.url) {
|
|
557
|
+
// A native screen IS first content: without this, an app whose
|
|
558
|
+
// entry path is native never finishes a request and the splash
|
|
559
|
+
// sits until its safety timeout.
|
|
560
|
+
dismissSplash()
|
|
561
|
+
return .acceptCustom(screen)
|
|
562
|
+
}
|
|
563
|
+
// Previewing someone else's app (a picked instance): its rules
|
|
564
|
+
// name native screens compiled into ITS build, not this shell.
|
|
565
|
+
// Say so, instead of falling through to a web 404. Outside a
|
|
566
|
+
// preview an unknown id keeps falling through — there it's a
|
|
567
|
+
// typo, and the web response is the honest answer.
|
|
568
|
+
if config.remoteInstances == true, EverywhereConfig.instanceURL != nil {
|
|
569
|
+
dismissSplash()
|
|
570
|
+
return .acceptCustom(everywhereHost(MissingExtensionScreen(identifier: identifier)))
|
|
571
|
+
}
|
|
553
572
|
}
|
|
554
573
|
return .accept
|
|
555
574
|
}
|
|
@@ -585,3 +604,51 @@ extension SceneDelegate: NavigatorDelegate {
|
|
|
585
604
|
activeNavigator.activeNavigationController.present(errorViewController, animated: true)
|
|
586
605
|
}
|
|
587
606
|
}
|
|
607
|
+
|
|
608
|
+
/// Tab bar that treats the Jump "leave preview" tab as a native action.
|
|
609
|
+
/// The page behind that tab is a gem-served browser fallback with no Turbo,
|
|
610
|
+
/// so letting it become a web visit can only end at Hotwire's "Turbo is not
|
|
611
|
+
/// ready" screen — selecting it must clear the picked instance directly,
|
|
612
|
+
/// and the tap never reaches the web layer.
|
|
613
|
+
final class EverywhereTabBarController: HotwireTabBarController {
|
|
614
|
+
static let jumpLeavePath = "/everywhere/jump/leave"
|
|
615
|
+
|
|
616
|
+
var leaveTabIndex: Int?
|
|
617
|
+
var onLeaveSelected: (() -> Void)?
|
|
618
|
+
|
|
619
|
+
func tabBarController(_ tabBarController: UITabBarController,
|
|
620
|
+
shouldSelect viewController: UIViewController) -> Bool {
|
|
621
|
+
guard let leaveTabIndex,
|
|
622
|
+
tabBarController.viewControllers?.firstIndex(of: viewController) == leaveTabIndex
|
|
623
|
+
else { return true }
|
|
624
|
+
|
|
625
|
+
onLeaveSelected?()
|
|
626
|
+
return false
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/// Shown while PREVIEWING an app whose path rules route here natively: the
|
|
631
|
+
/// screen exists as Swift in that app's native/ios, compiled into its real
|
|
632
|
+
/// build — this shell only carries its own extensions. Honest placeholder
|
|
633
|
+
/// beats a mystery web 404.
|
|
634
|
+
struct MissingExtensionScreen: View {
|
|
635
|
+
let identifier: String
|
|
636
|
+
|
|
637
|
+
var body: some View {
|
|
638
|
+
VStack(spacing: 14) {
|
|
639
|
+
Image(systemName: "puzzlepiece.extension")
|
|
640
|
+
.font(.system(size: 44))
|
|
641
|
+
.foregroundStyle(.secondary)
|
|
642
|
+
Text("This screen is native")
|
|
643
|
+
.font(.title3.bold())
|
|
644
|
+
Text("In the full build, “\(identifier)” is a native screen compiled " +
|
|
645
|
+
"into the app. Jump can't run another app's native code — " +
|
|
646
|
+
"everything else in this preview works.")
|
|
647
|
+
.font(.callout)
|
|
648
|
+
.foregroundStyle(.secondary)
|
|
649
|
+
.multilineTextAlignment(.center)
|
|
650
|
+
}
|
|
651
|
+
.padding(32)
|
|
652
|
+
.navigationTitle("Preview")
|
|
653
|
+
}
|
|
654
|
+
}
|
|
@@ -440,8 +440,8 @@
|
|
|
440
440
|
isa = XCRemoteSwiftPackageReference;
|
|
441
441
|
repositoryURL = "https://github.com/afomera/hotwire-native-ios";
|
|
442
442
|
requirement = {
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
kind = revision;
|
|
444
|
+
revision = 915194a338d0a4c686ee43ace7450b3aa0e885c3;
|
|
445
445
|
};
|
|
446
446
|
};
|
|
447
447
|
/* End XCRemoteSwiftPackageReference section */
|
|
@@ -42,12 +42,19 @@ recommitting — never in a stamped copy.
|
|
|
42
42
|
|
|
43
43
|
## SPM pin policy
|
|
44
44
|
|
|
45
|
-
The project depends on one package:
|
|
46
|
-
`https://github.com/
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
The project depends on one remote package: the fork
|
|
46
|
+
`https://github.com/afomera/hotwire-native-ios`, pinned to an **exact revision**
|
|
47
|
+
(`kind = revision` in the pbxproj `XCRemoteSwiftPackageReference` requirement).
|
|
48
|
+
A revision pin resolves offline from an existing SPM clone; a branch pin makes
|
|
49
|
+
every build re-check the remote, which is why the template does not use one.
|
|
50
|
+
|
|
51
|
+
Moving the pin is a deliberate two-file edit, and both files must carry the
|
|
52
|
+
same SHA:
|
|
53
|
+
|
|
54
|
+
1. `App.xcodeproj/project.pbxproj` — the `requirement` dict (`kind = revision;`
|
|
55
|
+
/ `revision = <40-char sha>;`)
|
|
56
|
+
2. `App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` —
|
|
57
|
+
the pin's `state.revision` (no `branch` key)
|
|
51
58
|
|
|
52
59
|
## Building standalone
|
|
53
60
|
|
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.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrea Fomera
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '0.15'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rqrcode
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
68
82
|
description: RubyEverywhere gives a Rails (or Sinatra, or Hanami) app a native life
|
|
69
83
|
on desktop, iOS, and Android. Desktop is a Tauri shell — in local mode the whole
|
|
70
84
|
app is pressed into a single-file binary with Tebako and runs on-device with no
|
|
@@ -116,6 +130,7 @@ files:
|
|
|
116
130
|
- lib/everywhere/commands/platform/login.rb
|
|
117
131
|
- lib/everywhere/commands/platform/logout.rb
|
|
118
132
|
- lib/everywhere/commands/platform/runner.rb
|
|
133
|
+
- lib/everywhere/commands/preview.rb
|
|
119
134
|
- lib/everywhere/commands/publish.rb
|
|
120
135
|
- lib/everywhere/commands/release.rb
|
|
121
136
|
- lib/everywhere/commands/shell_dir.rb
|
|
@@ -134,6 +149,7 @@ files:
|
|
|
134
149
|
- lib/everywhere/framework.rb
|
|
135
150
|
- lib/everywhere/icon.rb
|
|
136
151
|
- lib/everywhere/ignore.rb
|
|
152
|
+
- lib/everywhere/jump.rb
|
|
137
153
|
- lib/everywhere/line_pump.rb
|
|
138
154
|
- lib/everywhere/log_filter.rb
|
|
139
155
|
- lib/everywhere/minisign.rb
|