ruby_everywhere 0.6.0 → 0.8.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 +2 -2
- data/exe/rbe +2 -2
- data/lib/everywhere/agents_guide.rb +3 -1
- data/lib/everywhere/blake2b.rb +17 -1
- data/lib/everywhere/boot.rb +30 -6
- data/lib/everywhere/builders/android.rb +152 -64
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +32 -40
- data/lib/everywhere/builders/ios.rb +268 -36
- data/lib/everywhere/builders/native_sources.rb +38 -0
- data/lib/everywhere/child_processes.rb +4 -4
- data/lib/everywhere/child_supervision.rb +172 -0
- data/lib/everywhere/cli.rb +2 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +132 -66
- data/lib/everywhere/commands/clean.rb +8 -3
- data/lib/everywhere/commands/dev.rb +91 -242
- data/lib/everywhere/commands/doctor.rb +138 -21
- data/lib/everywhere/commands/install.rb +49 -8
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +141 -33
- data/lib/everywhere/commands/platform/login.rb +20 -6
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +170 -25
- data/lib/everywhere/commands/preview.rb +286 -0
- data/lib/everywhere/commands/publish.rb +22 -2
- data/lib/everywhere/commands/release.rb +155 -35
- data/lib/everywhere/commands/shell_dir.rb +4 -2
- data/lib/everywhere/commands/updates_keygen.rb +25 -1
- data/lib/everywhere/config/app.rb +126 -0
- data/lib/everywhere/config/auth.rb +108 -0
- data/lib/everywhere/config/data.rb +50 -0
- data/lib/everywhere/config/deep_linking.rb +107 -0
- data/lib/everywhere/config/desktop_ui.rb +153 -0
- data/lib/everywhere/config/mobile.rb +211 -0
- data/lib/everywhere/config/native_desktop.rb +168 -0
- data/lib/everywhere/config/native_mobile.rb +337 -0
- data/lib/everywhere/config/shell.rb +57 -0
- data/lib/everywhere/config/updates.rb +63 -0
- data/lib/everywhere/config.rb +59 -1367
- data/lib/everywhere/console.rb +2 -1
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/engine.rb +24 -0
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +23 -6
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +13 -5
- data/lib/everywhere/jump.rb +121 -0
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/mobile_config_endpoint.rb +47 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +36 -13
- data/lib/everywhere/platform/client.rb +46 -7
- data/lib/everywhere/platform/credentials.rb +18 -8
- data/lib/everywhere/platform/snapshot.rb +12 -0
- data/lib/everywhere/plist.rb +17 -0
- data/lib/everywhere/png.rb +1 -0
- data/lib/everywhere/raw_tty.rb +51 -0
- data/lib/everywhere/receipt.rb +55 -10
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/shellout.rb +19 -0
- data/lib/everywhere/simulator.rb +12 -1
- data/lib/everywhere/tab_filter.rb +36 -0
- data/lib/everywhere/task_pool.rb +3 -4
- data/lib/everywhere/ui.rb +6 -1
- data/lib/everywhere/version.rb +6 -4
- data/lib/everywhere.rb +1 -2
- 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 -4
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
- data/support/mobile/ios/README.md +13 -6
- data/support/release/macos/notarize.sh +3 -0
- metadata +40 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
class Config
|
|
5
|
+
module NativeMobile
|
|
6
|
+
# Native code extensions ("supernative"): Swift the app repo carries in
|
|
7
|
+
# native/ios/ that `every build` compiles into the shell, plus this
|
|
8
|
+
# declaration of what to hook up. Build-time only — stores forbid
|
|
9
|
+
# downloading native code, so extensions ship with the app binary.
|
|
10
|
+
#
|
|
11
|
+
# native:
|
|
12
|
+
# ios:
|
|
13
|
+
# components: [ChartComponent] # BridgeComponent subclasses to register
|
|
14
|
+
# screens: # path rules with view_controller: <id>
|
|
15
|
+
# map: MapScreen # SwiftUI View or UIViewController, init(url:)
|
|
16
|
+
# splash: LaunchSplash # SwiftUI View or UIViewController, init()
|
|
17
|
+
# splash_min_seconds: 1.0 # how long the custom splash stays up at minimum
|
|
18
|
+
# lazy_load_tabs: true # defer each tab's first visit until it's selected
|
|
19
|
+
def native_ios = native_section("ios")
|
|
20
|
+
|
|
21
|
+
def native_ios_components = native_components("ios")
|
|
22
|
+
|
|
23
|
+
def native_ios_screens = native_screens("ios")
|
|
24
|
+
|
|
25
|
+
def native_ios_splash = native_splash("ios")
|
|
26
|
+
|
|
27
|
+
# Minimum seconds a custom splash stays on screen (default 1.0 shell-side;
|
|
28
|
+
# a fast server would otherwise dismiss a branded splash in a ~50ms flash).
|
|
29
|
+
# Clamped to the shell's 8s give-up timeout.
|
|
30
|
+
def native_ios_splash_min_seconds = native_splash_seconds("ios")
|
|
31
|
+
|
|
32
|
+
# Whether the native tab bar defers each non-selected tab's initial visit
|
|
33
|
+
# until the tab is first selected (Hotwire Native `lazyLoadTabs`). Tri-state
|
|
34
|
+
# so apps can opt in or opt out explicitly: `true` lazy-loads, `false` loads
|
|
35
|
+
# every tab up front, and an absent key leaves the shell on its default
|
|
36
|
+
# (eager).
|
|
37
|
+
def native_ios_lazy_load_tabs = native_lazy_load("ios")
|
|
38
|
+
|
|
39
|
+
# Third-party Swift packages this app pins for its native/ios code. Each
|
|
40
|
+
# entry is one SPM dependency; `every build` writes them into the shell's
|
|
41
|
+
# local NativeExtensions package and re-exports every product, so native/ios
|
|
42
|
+
# code reaches them with a single `import NativeExtensions`.
|
|
43
|
+
#
|
|
44
|
+
# packages:
|
|
45
|
+
# - url: https://github.com/simibac/ConfettiSwiftUI
|
|
46
|
+
# from: "1.1.0" # or exact: / branch: / revision:
|
|
47
|
+
# products: [ConfettiSwiftUI] # optional; defaults to the repo name
|
|
48
|
+
#
|
|
49
|
+
# Returns normalized entries: { "url", "requirement" => { "kind", "value" },
|
|
50
|
+
# "products" => [...] }. Assumes the config validated clean (see
|
|
51
|
+
# native_ios_package_errors) — the requirement is always present here.
|
|
52
|
+
PACKAGE_REQUIREMENT_KEYS = %w[from exact branch revision].freeze
|
|
53
|
+
|
|
54
|
+
def native_ios_packages
|
|
55
|
+
Array(native_ios["packages"]).filter_map do |entry|
|
|
56
|
+
next unless entry.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
url = entry["url"].to_s.strip
|
|
59
|
+
kind = PACKAGE_REQUIREMENT_KEYS.find { |k| entry.key?(k) && !entry[k].to_s.strip.empty? }
|
|
60
|
+
products = Array(entry["products"]).map { |p| p.to_s.strip }.reject(&:empty?)
|
|
61
|
+
products = [package_identity(url)] if products.empty?
|
|
62
|
+
|
|
63
|
+
{ "url" => url,
|
|
64
|
+
"requirement" => (kind && { "kind" => kind, "value" => entry[kind].to_s.strip }),
|
|
65
|
+
"products" => products }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def native_ios_packages? = !native_ios_packages.empty?
|
|
70
|
+
|
|
71
|
+
# SPM's package identity: the URL's last path segment without a .git suffix
|
|
72
|
+
# (github.com/simibac/ConfettiSwiftUI → "ConfettiSwiftUI"). Used as the
|
|
73
|
+
# default product name and as the `package:` key in .product(name:package:).
|
|
74
|
+
def package_identity(url) = File.basename(url.to_s.sub(%r{/+\z}, "")).sub(/\.git\z/, "")
|
|
75
|
+
|
|
76
|
+
def native_ios? = native_declared?("ios")
|
|
77
|
+
|
|
78
|
+
# Everything declared here is interpolated into generated Swift, so names
|
|
79
|
+
# are validated hard: type names must be plain Swift identifiers, screen
|
|
80
|
+
# identifiers must be safe inside a Swift string literal.
|
|
81
|
+
SWIFT_TYPE_NAME = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
82
|
+
SCREEN_IDENTIFIER = /\A[A-Za-z0-9_-]+\z/
|
|
83
|
+
|
|
84
|
+
def native_ios_errors = native_type_errors("ios") + native_ios_package_errors
|
|
85
|
+
|
|
86
|
+
# A URL SPM accepts (https for the common case, git@ for SSH remotes) and a
|
|
87
|
+
# loose semver for from:/exact: — enough to catch typos without reimplementing
|
|
88
|
+
# SPM's resolver, which reports anything subtler at build time.
|
|
89
|
+
PACKAGE_URL = %r{\A(https?://|git@).+}
|
|
90
|
+
PACKAGE_VERSION = /\A\d+(\.\d+){0,2}(-[0-9A-Za-z.-]+)?\z/
|
|
91
|
+
|
|
92
|
+
def native_ios_package_errors
|
|
93
|
+
raw = native_ios["packages"]
|
|
94
|
+
return [] if raw.nil?
|
|
95
|
+
return ["native.ios.packages must be a list"] unless raw.is_a?(Array)
|
|
96
|
+
|
|
97
|
+
raw.each_with_index.flat_map do |entry, i|
|
|
98
|
+
at = "native.ios.packages[#{i}]"
|
|
99
|
+
next ["#{at} must be a mapping with a url:"] unless entry.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
errors = []
|
|
102
|
+
url = entry["url"].to_s.strip
|
|
103
|
+
if url.empty?
|
|
104
|
+
errors << "#{at}.url is required"
|
|
105
|
+
elsif !url.match?(PACKAGE_URL)
|
|
106
|
+
errors << "#{at}.url #{url.inspect} must be an https:// or git@ URL"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
present = PACKAGE_REQUIREMENT_KEYS.select { |k| entry.key?(k) && !entry[k].to_s.strip.empty? }
|
|
110
|
+
if present.empty?
|
|
111
|
+
errors << "#{at} needs one version requirement (#{PACKAGE_REQUIREMENT_KEYS.join(" / ")})"
|
|
112
|
+
elsif present.length > 1
|
|
113
|
+
errors << "#{at} sets conflicting requirements (#{present.join(", ")}) — pick one"
|
|
114
|
+
elsif %w[from exact].include?(present.first) && !entry[present.first].to_s.strip.match?(PACKAGE_VERSION)
|
|
115
|
+
errors << "#{at}.#{present.first} #{entry[present.first].to_s.inspect} must be a version like \"1.2.0\""
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Array(entry["products"]).each do |product|
|
|
119
|
+
next if product.to_s.match?(SWIFT_TYPE_NAME)
|
|
120
|
+
|
|
121
|
+
errors << "#{at}.products: #{product.to_s.inspect} is not a module name (letters, digits, _)"
|
|
122
|
+
end
|
|
123
|
+
errors
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The Android half of "supernative": Kotlin the app repo carries in
|
|
128
|
+
# native/android/ that `every build --android` compiles into the shell,
|
|
129
|
+
# declared the same way iOS declares Swift. Same build-time-only rule —
|
|
130
|
+
# Play forbids downloading executable code — and the same shape, so an app
|
|
131
|
+
# that already knows `native.ios:` knows this section too.
|
|
132
|
+
#
|
|
133
|
+
# native:
|
|
134
|
+
# android:
|
|
135
|
+
# components: [ChartComponent] # BridgeComponent subclasses to register
|
|
136
|
+
# screens: # path rules with view_controller: <id>
|
|
137
|
+
# map: MapFragment # Fragment with an (url) argument
|
|
138
|
+
# splash: LaunchSplash # Fragment/Activity shown while booting
|
|
139
|
+
# splash_min_seconds: 1.0 # how long that splash stays up at minimum
|
|
140
|
+
# lazy_load_tabs: true # defer each tab's first visit until selected
|
|
141
|
+
# icon_font: symbols # which Material icon font resolves icon names
|
|
142
|
+
# packages: # Maven coordinates, not SPM entries
|
|
143
|
+
# - "com.airbnb.android:lottie:6.4.0"
|
|
144
|
+
def native_android = native_section("android")
|
|
145
|
+
|
|
146
|
+
def native_android_components = native_components("android")
|
|
147
|
+
|
|
148
|
+
def native_android_screens = native_screens("android")
|
|
149
|
+
|
|
150
|
+
def native_android_splash = native_splash("android")
|
|
151
|
+
|
|
152
|
+
# Same contract as the iOS splash minimum, and the same clamp: the shell
|
|
153
|
+
# gives up waiting at 8s either way, so a larger number here would only
|
|
154
|
+
# promise something the shell won't honor.
|
|
155
|
+
def native_android_splash_min_seconds = native_splash_seconds("android")
|
|
156
|
+
|
|
157
|
+
# Tri-state like native_ios_lazy_load_tabs: true defers each non-selected
|
|
158
|
+
# tab's first visit, false loads them all up front, absent leaves the shell
|
|
159
|
+
# on its default. Android's bottom-nav hosts are created eagerly at
|
|
160
|
+
# onCreate, so this decides whether they *visit*, not whether they exist.
|
|
161
|
+
def native_android_lazy_load_tabs = native_lazy_load("android")
|
|
162
|
+
|
|
163
|
+
# Third-party dependencies for native/android code, as Gradle sees them:
|
|
164
|
+
# plain Maven coordinates. There is no SPM-style url + requirement split —
|
|
165
|
+
# a coordinate already carries group, artifact and version — so this
|
|
166
|
+
# accessor returns strings, one per `implementation(…)` line the builder
|
|
167
|
+
# writes into native-packages.gradle.kts. Assumes the config validated
|
|
168
|
+
# clean (see native_android_package_errors).
|
|
169
|
+
def native_android_packages
|
|
170
|
+
Array(native_android["packages"]).filter_map do |entry|
|
|
171
|
+
next unless entry.is_a?(String)
|
|
172
|
+
|
|
173
|
+
coordinate = entry.strip
|
|
174
|
+
coordinate unless coordinate.empty?
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def native_android_packages? = !native_android_packages.empty?
|
|
179
|
+
|
|
180
|
+
def native_android? = native_declared?("android")
|
|
181
|
+
|
|
182
|
+
# Kotlin's identifier rules match Swift's across everything we generate
|
|
183
|
+
# (no backticked names, no unicode escapes), so one pattern validates both.
|
|
184
|
+
# Aliased rather than reused by name so the Android messages can say
|
|
185
|
+
# "Kotlin" and the two paths can diverge later without touching iOS.
|
|
186
|
+
KOTLIN_TYPE_NAME = SWIFT_TYPE_NAME
|
|
187
|
+
|
|
188
|
+
def native_android_errors
|
|
189
|
+
native_type_errors("android") + native_android_icon_font_errors + native_android_package_errors
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# group:artifact:version — Gradle's shorthand form, and the only one we
|
|
193
|
+
# accept. The charset is narrower than Maven strictly allows on purpose:
|
|
194
|
+
# every coordinate is interpolated into a generated Kotlin DSL file inside
|
|
195
|
+
# a double-quoted literal, so a quote, a backslash, a `$` (Kotlin string
|
|
196
|
+
# templates), a newline or a space would rewrite the build script rather
|
|
197
|
+
# than name a dependency. Version ranges ("[1.0,2.0)") and BOM-style
|
|
198
|
+
# two-part coordinates are rejected for the same reason and because Gradle
|
|
199
|
+
# resolves anything subtler at build time anyway.
|
|
200
|
+
MAVEN_SEGMENT = /[A-Za-z0-9_][A-Za-z0-9_.-]*/
|
|
201
|
+
MAVEN_COORDINATE = /\A#{MAVEN_SEGMENT}:#{MAVEN_SEGMENT}:[A-Za-z0-9_][A-Za-z0-9_.+-]*\z/
|
|
202
|
+
|
|
203
|
+
def native_android_package_errors
|
|
204
|
+
raw = native_android["packages"]
|
|
205
|
+
return [] if raw.nil?
|
|
206
|
+
return ["native.android.packages must be a list"] unless raw.is_a?(Array)
|
|
207
|
+
|
|
208
|
+
raw.each_with_index.filter_map do |entry, i|
|
|
209
|
+
at = "native.android.packages[#{i}]"
|
|
210
|
+
next "#{at} must be a Maven coordinate string like \"com.airbnb.android:lottie:6.4.0\"" unless entry.is_a?(String)
|
|
211
|
+
|
|
212
|
+
coordinate = entry.strip
|
|
213
|
+
next if coordinate.match?(MAVEN_COORDINATE)
|
|
214
|
+
|
|
215
|
+
"#{at} #{coordinate.inspect} is not a Maven coordinate — " \
|
|
216
|
+
"\"group:artifact:version\" (letters, digits, . _ -)"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Which Material icon font `icons.android` names — and the per-platform
|
|
221
|
+
# icon names nav buttons and menu items carry in page HTML — resolve
|
|
222
|
+
# against. Android has no UIImage(systemName:), so the shell draws the
|
|
223
|
+
# icon's codepoint from a font; the choice is which font ships.
|
|
224
|
+
#
|
|
225
|
+
# `bundled` is the whole reason this is a map and not a list: only the
|
|
226
|
+
# classic set (357 KB) rides inside the gem, because `ruby_everywhere` is
|
|
227
|
+
# 229 KB today and Symbols is ~10.6 MB — a 48x install cost paid by every
|
|
228
|
+
# user, including the desktop-only ones. The Symbols variants are fetched
|
|
229
|
+
# once on first Android build and cached under ~/.rubyeverywhere, which is
|
|
230
|
+
# noise next to the Gradle and AGP downloads the same build already makes.
|
|
231
|
+
#
|
|
232
|
+
# `codepoints` is the name → codepoint map Ruby validates every declared
|
|
233
|
+
# icon name against at build time, so a typo fails the build naming the tab
|
|
234
|
+
# it came from instead of rendering a blank icon on device.
|
|
235
|
+
ANDROID_ICON_FONTS = {
|
|
236
|
+
"symbols" => { "file" => "MaterialSymbolsOutlined.ttf", "bundled" => false },
|
|
237
|
+
"symbols-rounded" => { "file" => "MaterialSymbolsRounded.ttf", "bundled" => false },
|
|
238
|
+
"symbols-sharp" => { "file" => "MaterialSymbolsSharp.ttf", "bundled" => false },
|
|
239
|
+
"classic" => { "file" => "MaterialIcons-Regular.ttf", "bundled" => true },
|
|
240
|
+
# No font at all: the app brings its own drawables in native/android/res/.
|
|
241
|
+
"none" => { "file" => nil, "bundled" => true }
|
|
242
|
+
}.freeze
|
|
243
|
+
|
|
244
|
+
DEFAULT_ANDROID_ICON_FONT = "symbols"
|
|
245
|
+
|
|
246
|
+
def native_android_icon_font
|
|
247
|
+
value = native_android["icon_font"].to_s.strip
|
|
248
|
+
value.empty? ? DEFAULT_ANDROID_ICON_FONT : value
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# The font's asset filename (assets/fonts/<file>), or nil for "none".
|
|
252
|
+
def native_android_icon_font_file = ANDROID_ICON_FONTS.dig(native_android_icon_font, "file")
|
|
253
|
+
|
|
254
|
+
# The sidecar map the builder validates names against, alongside the font.
|
|
255
|
+
def native_android_icon_font_codepoints
|
|
256
|
+
native_android_icon_font_file&.sub(/\.ttf\z/, ".codepoints")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# True when the selected font has to be downloaded before the first build
|
|
260
|
+
# can stamp it — the only case that can fail offline, so the builder warns
|
|
261
|
+
# about it (and points at `classic`) rather than dying mid-Gradle.
|
|
262
|
+
def native_android_icon_font_fetched?
|
|
263
|
+
entry = ANDROID_ICON_FONTS[native_android_icon_font]
|
|
264
|
+
!entry.nil? && entry["bundled"] == false
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def native_android_icons? = !native_android_icon_font_file.nil?
|
|
268
|
+
|
|
269
|
+
def native_android_icon_font_errors
|
|
270
|
+
return [] if ANDROID_ICON_FONTS.key?(native_android_icon_font)
|
|
271
|
+
|
|
272
|
+
["native.android.icon_font #{native_android_icon_font.inspect} is not a known font — " \
|
|
273
|
+
"one of: #{ANDROID_ICON_FONTS.keys.join(", ")}"]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# The language each shell's extensions are written in, named in the
|
|
277
|
+
# messages because that's what the app author is looking at, and the
|
|
278
|
+
# pattern their type names must match — one entry per platform so the two
|
|
279
|
+
# can still diverge without either one's half changing.
|
|
280
|
+
NATIVE_LANGUAGES = { "ios" => "Swift", "android" => "Kotlin" }.freeze
|
|
281
|
+
NATIVE_TYPE_NAMES = { "ios" => SWIFT_TYPE_NAME, "android" => KOTLIN_TYPE_NAME }.freeze
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
# The two `native:` halves are declared the same way and read the same
|
|
286
|
+
# way, so the readers above are one line each over these. What genuinely
|
|
287
|
+
# differs — SPM packages vs Maven coordinates, the Android icon font —
|
|
288
|
+
# stays spelled out per platform.
|
|
289
|
+
def native_section(os)
|
|
290
|
+
section = @data.dig("native", os)
|
|
291
|
+
section.is_a?(Hash) ? section : {}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def native_components(os) = Array(native_section(os)["components"]).map(&:to_s)
|
|
295
|
+
|
|
296
|
+
def native_screens(os)
|
|
297
|
+
entries = native_section(os)["screens"]
|
|
298
|
+
return {} unless entries.is_a?(Hash)
|
|
299
|
+
|
|
300
|
+
entries.to_h { |id, type| [id.to_s, type.to_s] }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def native_splash(os) = native_section(os)["splash"]&.to_s
|
|
304
|
+
|
|
305
|
+
def native_splash_seconds(os)
|
|
306
|
+
value = native_section(os)["splash_min_seconds"]
|
|
307
|
+
value.to_f.clamp(0.0, 8.0) if value.is_a?(Numeric)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def native_lazy_load(os)
|
|
311
|
+
value = native_section(os)["lazy_load_tabs"]
|
|
312
|
+
value if value == true || value == false
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def native_declared?(os)
|
|
316
|
+
!(native_components(os).empty? && native_screens(os).empty? && native_splash(os).nil?)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# The type/identifier half of one platform's errors — everything both
|
|
320
|
+
# shells validate identically. Each platform's public *_errors appends
|
|
321
|
+
# what only it has (packages, and Android's icon font).
|
|
322
|
+
def native_type_errors(os)
|
|
323
|
+
types = native_components(os) + native_screens(os).values + [native_splash(os)].compact
|
|
324
|
+
errors = types.reject { |t| t.match?(NATIVE_TYPE_NAMES[os]) }.map do |t|
|
|
325
|
+
"native.#{os}: #{t.inspect} is not a #{NATIVE_LANGUAGES[os]} type name (letters, digits, _)"
|
|
326
|
+
end
|
|
327
|
+
errors += native_screens(os).keys.reject { |id| id.match?(SCREEN_IDENTIFIER) }.map do |id|
|
|
328
|
+
"native.#{os}.screens: identifier #{id.inspect} must match #{SCREEN_IDENTIFIER.inspect}"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
raw = native_section(os)["splash_min_seconds"]
|
|
332
|
+
errors << "native.#{os}.splash_min_seconds must be a number (seconds)" if raw && !raw.is_a?(Numeric)
|
|
333
|
+
errors
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
class Config
|
|
5
|
+
module Shell
|
|
6
|
+
# The two `native:` knobs that ship inside everywhere.json rather than
|
|
7
|
+
# being compiled in, resolved for the platform being stamped. They read the
|
|
8
|
+
# target's own section: everywhere.json is written once per target, so an
|
|
9
|
+
# Android build that inherited native.ios.lazy_load_tabs would silently
|
|
10
|
+
# apply a decision the app made about a different shell — and the two
|
|
11
|
+
# platforms load tabs differently enough that the answer legitimately
|
|
12
|
+
# differs. Without a target (the desktop dev loop, and every caller written
|
|
13
|
+
# before Android existed) the iOS answer stands, which is what those
|
|
14
|
+
# callers have always gotten.
|
|
15
|
+
def native_lazy_load_tabs(target: nil)
|
|
16
|
+
android_target?(target) ? native_android_lazy_load_tabs : native_ios_lazy_load_tabs
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def native_splash_min_seconds(target: nil)
|
|
20
|
+
android_target?(target) ? native_android_splash_min_seconds : native_ios_splash_min_seconds
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The subset the shell needs, shipped as JSON (env var in dev,
|
|
24
|
+
# Resources/everywhere.json inside a bundled .app). Pass `target:` so the
|
|
25
|
+
# packaged config reflects the platform being built (its bundle id / name).
|
|
26
|
+
def to_shell_hash(target: nil)
|
|
27
|
+
{
|
|
28
|
+
"name" => name(target: target),
|
|
29
|
+
"bundle_id" => bundle_id(target: target),
|
|
30
|
+
"version" => version(target: target),
|
|
31
|
+
"mode" => mode,
|
|
32
|
+
"remote_url" => remote_url,
|
|
33
|
+
"remote_instances" => (true if remote_instances?),
|
|
34
|
+
"entry_path" => entry_path,
|
|
35
|
+
"tint_color" => tint_color,
|
|
36
|
+
"background_color" => background_color,
|
|
37
|
+
"menu" => (menu unless menu.empty?),
|
|
38
|
+
"tray" => (tray unless tray.empty?),
|
|
39
|
+
"window" => window_shell_hash,
|
|
40
|
+
"lazy_load_tabs" => native_lazy_load_tabs(target: target),
|
|
41
|
+
"splash_min_seconds" => native_splash_min_seconds(target: target),
|
|
42
|
+
"updates" => updates_shell_hash,
|
|
43
|
+
"permissions" => (permissions.keys unless permissions.empty?),
|
|
44
|
+
"auth" => auth_shell_hash,
|
|
45
|
+
# Hosts the shell treats as its own for incoming universal links, so a
|
|
46
|
+
# tapped link to any associated domain opens in-app rather than Safari.
|
|
47
|
+
"universal_link_hosts" => (deep_linking_domains if deep_linking? && !deep_linking_domains.empty?)
|
|
48
|
+
}.compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_shell_json(target: nil)
|
|
52
|
+
require "json"
|
|
53
|
+
JSON.generate(to_shell_hash(target: target))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
class Config
|
|
5
|
+
module Updates
|
|
6
|
+
# The `updates:` section — self-hosted auto-updates. Two halves:
|
|
7
|
+
# a client half (url/channel/public_key/auto/interval) that ships to the
|
|
8
|
+
# shell, and a publish-side `s3:` half that NEVER leaves the dev machine.
|
|
9
|
+
# `channel` here is a *release channel* (stable/beta) — not the receipt's
|
|
10
|
+
# `distribution_channel` (direct vs app stores).
|
|
11
|
+
def updates = @data.fetch("updates", nil) || {}
|
|
12
|
+
|
|
13
|
+
def updates_s3 = updates.fetch("s3", nil) || {}
|
|
14
|
+
|
|
15
|
+
def updates_channel = updates["channel"] || "stable"
|
|
16
|
+
|
|
17
|
+
def updates_url = updates["url"]&.chomp("/")
|
|
18
|
+
|
|
19
|
+
def updates_public_key = updates["public_key"]
|
|
20
|
+
|
|
21
|
+
# off — never check; check — check + notify (default); download — also
|
|
22
|
+
# fetch/verify in the background; install — silent auto-update.
|
|
23
|
+
def updates_auto = updates["auto"] || "check"
|
|
24
|
+
|
|
25
|
+
def updates_interval = [(updates["interval"] || 21_600).to_i, 300].max
|
|
26
|
+
|
|
27
|
+
# A dev machine serving a feed to itself is the one case http is fine.
|
|
28
|
+
UPDATES_LOOPBACK = /\A(localhost|127(\.\d+){3}|::1)\z/
|
|
29
|
+
|
|
30
|
+
# An http updates.url is not a smaller version of a working updater: the
|
|
31
|
+
# shell refuses to fetch it and silently stops checking, so the app ships
|
|
32
|
+
# looking fine and never updates again. Say so at build/publish time.
|
|
33
|
+
def updates_errors
|
|
34
|
+
url = updates_url.to_s
|
|
35
|
+
return [] if url.empty?
|
|
36
|
+
|
|
37
|
+
require "uri"
|
|
38
|
+
uri = URI.parse(url)
|
|
39
|
+
return [] if uri.scheme == "https"
|
|
40
|
+
return [] if uri.scheme == "http" && uri.host.to_s.delete("[]").match?(UPDATES_LOOPBACK)
|
|
41
|
+
|
|
42
|
+
["updates.url #{url.inspect} must be https — the shell disables the updater otherwise"]
|
|
43
|
+
rescue URI::Error => e
|
|
44
|
+
["updates.url #{url.inspect} is not a URL (#{e.message})"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The client subset for the shell; nil (and thus absent from
|
|
48
|
+
# everywhere.json) until both url and public_key are configured — an
|
|
49
|
+
# unsigned update feed is not a thing.
|
|
50
|
+
def updates_shell_hash
|
|
51
|
+
return nil unless updates_url && updates_public_key
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
"url" => updates_url,
|
|
55
|
+
"channel" => updates_channel,
|
|
56
|
+
"public_key" => updates_public_key,
|
|
57
|
+
"auto" => updates_auto,
|
|
58
|
+
"interval" => updates_interval
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|