Module: Everywhere::Config::NativeMobile
- Included in:
- Everywhere::Config
- Defined in:
- lib/everywhere/config/native_mobile.rb
Constant Summary collapse
- PACKAGE_REQUIREMENT_KEYS =
Third-party Swift packages this app pins for its native/ios code. Each entry is one SPM dependency;
every buildwrites them into the shell's local NativeExtensions package and re-exports every product, so native/ios code reaches them with a singleimport NativeExtensions.packages: - url: https://github.com/simibac/ConfettiSwiftUI from: "1.1.0" # or exact: / branch: / revision: products: [ConfettiSwiftUI] # optional; defaults to the repo nameReturns normalized entries: { "url", "requirement" => { "kind", "value" }, "products" => [...] }. Assumes the config validated clean (see native_ios_package_errors) — the requirement is always present here.
%w[from exact branch revision].freeze
- SWIFT_TYPE_NAME =
Everything declared here is interpolated into generated Swift, so names are validated hard: type names must be plain Swift identifiers, screen identifiers must be safe inside a Swift string literal.
/\A[A-Za-z_][A-Za-z0-9_]*\z/- SCREEN_IDENTIFIER =
/\A[A-Za-z0-9_-]+\z/- PACKAGE_URL =
A URL SPM accepts (https for the common case, git@ for SSH remotes) and a loose semver for from:/exact: — enough to catch typos without reimplementing SPM's resolver, which reports anything subtler at build time.
%r{\A(https?://|git@).+}- PACKAGE_VERSION =
/\A\d+(\.\d+){0,2}(-[0-9A-Za-z.-]+)?\z/- KOTLIN_TYPE_NAME =
Kotlin's identifier rules match Swift's across everything we generate (no backticked names, no unicode escapes), so one pattern validates both. Aliased rather than reused by name so the Android messages can say "Kotlin" and the two paths can diverge later without touching iOS.
SWIFT_TYPE_NAME- MAVEN_SEGMENT =
group:artifact:version — Gradle's shorthand form, and the only one we accept. The charset is narrower than Maven strictly allows on purpose: every coordinate is interpolated into a generated Kotlin DSL file inside a double-quoted literal, so a quote, a backslash, a
$(Kotlin string templates), a newline or a space would rewrite the build script rather than name a dependency. Version ranges ("[1.0,2.0)") and BOM-style two-part coordinates are rejected for the same reason and because Gradle resolves anything subtler at build time anyway. /[A-Za-z0-9_][A-Za-z0-9_.-]*/- MAVEN_COORDINATE =
/\A#{MAVEN_SEGMENT}:#{MAVEN_SEGMENT}:[A-Za-z0-9_][A-Za-z0-9_.+-]*\z/- ANDROID_ICON_FONTS =
Which Material icon font
icons.androidnames — and the per-platform icon names nav buttons and menu items carry in page HTML — resolve against. Android has no UIImage(systemName:), so the shell draws the icon's codepoint from a font; the choice is which font ships.bundledis the whole reason this is a map and not a list: only the classic set (357 KB) rides inside the gem, becauseruby_everywhereis 229 KB today and Symbols is ~10.6 MB — a 48x install cost paid by every user, including the desktop-only ones. The Symbols variants are fetched once on first Android build and cached under ~/.rubyeverywhere, which is noise next to the Gradle and AGP downloads the same build already makes.codepointsis the name → codepoint map Ruby validates every declared icon name against at build time, so a typo fails the build naming the tab it came from instead of rendering a blank icon on device. { "symbols" => { "file" => "MaterialSymbolsOutlined.ttf", "bundled" => false }, "symbols-rounded" => { "file" => "MaterialSymbolsRounded.ttf", "bundled" => false }, "symbols-sharp" => { "file" => "MaterialSymbolsSharp.ttf", "bundled" => false }, "classic" => { "file" => "MaterialIcons-Regular.ttf", "bundled" => true }, # No font at all: the app brings its own drawables in native/android/res/. "none" => { "file" => nil, "bundled" => true } }.freeze
- DEFAULT_ANDROID_ICON_FONT =
"symbols"- NATIVE_LANGUAGES =
The language each shell's extensions are written in, named in the messages because that's what the app author is looking at, and the pattern their type names must match — one entry per platform so the two can still diverge without either one's half changing.
{ "ios" => "Swift", "android" => "Kotlin" }.freeze
- NATIVE_TYPE_NAMES =
{ "ios" => SWIFT_TYPE_NAME, "android" => KOTLIN_TYPE_NAME }.freeze
Instance Method Summary collapse
-
#native_android ⇒ Object
The Android half of "supernative": Kotlin the app repo carries in native/android/ that
every build --androidcompiles into the shell, declared the same way iOS declares Swift. - #native_android? ⇒ Boolean
- #native_android_components ⇒ Object
- #native_android_errors ⇒ Object
- #native_android_icon_font ⇒ Object
-
#native_android_icon_font_codepoints ⇒ Object
The sidecar map the builder validates names against, alongside the font.
- #native_android_icon_font_errors ⇒ Object
-
#native_android_icon_font_fetched? ⇒ Boolean
True when the selected font has to be downloaded before the first build can stamp it — the only case that can fail offline, so the builder warns about it (and points at
classic) rather than dying mid-Gradle. -
#native_android_icon_font_file ⇒ Object
The font's asset filename (assets/fonts/
), or nil for "none". - #native_android_icons? ⇒ Boolean
-
#native_android_lazy_load_tabs ⇒ Object
Tri-state like native_ios_lazy_load_tabs: true defers each non-selected tab's first visit, false loads them all up front, absent leaves the shell on its default.
- #native_android_package_errors ⇒ Object
-
#native_android_packages ⇒ Object
Third-party dependencies for native/android code, as Gradle sees them: plain Maven coordinates.
- #native_android_packages? ⇒ Boolean
- #native_android_screens ⇒ Object
- #native_android_splash ⇒ Object
-
#native_android_splash_min_seconds ⇒ Object
Same contract as the iOS splash minimum, and the same clamp: the shell gives up waiting at 8s either way, so a larger number here would only promise something the shell won't honor.
-
#native_ios ⇒ Object
Native code extensions ("supernative"): Swift the app repo carries in native/ios/ that
every buildcompiles into the shell, plus this declaration of what to hook up. - #native_ios? ⇒ Boolean
- #native_ios_components ⇒ Object
- #native_ios_errors ⇒ Object
-
#native_ios_lazy_load_tabs ⇒ Object
Whether the native tab bar defers each non-selected tab's initial visit until the tab is first selected (Hotwire Native
lazyLoadTabs). - #native_ios_package_errors ⇒ Object
- #native_ios_packages ⇒ Object
- #native_ios_packages? ⇒ Boolean
- #native_ios_screens ⇒ Object
- #native_ios_splash ⇒ Object
-
#native_ios_splash_min_seconds ⇒ Object
Minimum seconds a custom splash stays on screen (default 1.0 shell-side; a fast server would otherwise dismiss a branded splash in a ~50ms flash).
-
#package_identity(url) ⇒ Object
SPM's package identity: the URL's last path segment without a .git suffix (github.com/simibac/ConfettiSwiftUI → "ConfettiSwiftUI").
Instance Method Details
#native_android ⇒ Object
The Android half of "supernative": Kotlin the app repo carries in
native/android/ that every build --android compiles into the shell,
declared the same way iOS declares Swift. Same build-time-only rule —
Play forbids downloading executable code — and the same shape, so an app
that already knows native.ios: knows this section too.
native:
android:
components: [ChartComponent] # BridgeComponent subclasses to register
screens: # path rules with view_controller: <id>
map: MapFragment # Fragment with an (url) argument
splash: LaunchSplash # Fragment/Activity shown while booting
splash_min_seconds: 1.0 # how long that splash stays up at minimum
lazy_load_tabs: true # defer each tab's first visit until selected
icon_font: symbols # which Material icon font resolves icon names
packages: # Maven coordinates, not SPM entries
- "com.airbnb.android:lottie:6.4.0"
144 |
# File 'lib/everywhere/config/native_mobile.rb', line 144 def native_android = native_section("android") |
#native_android? ⇒ Boolean
180 |
# File 'lib/everywhere/config/native_mobile.rb', line 180 def native_android? = native_declared?("android") |
#native_android_components ⇒ Object
146 |
# File 'lib/everywhere/config/native_mobile.rb', line 146 def native_android_components = native_components("android") |
#native_android_errors ⇒ Object
188 189 190 |
# File 'lib/everywhere/config/native_mobile.rb', line 188 def native_android_errors native_type_errors("android") + native_android_icon_font_errors + native_android_package_errors end |
#native_android_icon_font ⇒ Object
246 247 248 249 |
# File 'lib/everywhere/config/native_mobile.rb', line 246 def native_android_icon_font value = native_android["icon_font"].to_s.strip value.empty? ? DEFAULT_ANDROID_ICON_FONT : value end |
#native_android_icon_font_codepoints ⇒ Object
The sidecar map the builder validates names against, alongside the font.
255 256 257 |
# File 'lib/everywhere/config/native_mobile.rb', line 255 def native_android_icon_font_codepoints native_android_icon_font_file&.sub(/\.ttf\z/, ".codepoints") end |
#native_android_icon_font_errors ⇒ Object
269 270 271 272 273 274 |
# File 'lib/everywhere/config/native_mobile.rb', line 269 def native_android_icon_font_errors return [] if ANDROID_ICON_FONTS.key?(native_android_icon_font) ["native.android.icon_font #{native_android_icon_font.inspect} is not a known font — " \ "one of: #{ANDROID_ICON_FONTS.keys.join(", ")}"] end |
#native_android_icon_font_fetched? ⇒ Boolean
True when the selected font has to be downloaded before the first build
can stamp it — the only case that can fail offline, so the builder warns
about it (and points at classic) rather than dying mid-Gradle.
262 263 264 265 |
# File 'lib/everywhere/config/native_mobile.rb', line 262 def native_android_icon_font_fetched? entry = ANDROID_ICON_FONTS[native_android_icon_font] !entry.nil? && entry["bundled"] == false end |
#native_android_icon_font_file ⇒ Object
The font's asset filename (assets/fonts/
252 |
# File 'lib/everywhere/config/native_mobile.rb', line 252 def native_android_icon_font_file = ANDROID_ICON_FONTS.dig(native_android_icon_font, "file") |
#native_android_icons? ⇒ Boolean
267 |
# File 'lib/everywhere/config/native_mobile.rb', line 267 def native_android_icons? = !native_android_icon_font_file.nil? |
#native_android_lazy_load_tabs ⇒ Object
Tri-state like native_ios_lazy_load_tabs: true defers each non-selected tab's first visit, false loads them all up front, absent leaves the shell on its default. Android's bottom-nav hosts are created eagerly at onCreate, so this decides whether they visit, not whether they exist.
161 |
# File 'lib/everywhere/config/native_mobile.rb', line 161 def native_android_lazy_load_tabs = native_lazy_load("android") |
#native_android_package_errors ⇒ Object
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/everywhere/config/native_mobile.rb', line 203 def native_android_package_errors raw = native_android["packages"] return [] if raw.nil? return ["native.android.packages must be a list"] unless raw.is_a?(Array) raw.each_with_index.filter_map do |entry, i| at = "native.android.packages[#{i}]" next "#{at} must be a Maven coordinate string like \"com.airbnb.android:lottie:6.4.0\"" unless entry.is_a?(String) coordinate = entry.strip next if coordinate.match?(MAVEN_COORDINATE) "#{at} #{coordinate.inspect} is not a Maven coordinate — " \ "\"group:artifact:version\" (letters, digits, . _ -)" end end |
#native_android_packages ⇒ Object
Third-party dependencies for native/android code, as Gradle sees them:
plain Maven coordinates. There is no SPM-style url + requirement split —
a coordinate already carries group, artifact and version — so this
accessor returns strings, one per implementation(…) line the builder
writes into native-packages.gradle.kts. Assumes the config validated
clean (see native_android_package_errors).
169 170 171 172 173 174 175 176 |
# File 'lib/everywhere/config/native_mobile.rb', line 169 def native_android_packages Array(native_android["packages"]).filter_map do |entry| next unless entry.is_a?(String) coordinate = entry.strip coordinate unless coordinate.empty? end end |
#native_android_packages? ⇒ Boolean
178 |
# File 'lib/everywhere/config/native_mobile.rb', line 178 def native_android_packages? = !native_android_packages.empty? |
#native_android_screens ⇒ Object
148 |
# File 'lib/everywhere/config/native_mobile.rb', line 148 def native_android_screens = native_screens("android") |
#native_android_splash ⇒ Object
150 |
# File 'lib/everywhere/config/native_mobile.rb', line 150 def native_android_splash = native_splash("android") |
#native_android_splash_min_seconds ⇒ Object
Same contract as the iOS splash minimum, and the same clamp: the shell gives up waiting at 8s either way, so a larger number here would only promise something the shell won't honor.
155 |
# File 'lib/everywhere/config/native_mobile.rb', line 155 def native_android_splash_min_seconds = native_splash_seconds("android") |
#native_ios ⇒ Object
Native code extensions ("supernative"): Swift the app repo carries in
native/ios/ that every build compiles into the shell, plus this
declaration of what to hook up. Build-time only — stores forbid
downloading native code, so extensions ship with the app binary.
native:
ios:
components: [ChartComponent] # BridgeComponent subclasses to register
screens: # path rules with view_controller: <id>
map: MapScreen # SwiftUI View or UIViewController, init(url:)
splash: LaunchSplash # SwiftUI View or UIViewController, init()
splash_min_seconds: 1.0 # how long the custom splash stays up at minimum
lazy_load_tabs: true # defer each tab's first visit until it's selected
19 |
# File 'lib/everywhere/config/native_mobile.rb', line 19 def native_ios = native_section("ios") |
#native_ios? ⇒ Boolean
76 |
# File 'lib/everywhere/config/native_mobile.rb', line 76 def native_ios? = native_declared?("ios") |
#native_ios_components ⇒ Object
21 |
# File 'lib/everywhere/config/native_mobile.rb', line 21 def native_ios_components = native_components("ios") |
#native_ios_errors ⇒ Object
84 |
# File 'lib/everywhere/config/native_mobile.rb', line 84 def native_ios_errors = native_type_errors("ios") + native_ios_package_errors |
#native_ios_lazy_load_tabs ⇒ Object
Whether the native tab bar defers each non-selected tab's initial visit
until the tab is first selected (Hotwire Native lazyLoadTabs). Tri-state
so apps can opt in or opt out explicitly: true lazy-loads, false loads
every tab up front, and an absent key leaves the shell on its default
(eager).
37 |
# File 'lib/everywhere/config/native_mobile.rb', line 37 def native_ios_lazy_load_tabs = native_lazy_load("ios") |
#native_ios_package_errors ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/everywhere/config/native_mobile.rb', line 92 def native_ios_package_errors raw = native_ios["packages"] return [] if raw.nil? return ["native.ios.packages must be a list"] unless raw.is_a?(Array) raw.each_with_index.flat_map do |entry, i| at = "native.ios.packages[#{i}]" next ["#{at} must be a mapping with a url:"] unless entry.is_a?(Hash) errors = [] url = entry["url"].to_s.strip if url.empty? errors << "#{at}.url is required" elsif !url.match?(PACKAGE_URL) errors << "#{at}.url #{url.inspect} must be an https:// or git@ URL" end present = PACKAGE_REQUIREMENT_KEYS.select { |k| entry.key?(k) && !entry[k].to_s.strip.empty? } if present.empty? errors << "#{at} needs one version requirement (#{PACKAGE_REQUIREMENT_KEYS.join(" / ")})" elsif present.length > 1 errors << "#{at} sets conflicting requirements (#{present.join(", ")}) — pick one" elsif %w[from exact].include?(present.first) && !entry[present.first].to_s.strip.match?(PACKAGE_VERSION) errors << "#{at}.#{present.first} #{entry[present.first].to_s.inspect} must be a version like \"1.2.0\"" end Array(entry["products"]).each do |product| next if product.to_s.match?(SWIFT_TYPE_NAME) errors << "#{at}.products: #{product.to_s.inspect} is not a module name (letters, digits, _)" end errors end end |
#native_ios_packages ⇒ Object
54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/everywhere/config/native_mobile.rb', line 54 def native_ios_packages Array(native_ios["packages"]).filter_map do |entry| next unless entry.is_a?(Hash) url = entry["url"].to_s.strip kind = PACKAGE_REQUIREMENT_KEYS.find { |k| entry.key?(k) && !entry[k].to_s.strip.empty? } products = Array(entry["products"]).map { |p| p.to_s.strip }.reject(&:empty?) products = [package_identity(url)] if products.empty? { "url" => url, "requirement" => (kind && { "kind" => kind, "value" => entry[kind].to_s.strip }), "products" => products } end end |
#native_ios_packages? ⇒ Boolean
69 |
# File 'lib/everywhere/config/native_mobile.rb', line 69 def native_ios_packages? = !native_ios_packages.empty? |
#native_ios_screens ⇒ Object
23 |
# File 'lib/everywhere/config/native_mobile.rb', line 23 def native_ios_screens = native_screens("ios") |
#native_ios_splash ⇒ Object
25 |
# File 'lib/everywhere/config/native_mobile.rb', line 25 def native_ios_splash = native_splash("ios") |
#native_ios_splash_min_seconds ⇒ Object
Minimum seconds a custom splash stays on screen (default 1.0 shell-side; a fast server would otherwise dismiss a branded splash in a ~50ms flash). Clamped to the shell's 8s give-up timeout.
30 |
# File 'lib/everywhere/config/native_mobile.rb', line 30 def native_ios_splash_min_seconds = native_splash_seconds("ios") |
#package_identity(url) ⇒ Object
SPM's package identity: the URL's last path segment without a .git suffix
(github.com/simibac/ConfettiSwiftUI → "ConfettiSwiftUI"). Used as the
default product name and as the package: key in .product(name:package:).
74 |
# File 'lib/everywhere/config/native_mobile.rb', line 74 def package_identity(url) = File.basename(url.to_s.sub(%r{/+\z}, "")).sub(/\.git\z/, "") |