Class: Everywhere::Config
- Inherits:
-
Object
- Object
- Everywhere::Config
- Defined in:
- lib/everywhere/config.rb
Overview
Loads config/everywhere.yml:
app:
name: My Really Awesome App
entry_path: /dashboard # initial url on app launch
appearance:
tint_color: "#CC342D" # a hex string...
background_color: # ...or split by system theme
light: "#FAF9F7"
dark: "#1C1B1A"
Colors always normalize to { "light" => ..., "dark" => ... }.
Constant Summary collapse
- FILE =
File.join("config", "everywhere.yml")
- PERMITTED_YAML_CLASSES =
Dates, times and symbols are permitted rather than fatal: an unquoted
version: 2026-07-25ormode: :remoteis a typo of a string, and every value here is read as one anyway — dying on it would be theatre. [Date, Time, Symbol].freeze
- DEFAULT_TAB_ICONS =
{ "ios" => "circle", "android" => "circle" }.freeze
- MOBILE_PATH_RULES =
The Hotwire Native path-configuration rules every RubyEverywhere mobile shell starts from. Single source of truth: the builder bakes this into the app bundle and MobileConfigEndpoint serves it live.
[ { "patterns" => [".*"], "properties" => { "context" => "default", "pull_to_refresh_enabled" => true } }, { "patterns" => ["/new$", "/edit$"], "properties" => { "context" => "modal", "pull_to_refresh_enabled" => false } } ].freeze
- MOBILE_PERMISSIONS =
Native permissions the app declares (top-level
permissions:). Only declared permissions can be requested at runtime — the shell answersundeclaredfor everything else, so an app that doesn't need a permission can never prompt for it. For camera and location the value is the user-facing usage string iOS shows in the permission prompt (the "why"); it's mandatory there because requesting without one crashes the app. Notifications need no string — declare withtrue.permissions: notifications: true camera: "Scan QR codes to pair devices." location: ios: "Find build agents near you." biometrics: "Unlock your account with Face ID."Returns { name => { "ios" => usage-or-nil, ... } }.
%w[notifications camera location biometrics].freeze
- IOS_USAGE_KEYS =
Info.plist usage-description keys per permission. Presence here makes the usage string mandatory on iOS.
{ "camera" => "NSCameraUsageDescription", "location" => "NSLocationWhenInUseUsageDescription", "biometrics" => "NSFaceIDUsageDescription" }.freeze
- ANDROID_PERMISSIONS =
Manifest
names per permission, stamped into src/stamped/AndroidManifest.xml. Deliberately not the mirror of IOS_USAGE_KEYS: presence here mandates nothing, because Android has no Info.plist-style contract — a runtime request without a rationale string prompts normally instead of terminating the process, and the rationale is a dialog the app draws itself, not a manifest value. So the map only answers "which manifest entry does this permission need", and every declared name has one. ACCESS_FINE_LOCATION implies ACCESS_COARSE_LOCATION on API 31+ only when both are declared, but the shell asks for precise location, so the fine permission alone is the honest declaration.
{ "notifications" => "android.permission.POST_NOTIFICATIONS", "camera" => "android.permission.CAMERA", "location" => "android.permission.ACCESS_FINE_LOCATION", "biometrics" => "android.permission.USE_BIOMETRIC" }.freeze
- 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"- RUST_FN_NAME =
Everything here is interpolated into generated Rust and TOML, so names are validated hard — same reasoning as MAVEN_COORDINATE above. A command name becomes a path segment in a generate_handler! list; a crate name and its features become bare TOML keys and quoted strings.
/\A[a-z_][a-z0-9_]*\z/- CRATE_NAME =
/\A[a-zA-Z0-9_][a-zA-Z0-9_-]*\z/- CRATE_VERSION =
Cargo's requirement syntax, loosely: ^1.2, ~1.2, >=1.0, 1.*, 1.2.3-beta.1. Deliberately no spaces and no commas — a multi-requirement string would need quoting rules we don't want to own, and cargo reports anything subtler at build time.
/\A[\^~<>=]{0,2}\d+(\.\d+){0,2}(\.\*)?(-[0-9A-Za-z.-]+)?\z/- CRATE_FEATURE =
/\A[A-Za-z0-9_][A-Za-z0-9_+-]*\z/- CRATE_GIT_URL =
Where a git dependency may point. Same rule the SPM packages use.
PACKAGE_URL- DEFAULT_OAUTH_PATHS =
Paths that begin a third-party auth flow, as regex source strings matched against the request path. Declaring
auth:at all opts in, defaulting to OmniAuth's/auth/…convention. ["^/auth/"].freeze
- URL_SCHEME =
The custom URL scheme ASWebAuthenticationSession returns through. Defaults to the iOS bundle id — the convention, and already unique per app. Only the characters RFC 3986 allows in a scheme.
/\A[a-zA-Z][a-zA-Z0-9+.-]*\z/- WINDOW_TITLE_BARS =
Desktop window chrome. Desktop-only: on mobile the OS owns the frame, so mobile builds ignore this section entirely.
window: title_bar: overlay # decorated (default) | overlay | frameless title: false # draw the title text (default true) size: [1100, 750] # initial inner size min_size: [600, 400] resizable: true drag_height: 28 # top strip that drags the window; 0 turns it offEvery reader returns nil when the key is absent so
to_shell_hashdrops it and the shell keeps its own default — there is exactly one place each default lives, and it's the Rust side.overlayis the macOS "traffic lights floating over your content" look (TitleBarStyle::Overlay + hidden title). Windows and Linux have no such style, so the shell falls back to frameless there and the page draws its own controls via Everywhere.window. %w[decorated overlay frameless].freeze
Instance Attribute Summary collapse
-
#root ⇒ Object
readonly
Returns the value of attribute root.
Class Method Summary collapse
-
.channel_of(target) ⇒ Object
A target may carry a ":
" suffix ("ios-arm64:testflight") that selects the distribution channel for THAT target. - .load(root = Dir.pwd) ⇒ Object
-
.os_of(target) ⇒ Object
Every target (os-arch string) resolves to a single platform (its os), and a platform may override the app-level identity/version keys — see
platforms:below.
Instance Method Summary collapse
-
#android_manifest_permissions ⇒ Object
The manifest permissions an Android build declares, in declaration order.
-
#android_screen_properties(properties) ⇒ Object
Hotwire Native iOS picks a native screen with
view_controller: <id>; Android picks one withuri: hotwire://fragment/<id>, matched against the Fragment's own @HotwireDestinationDeepLink annotation. -
#apple_app_site_association ⇒ Object
The apple-app-site-association document (a Hash), or nil when no Apple app ids are configured.
- #apple_app_site_association_json ⇒ Object
-
#asset_links ⇒ Object
The Digital Asset Links document (an Array), or nil without an Android app.
- #asset_links_json ⇒ Object
-
#auth ⇒ Object
Third-party sign-in (Sign in with Apple / Google / GitHub / anything OmniAuth speaks).
-
#auth_cookie?(name) ⇒ Boolean
Cookie names to carry from the auth browser back into the app's web view, filtered by an optional allow/deny list.
- #auth_errors ⇒ Object
- #auth_scheme ⇒ Object
-
#auth_shell_hash ⇒ Object
The auth subset the shell needs: which visits to divert into the auth session, and the scheme to bring the answer back through.
-
#auth_token_ttl ⇒ Object
How long a minted handoff token stays valid.
- #background_color ⇒ Object
-
#build ⇒ Object
The
build:section — durable build knobs that were onceevery buildflags (ruby, targets, capabilities). -
#build_capabilities ⇒ Object
Desktop OS-integration capabilities the app declares (
build.capabilities), as a plain list. -
#build_ruby ⇒ Object
Ruby version to package.
-
#build_target_specs ⇒ Object
The raw
build.targetsentries, suffixes intact ("ios-arm64:testflight"). -
#bundle_id(target: nil) ⇒ Object
Reverse-DNS identifier: macOS bundle id, and the name of the per-user app-data directory on every platform.
-
#deep_linking ⇒ Object
Deep linking / universal links.
- #deep_linking? ⇒ Boolean
- #deep_linking_android ⇒ Object
- #deep_linking_android? ⇒ Boolean
- #deep_linking_android_fingerprints ⇒ Object
- #deep_linking_android_package ⇒ Object
- #deep_linking_apple? ⇒ Boolean
-
#deep_linking_apple_app_ids ⇒ Object
The iOS/tvOS/...
-
#deep_linking_domains ⇒ Object
Domains for the iOS Associated Domains entitlement (applinks:
) and the shell's universal-link host allowlist: the remote host plus any extra domains:. -
#deep_linking_paths ⇒ Object
URL path patterns that open the app.
- #entry_path ⇒ Object
-
#icon ⇒ Object
App icon source: a PNG (ideally square, 1024px+).
-
#initialize(data, root) ⇒ Config
constructor
A new instance of Config.
-
#menu ⇒ Object
Custom native menu items (macOS app menu).
-
#mobile_rules(os = nil) ⇒ Object
App-declared Hotwire Native path-configuration rules (everywhere.yml top-level
rules:), appended after MOBILE_PATH_RULES — later rules win per property, so apps can make routes modal, disable pull-to-refresh, or set any other Hotwire path property without touching native code:. -
#mode ⇒ Object
"local" — the app is tebako-pressed and runs on-device (default) "remote" — thin shell around an already-deployed app: no press, no sidecar.
- #name(target: nil) ⇒ Object
-
#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_desktop ⇒ Object
The desktop half of "supernative": Rust the app repo carries in native/desktop/ that
every buildcompiles into the Tauri shell. - #native_desktop? ⇒ Boolean
- #native_desktop_commands ⇒ Object
- #native_desktop_crate_errors ⇒ Object
-
#native_desktop_crates ⇒ Object
Cargo dependencies, normalized to { "name" => .....table } so the generator has one shape to write.
- #native_desktop_errors ⇒ Object
- #native_desktop_setup? ⇒ Boolean
-
#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).
-
#native_lazy_load_tabs(target: nil) ⇒ Object
The two
native:knobs that ship inside everywhere.json rather than being compiled in, resolved for the platform being stamped. - #native_splash_min_seconds(target: nil) ⇒ Object
- #oauth? ⇒ Boolean
-
#oauth_path?(path) ⇒ Boolean
Whether a request path starts a provider flow.
- #oauth_paths ⇒ Object
-
#package_identity(url) ⇒ Object
SPM's package identity: the URL's last path segment without a .git suffix (github.com/simibac/ConfettiSwiftUI → "ConfettiSwiftUI").
-
#path_configuration_hash(os, tabs: nil) ⇒ Object
The full path-configuration document for one mobile platform — rules plus our settings (tabs live in settings, per Hotwire Native convention).
- #path_configuration_json(os, tabs: nil) ⇒ Object
-
#permission_errors(os) ⇒ Object
Problems with the permissions declaration for one platform, as human-readable strings — unknown names, and camera/location missing the mandatory usage string.
- #permissions ⇒ Object
- #remote? ⇒ Boolean
-
#remote_instances? ⇒ Boolean
Multi-instance apps (
remote.instances: true): the mobile shell boots into remote.url — a hosted instance-picker page — and lets that page re-root the app onto a chosen instance via Everywhere.instance.set, persisted across launches until Everywhere.instance.clear. - #remote_url ⇒ Object
-
#splash ⇒ Object
Optional custom splash page (HTML file, path relative to the app root).
-
#tabs ⇒ Object
Mobile tab bar, platform-neutral.
-
#tabs_for(os) ⇒ Object
Tabs with the icon resolved for one platform: icons.
, then the shared icon, then a safe default. -
#targets ⇒ Object
Build targets as BARE os-arch strings — any ":
" suffix is stripped. - #tint_color ⇒ Object
-
#to_shell_hash(target: nil) ⇒ Object
The subset the shell needs, shipped as JSON (env var in dev, Resources/everywhere.json inside a bundled .app).
- #to_shell_json(target: nil) ⇒ Object
-
#tray ⇒ Object
System tray: same entry shape as
menu:plusaction: quit | show. -
#updates ⇒ Object
The
updates:section — self-hosted auto-updates. -
#updates_auto ⇒ Object
off — never check; check — check + notify (default); download — also fetch/verify in the background; install — silent auto-update.
- #updates_channel ⇒ Object
- #updates_interval ⇒ Object
- #updates_public_key ⇒ Object
- #updates_s3 ⇒ Object
-
#updates_shell_hash ⇒ Object
The client subset for the shell; nil (and thus absent from everywhere.json) until both url and public_key are configured — an unsigned update feed is not a thing.
- #updates_url ⇒ Object
-
#version(target: nil) ⇒ Object
Marketing/display version of the app (CFBundleShortVersionString, and the
versionrecorded in the build receipt). - #window ⇒ Object
-
#window_drag_height ⇒ Object
How much of the top of the page drags the window, in CSS pixels.
- #window_errors ⇒ Object
- #window_min_size ⇒ Object
- #window_resizable ⇒ Object
-
#window_shell_hash ⇒ Object
The window subset the shell needs.
- #window_size ⇒ Object
-
#window_title ⇒ Object
Tri-state, like native_ios_lazy_load_tabs: an absent key leaves the shell on its default rather than forcing one.
- #window_title_bar ⇒ Object
Constructor Details
Instance Attribute Details
#root ⇒ Object (readonly)
Returns the value of attribute root.
230 231 232 |
# File 'lib/everywhere/config.rb', line 230 def root @root end |
Class Method Details
.channel_of(target) ⇒ Object
A target may carry a ":
245 246 247 248 |
# File 'lib/everywhere/config.rb', line 245 def self.channel_of(target) suffix = target.to_s.split(":", 2)[1] suffix unless suffix.nil? || suffix.empty? end |
.load(root = Dir.pwd) ⇒ Object
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/everywhere/config.rb', line 207 def self.load(root = Dir.pwd) path = File.join(root, FILE) return new({}, root) unless File.exist?(path) data = YAML.safe_load_file(path, aliases: true, permitted_classes: PERMITTED_YAML_CLASSES) data = {} if data.nil? || data == false unless data.is_a?(Hash) # Raise, never die!: this also runs inside the packaged app's request # middleware, where a SystemExit would take the whole server down — # runtime callers rescue and keep the last good config, and the CLI's # top-level handler renders an Error just as cleanly. raise Everywhere::Error, "#{UI.short_path(path)} must be a mapping of sections (app:, build:, …), not a #{data.class}" end new(data, root) rescue Psych::Exception => e # Psych prefixes syntax errors with the absolute path; the line/column # tail is the useful half, and we've already named the file. detail = e..sub(/\A\(#{Regexp.escape(path)}\):\s*/, "") raise Everywhere::Error, "#{UI.short_path(path)} isn't valid YAML: #{detail}" end |
.os_of(target) ⇒ Object
Every target (os-arch string) resolves to a single platform (its os), and
a platform may override the app-level identity/version keys — see
platforms: below. Pass target: to any of these accessors to apply the
matching override; omit it for the shared app-level value.
241 |
# File 'lib/everywhere/config.rb', line 241 def self.os_of(target) = target.to_s.split("-", 2).first |
Instance Method Details
#android_manifest_permissions ⇒ Object
The manifest permissions an Android build declares, in declaration order. Only known names map to anything; permission_errors("android") has already failed the build on the rest by the time the builder asks.
498 499 500 |
# File 'lib/everywhere/config.rb', line 498 def .keys.filter_map { |name| ANDROID_PERMISSIONS[name] }.uniq end |
#android_screen_properties(properties) ⇒ Object
Hotwire Native iOS picks a native screen with view_controller: <id>;
Android picks one with uri: hotwire://fragment/<id>, matched against the
Fragment's own @HotwireDestinationDeepLink annotation. Same rule, same
identifier, different property name — so the Android document derives the
uri rather than making apps write the route twice and keep the two in
sync by hand.
Derived only for ids declared under native.android.screens: an id that
names an iOS-only screen must fall through to the web fragment, because a
uri pointing at a Fragment this build doesn't contain resolves to
nothing and the visit dead-ends. An explicit uri: always wins — that's
the escape hatch for a screen whose annotation says something else.
565 566 567 568 569 570 571 |
# File 'lib/everywhere/config.rb', line 565 def android_screen_properties(properties) id = properties["view_controller"] return properties if id.nil? || properties.key?("uri") return properties unless native_android_screens.key?(id.to_s) properties.merge("uri" => "hotwire://fragment/#{id}") end |
#apple_app_site_association ⇒ Object
The apple-app-site-association document (a Hash), or nil when no Apple app
ids are configured. applinks for universal links; webcredentials so
associated-domains password autofill / Face ID credentials work too.
1316 1317 1318 1319 1320 1321 1322 1323 1324 |
# File 'lib/everywhere/config.rb', line 1316 def apple_app_site_association ids = deep_linking_apple_app_ids return nil if ids.empty? { "applinks" => { "details" => [{ "appIDs" => ids, "components" => deep_linking_paths.map { |p| { "/" => p } } }] }, "webcredentials" => { "apps" => ids } } end |
#apple_app_site_association_json ⇒ Object
1326 1327 1328 1329 1330 |
# File 'lib/everywhere/config.rb', line 1326 def apple_app_site_association_json require "json" doc = apple_app_site_association or return nil JSON.generate(doc) end |
#asset_links ⇒ Object
The Digital Asset Links document (an Array), or nil without an Android app.
1333 1334 1335 1336 1337 1338 1339 1340 |
# File 'lib/everywhere/config.rb', line 1333 def asset_links return nil unless deep_linking_android? [{ "relation" => ["delegate_permission/common.handle_all_urls"], "target" => { "namespace" => "android_app", "package_name" => deep_linking_android_package, "sha256_cert_fingerprints" => deep_linking_android_fingerprints } }] end |
#asset_links_json ⇒ Object
1342 1343 1344 1345 1346 |
# File 'lib/everywhere/config.rb', line 1342 def asset_links_json require "json" doc = asset_links or return nil JSON.generate(doc) end |
#auth ⇒ Object
Third-party sign-in (Sign in with Apple / Google / GitHub / anything OmniAuth speaks). Providers refuse to run — or run badly — inside an app's web view, so the mobile shell hands these paths to an ASWebAuthenticationSession instead, and the gem bridges the resulting session back into the app (see AuthHandoff).
auth:
oauth_paths: # paths that begin a provider flow
- ^/auth/ # unanchored regexes, like `rules:`
scheme: com.example.app # callback scheme (default: the iOS bundle id)
cookies: # which cookies cross back into the app
except: ["_ga"] # (default: all of them)
The app keeps its own auth: auth: declares nothing about providers,
only which paths the shell must not open in its web view.
1035 |
# File 'lib/everywhere/config.rb', line 1035 def auth = @data.fetch("auth", nil) || {} |
#auth_cookie?(name) ⇒ Boolean
Cookie names to carry from the auth browser back into the app's web view, filtered by an optional allow/deny list. Everything the flow set is carried by default: the gem can't know which cookie an app's auth library signs its session with.
1074 1075 1076 1077 1078 1079 1080 1081 1082 |
# File 'lib/everywhere/config.rb', line 1074 def (name) = auth["cookies"] return true unless .is_a?(Hash) only = Array(["only"]).map(&:to_s) return only.include?(name.to_s) unless only.empty? !Array(["except"]).map(&:to_s).include?(name.to_s) end |
#auth_errors ⇒ Object
1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 |
# File 'lib/everywhere/config.rb', line 1088 def auth_errors return [] unless @data.key?("auth") errors = oauth_paths.filter_map do |pattern| Regexp.new(pattern) nil rescue RegexpError => e "auth.oauth_paths: #{pattern.inspect} is not a valid pattern (#{e.})" end unless auth_scheme.match?(URL_SCHEME) errors << "auth.scheme #{auth_scheme.inspect} is not a URL scheme (letters, digits, +, -, .)" end errors end |
#auth_scheme ⇒ Object
1065 1066 1067 1068 |
# File 'lib/everywhere/config.rb', line 1065 def auth_scheme explicit = auth["scheme"].to_s.strip explicit.empty? ? bundle_id(target: "ios") : explicit end |
#auth_shell_hash ⇒ Object
The auth subset the shell needs: which visits to divert into the auth
session, and the scheme to bring the answer back through. nil (absent from
everywhere.json) when the app declares no auth: — a shell that can't
divert anything is a shell with no new surface.
1108 1109 1110 1111 1112 |
# File 'lib/everywhere/config.rb', line 1108 def auth_shell_hash return nil unless oauth? { "oauth_paths" => oauth_paths, "scheme" => auth_scheme } end |
#auth_token_ttl ⇒ Object
How long a minted handoff token stays valid. It travels device-locally (browser sheet → shell → web view), so seconds are plenty.
1086 |
# File 'lib/everywhere/config.rb', line 1086 def auth_token_ttl = [(auth["token_ttl"] || 60).to_i, 5].max |
#background_color ⇒ Object
382 383 384 |
# File 'lib/everywhere/config.rb', line 382 def background_color normalize_color(appearance["background_color"]) end |
#build ⇒ Object
The build: section — durable build knobs that were once every build
flags (ruby, targets, capabilities). CLI flags still override per-run.
See platform/docs/build-engine.md §2.
298 |
# File 'lib/everywhere/config.rb', line 298 def build = @data.fetch("build", nil) || {} |
#build_capabilities ⇒ Object
Desktop OS-integration capabilities the app declares (build.capabilities),
as a plain list. Deliberately not called permissions: the top-level
permissions: below is the mobile prompt declaration — a different shape
for a different consumer — and while both were spelled "permissions" this
accessor was silently redefined by that one and could never be read.
308 |
# File 'lib/everywhere/config.rb', line 308 def build_capabilities = Array(build["capabilities"]) |
#build_ruby ⇒ Object
Ruby version to package. nil here means "let the CLI decide its default".
301 |
# File 'lib/everywhere/config.rb', line 301 def build_ruby = build["ruby"] |
#build_target_specs ⇒ Object
The raw build.targets entries, suffixes intact ("ios-arm64:testflight").
318 |
# File 'lib/everywhere/config.rb', line 318 def build_target_specs = Array(build["targets"]) |
#bundle_id(target: nil) ⇒ Object
Reverse-DNS identifier: macOS bundle id, and the name of the per-user
app-data directory on every platform. Set it explicitly and never change
it — renaming moves users' data. A platform may override it (e.g. the App
Store often wants com.example.app.ios).
258 259 260 261 |
# File 'lib/everywhere/config.rb', line 258 def bundle_id(target: nil) resolved(target)["bundle_id"] || "com.rubyeverywhere.#{name(target: target).downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")}" end |
#deep_linking ⇒ Object
Deep linking / universal links. Declares the app's association with its web domains so the OS can hand matching URLs to the app instead of the browser. The gem serves the two well-known association files this needs — /.well-known/apple-app-site-association and /.well-known/assetlinks.json — generated from this config (Everywhere::Engine in Rails; MobileConfigEndpoint for Sinatra/Hanami). The iOS builder stamps the matching Associated Domains entitlement, and the shell routes an incoming link to its path.
deep_linking:
team_id: ABCDE12345 # derives the iOS app id from bundle_id
# or set app ids explicitly:
# apple_app_id: ABCDE12345.com.example.app
# apple_app_ids: ["ABCDE12345.com.example.app"]
paths: ["/*"] # which paths open the app (default: all)
domains: ["www.example.com"] # extra applinks: domains (remote host is implicit)
android:
package: com.example.app
sha256_cert_fingerprints: ["AB:CD:EF:..."]
1266 |
# File 'lib/everywhere/config.rb', line 1266 def deep_linking = @data.fetch("deep_linking", nil) || {} |
#deep_linking? ⇒ Boolean
1296 |
# File 'lib/everywhere/config.rb', line 1296 def deep_linking? = deep_linking_apple? || deep_linking_android? |
#deep_linking_android ⇒ Object
1285 |
# File 'lib/everywhere/config.rb', line 1285 def deep_linking_android = deep_linking["android"].is_a?(Hash) ? deep_linking["android"] : {} |
#deep_linking_android? ⇒ Boolean
1292 |
# File 'lib/everywhere/config.rb', line 1292 def deep_linking_android? = !deep_linking_android_package.to_s.empty? && !deep_linking_android_fingerprints.empty? |
#deep_linking_android_fingerprints ⇒ Object
1288 1289 1290 |
# File 'lib/everywhere/config.rb', line 1288 def deep_linking_android_fingerprints Array(deep_linking_android["sha256_cert_fingerprints"]).map { |f| f.to_s.strip }.reject(&:empty?) end |
#deep_linking_android_package ⇒ Object
1286 |
# File 'lib/everywhere/config.rb', line 1286 def deep_linking_android_package = deep_linking_android["package"]&.to_s |
#deep_linking_apple? ⇒ Boolean
1294 |
# File 'lib/everywhere/config.rb', line 1294 def deep_linking_apple? = !deep_linking_apple_app_ids.empty? |
#deep_linking_apple_app_ids ⇒ Object
The iOS/tvOS/... app identifiers (TeamID.bundleID) the association file advertises. Explicit ids win; otherwise team_id + the app's iOS bundle id.
1270 1271 1272 1273 1274 1275 1276 |
# File 'lib/everywhere/config.rb', line 1270 def deep_linking_apple_app_ids explicit = Array(deep_linking["apple_app_ids"]) + [deep_linking["apple_app_id"]].compact return explicit.map(&:to_s).uniq unless explicit.empty? team = deep_linking["team_id"].to_s.strip team.empty? ? [] : ["#{team}.#{bundle_id(target: "ios")}"] end |
#deep_linking_domains ⇒ Object
Domains for the iOS Associated Domains entitlement (applinks:domains:. Bare hostnames (no scheme, no path).
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 |
# File 'lib/everywhere/config.rb', line 1301 def deep_linking_domains hosts = [] if remote_url require "uri" hosts << URI(remote_url).host end hosts += Array(deep_linking["domains"]).map { |d| d.to_s.sub(%r{\Ahttps?://}, "").sub(%r{/.*\z}, "") } hosts.compact.reject(&:empty?).uniq rescue URI::InvalidURIError Array(deep_linking["domains"]).map(&:to_s).reject(&:empty?).uniq end |
#deep_linking_paths ⇒ Object
URL path patterns that open the app. Modern (iOS 14+) component form; defaults to every path.
1280 1281 1282 1283 |
# File 'lib/everywhere/config.rb', line 1280 def deep_linking_paths paths = Array(deep_linking["paths"]).map(&:to_s).reject(&:empty?) paths.empty? ? ["*"] : paths end |
#entry_path ⇒ Object
282 283 284 285 |
# File 'lib/everywhere/config.rb', line 282 def entry_path path = app["entry_path"] || "/" path.start_with?("/") ? path : "/#{path}" end |
#icon ⇒ Object
App icon source: a PNG (ideally square, 1024px+). Explicit app.icon
path relative to the app root, or icon.png at the root by convention.
265 266 267 268 269 270 271 272 |
# File 'lib/everywhere/config.rb', line 265 def icon if (explicit = app["icon"]) File.(explicit, root) else default = File.join(root, "icon.png") File.exist?(default) ? default : nil end end |
#menu ⇒ Object
Custom native menu items (macOS app menu). Each entry: label + path (+ optional accelerator), or "separator". Clicks arrive in the page as "everywhere:menu" events and navigate via Turbo.
1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 |
# File 'lib/everywhere/config.rb', line 1117 def entries = @data["menu"] return [] unless entries.is_a?(Array) entries.filter_map do |item| if item == "separator" || (item.is_a?(Hash) && item["separator"]) { "separator" => true } elsif item.is_a?(Hash) && item["label"] && item["path"] { "label" => item["label"], "path" => item["path"], "accelerator" => item["accelerator"] }.compact end end end |
#mobile_rules(os = nil) ⇒ Object
App-declared Hotwire Native path-configuration rules (everywhere.yml
top-level rules:), appended after MOBILE_PATH_RULES — later rules win
per property, so apps can make routes modal, disable pull-to-refresh,
or set any other Hotwire path property without touching native code:
rules:
- patterns: ["/preferences$"]
properties:
context: default # not a modal, unlike other /edit routes
- patterns: ["/live/"]
properties:
pull_to_refresh_enabled: false
Native screens are selected by a different property on each platform, so
os decides how a rule's view_controller: is read (see
#android_screen_properties).
540 541 542 543 544 545 546 547 548 549 550 551 |
# File 'lib/everywhere/config.rb', line 540 def mobile_rules(os = nil) entries = @data["rules"] return [] unless entries.is_a?(Array) entries.filter_map do |rule| next unless rule.is_a?(Hash) && rule["patterns"].is_a?(Array) && rule["properties"].is_a?(Hash) properties = rule["properties"] properties = android_screen_properties(properties) if android_target?(os) { "patterns" => rule["patterns"].map(&:to_s), "properties" => properties } end end |
#mode ⇒ Object
"local" — the app is tebako-pressed and runs on-device (default) "remote" — thin shell around an already-deployed app: no press, no sidecar
322 323 324 |
# File 'lib/everywhere/config.rb', line 322 def mode app["mode"] || (remote_url ? "remote" : "local") end |
#name(target: nil) ⇒ Object
250 251 252 |
# File 'lib/everywhere/config.rb', line 250 def name(target: nil) resolved(target)["name"] || File.basename(File.(root)).split(/[-_]/).map(&:capitalize).join(" ") end |
#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"
754 755 756 757 |
# File 'lib/everywhere/config.rb', line 754 def native_android section = @data.dig("native", "android") section.is_a?(Hash) ? section : {} end |
#native_android? ⇒ Boolean
804 805 |
# File 'lib/everywhere/config.rb', line 804 def native_android? = !(native_android_components.empty? && native_android_screens.empty? && native_android_splash.nil?) |
#native_android_components ⇒ Object
759 |
# File 'lib/everywhere/config.rb', line 759 def native_android_components = Array(native_android["components"]).map(&:to_s) |
#native_android_errors ⇒ Object
813 814 815 816 817 818 819 820 821 822 823 824 825 |
# File 'lib/everywhere/config.rb', line 813 def native_android_errors types = native_android_components + native_android_screens.values + [native_android_splash].compact errors = types.reject { |t| t.match?(KOTLIN_TYPE_NAME) }.map do |t| "native.android: #{t.inspect} is not a Kotlin type name (letters, digits, _)" end errors += native_android_screens.keys.reject { |id| id.match?(SCREEN_IDENTIFIER) }.map do |id| "native.android.screens: identifier #{id.inspect} must match #{SCREEN_IDENTIFIER.inspect}" end raw = native_android["splash_min_seconds"] errors << "native.android.splash_min_seconds must be a number (seconds)" if raw && !raw.is_a?(Numeric) errors + native_android_icon_font_errors + native_android_package_errors end |
#native_android_icon_font ⇒ Object
881 882 883 884 |
# File 'lib/everywhere/config.rb', line 881 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.
890 891 892 |
# File 'lib/everywhere/config.rb', line 890 def native_android_icon_font_codepoints native_android_icon_font_file&.sub(/\.ttf\z/, ".codepoints") end |
#native_android_icon_font_errors ⇒ Object
904 905 906 907 908 909 |
# File 'lib/everywhere/config.rb', line 904 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.
897 898 899 900 |
# File 'lib/everywhere/config.rb', line 897 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/
887 |
# File 'lib/everywhere/config.rb', line 887 def native_android_icon_font_file = ANDROID_ICON_FONTS.dig(native_android_icon_font, "file") |
#native_android_icons? ⇒ Boolean
902 |
# File 'lib/everywhere/config.rb', line 902 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.
782 783 784 785 |
# File 'lib/everywhere/config.rb', line 782 def native_android_lazy_load_tabs value = native_android["lazy_load_tabs"] value if value == true || value == false end |
#native_android_package_errors ⇒ Object
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 |
# File 'lib/everywhere/config.rb', line 838 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).
793 794 795 796 797 798 799 800 |
# File 'lib/everywhere/config.rb', line 793 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
802 |
# File 'lib/everywhere/config.rb', line 802 def native_android_packages? = !native_android_packages.empty? |
#native_android_screens ⇒ Object
761 762 763 764 765 766 |
# File 'lib/everywhere/config.rb', line 761 def native_android_screens entries = native_android["screens"] return {} unless entries.is_a?(Hash) entries.to_h { |id, type| [id.to_s, type.to_s] } end |
#native_android_splash ⇒ Object
768 |
# File 'lib/everywhere/config.rb', line 768 def native_android_splash = native_android["splash"]&.to_s |
#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.
773 774 775 776 |
# File 'lib/everywhere/config.rb', line 773 def native_android_splash_min_seconds value = native_android["splash_min_seconds"] value.to_f.clamp(0.0, 8.0) if value.is_a?(Numeric) end |
#native_desktop ⇒ Object
The desktop half of "supernative": Rust the app repo carries in
native/desktop/ that every build compiles into the Tauri shell.
The shape differs from ios/android because desktop differs: the whole UI is a webview, so there are no native screens or splash views to register. What an app actually wants down here is the machine — a serial port, a USB device, an FFI library, a Tauri plugin — reached from the page.
native:
desktop:
commands: [scan_ports] # fns reachable from the page
setup: true # run native/desktop/setup.rs at boot
crates:
serialport: "4.3"
reqwest: { version: "0.12", features: [json] }
A command is a plain function, NOT a #[tauri::command] — Tauri's typed commands only arrive through invoke, and invoke is unavailable to our pages (they're served over http; see extension_host.rs in the shell). The signature is uniform:
pub fn scan_ports(app: &tauri::AppHandle, payload: serde_json::Value)
-> Result<serde_json::Value, String>
Declaring anything here has a real cost the mobile sections don't have: the shell stops being one prebuilt crate shared by every app and gets stamped and compiled per app (see Builders::Desktop). Apps that declare nothing keep the fast path.
939 940 941 942 |
# File 'lib/everywhere/config.rb', line 939 def native_desktop section = @data.dig("native", "desktop") section.is_a?(Hash) ? section : {} end |
#native_desktop? ⇒ Boolean
961 962 |
# File 'lib/everywhere/config.rb', line 961 def native_desktop? = !(native_desktop_commands.empty? && native_desktop_crates.empty?) || native_desktop_setup? |
#native_desktop_commands ⇒ Object
944 |
# File 'lib/everywhere/config.rb', line 944 def native_desktop_commands = Array(native_desktop["commands"]).map { |name| name.to_s.strip } |
#native_desktop_crate_errors ⇒ Object
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 |
# File 'lib/everywhere/config.rb', line 1005 def native_desktop_crate_errors raw = native_desktop["crates"] return [] if raw.nil? return ["native.desktop.crates must be a mapping of crate name to version"] unless raw.is_a?(Hash) raw.flat_map do |name, spec| at = "native.desktop.crates.#{name}" errors = [] unless name.to_s.strip.match?(CRATE_NAME) errors << "#{at}: #{name.to_s.inspect} is not a crate name (letters, digits, _, -)" end errors + crate_spec_errors(at, spec) end end |
#native_desktop_crates ⇒ Object
Cargo dependencies, normalized to { "name" => Everywhere::Config......table } so the generator has one shape to write. The short form (a bare version string) becomes { "version" => "4.3" }.
951 952 953 954 955 956 957 958 959 |
# File 'lib/everywhere/config.rb', line 951 def native_desktop_crates entries = native_desktop["crates"] return {} unless entries.is_a?(Hash) entries.to_h do |name, spec| table = spec.is_a?(Hash) ? spec : { "version" => spec.to_s } [name.to_s.strip, table] end end |
#native_desktop_errors ⇒ Object
979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 |
# File 'lib/everywhere/config.rb', line 979 def native_desktop_errors raw = @data.dig("native", "desktop") return [] if raw.nil? return ["native.desktop must be a mapping"] unless raw.is_a?(Hash) errors = [] commands = raw["commands"] if !commands.nil? && !commands.is_a?(Array) errors << "native.desktop.commands must be a list of function names" else errors += native_desktop_commands.reject { |name| name.match?(RUST_FN_NAME) }.map do |name| "native.desktop.commands: #{name.inspect} is not a Rust function name — " \ "snake_case (letters, digits, _), matching the fn in native/desktop/" end duplicates = native_desktop_commands.tally.select { |_name, count| count > 1 }.keys errors += duplicates.map do |name| "native.desktop.commands: #{name.inspect} is listed twice" end end setup = raw["setup"] errors << "native.desktop.setup must be true or false" unless setup.nil? || boolean_or_nil(setup) == setup errors + native_desktop_crate_errors end |
#native_desktop_setup? ⇒ Boolean
946 |
# File 'lib/everywhere/config.rb', line 946 def native_desktop_setup? = native_desktop["setup"] == true |
#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
603 604 605 606 |
# File 'lib/everywhere/config.rb', line 603 def native_ios section = @data.dig("native", "ios") section.is_a?(Hash) ? section : {} end |
#native_ios? ⇒ Boolean
674 |
# File 'lib/everywhere/config.rb', line 674 def native_ios? = !(native_ios_components.empty? && native_ios_screens.empty? && native_ios_splash.nil?) |
#native_ios_components ⇒ Object
608 |
# File 'lib/everywhere/config.rb', line 608 def native_ios_components = Array(native_ios["components"]).map(&:to_s) |
#native_ios_errors ⇒ Object
682 683 684 685 686 687 688 689 690 691 692 693 694 |
# File 'lib/everywhere/config.rb', line 682 def native_ios_errors types = native_ios_components + native_ios_screens.values + [native_ios_splash].compact errors = types.reject { |t| t.match?(SWIFT_TYPE_NAME) }.map do |t| "native.ios: #{t.inspect} is not a Swift type name (letters, digits, _)" end errors += native_ios_screens.keys.reject { |id| id.match?(SCREEN_IDENTIFIER) }.map do |id| "native.ios.screens: identifier #{id.inspect} must match #{SCREEN_IDENTIFIER.inspect}" end raw = native_ios["splash_min_seconds"] errors << "native.ios.splash_min_seconds must be a number (seconds)" if raw && !raw.is_a?(Numeric) errors + native_ios_package_errors end |
#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).
632 633 634 635 |
# File 'lib/everywhere/config.rb', line 632 def native_ios_lazy_load_tabs value = native_ios["lazy_load_tabs"] value if value == true || value == false end |
#native_ios_package_errors ⇒ Object
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 |
# File 'lib/everywhere/config.rb', line 702 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
652 653 654 655 656 657 658 659 660 661 662 663 664 665 |
# File 'lib/everywhere/config.rb', line 652 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
667 |
# File 'lib/everywhere/config.rb', line 667 def native_ios_packages? = !native_ios_packages.empty? |
#native_ios_screens ⇒ Object
610 611 612 613 614 615 |
# File 'lib/everywhere/config.rb', line 610 def native_ios_screens entries = native_ios["screens"] return {} unless entries.is_a?(Hash) entries.to_h { |id, type| [id.to_s, type.to_s] } end |
#native_ios_splash ⇒ Object
617 |
# File 'lib/everywhere/config.rb', line 617 def native_ios_splash = native_ios["splash"]&.to_s |
#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.
622 623 624 625 |
# File 'lib/everywhere/config.rb', line 622 def native_ios_splash_min_seconds value = native_ios["splash_min_seconds"] value.to_f.clamp(0.0, 8.0) if value.is_a?(Numeric) end |
#native_lazy_load_tabs(target: nil) ⇒ Object
The two native: knobs that ship inside everywhere.json rather than
being compiled in, resolved for the platform being stamped. They read the
target's own section: everywhere.json is written once per target, so an
Android build that inherited native.ios.lazy_load_tabs would silently
apply a decision the app made about a different shell — and the two
platforms load tabs differently enough that the answer legitimately
differs. Without a target (the desktop dev loop, and every caller written
before Android existed) the iOS answer stands, which is what those
callers have always gotten.
1357 1358 1359 |
# File 'lib/everywhere/config.rb', line 1357 def native_lazy_load_tabs(target: nil) android_target?(target) ? native_android_lazy_load_tabs : native_ios_lazy_load_tabs end |
#native_splash_min_seconds(target: nil) ⇒ Object
1361 1362 1363 |
# File 'lib/everywhere/config.rb', line 1361 def native_splash_min_seconds(target: nil) android_target?(target) ? native_android_splash_min_seconds : native_ios_splash_min_seconds end |
#oauth? ⇒ Boolean
1049 |
# File 'lib/everywhere/config.rb', line 1049 def oauth? = !oauth_paths.empty? |
#oauth_path?(path) ⇒ Boolean
Whether a request path starts a provider flow. Both halves of the system ask this — the middleware per request, the shell per visit proposal — so the patterns are compiled the same way on both sides (unanchored regex).
1054 1055 1056 1057 1058 |
# File 'lib/everywhere/config.rb', line 1054 def oauth_path?(path) oauth_paths.any? { |pattern| Regexp.new(pattern).match?(path.to_s) } rescue RegexpError false end |
#oauth_paths ⇒ Object
1042 1043 1044 1045 1046 1047 |
# File 'lib/everywhere/config.rb', line 1042 def oauth_paths return [] unless @data.key?("auth") paths = Array(auth["oauth_paths"]).map { |p| p.to_s.strip }.reject(&:empty?) paths.empty? ? DEFAULT_OAUTH_PATHS.dup : paths end |
#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:).
672 |
# File 'lib/everywhere/config.rb', line 672 def package_identity(url) = File.basename(url.to_s.sub(%r{/+\z}, "")).sub(/\.git\z/, "") |
#path_configuration_hash(os, tabs: nil) ⇒ Object
The full path-configuration document for one mobile platform — rules plus
our settings (tabs live in settings, per Hotwire Native convention).
Pass tabs: to override the resolved list (the mobile config endpoint
passes a per-request-filtered list; build-time stamping omits it and
bakes them all).
578 579 580 581 582 583 |
# File 'lib/everywhere/config.rb', line 578 def path_configuration_hash(os, tabs: nil) settings = {} platform_tabs = tabs || tabs_for(os) settings["tabs"] = platform_tabs unless platform_tabs.empty? { "settings" => settings, "rules" => MOBILE_PATH_RULES + mobile_rules(os) } end |
#path_configuration_json(os, tabs: nil) ⇒ Object
585 586 587 588 |
# File 'lib/everywhere/config.rb', line 585 def path_configuration_json(os, tabs: nil) require "json" JSON.generate(path_configuration_hash(os, tabs: tabs)) end |
#permission_errors(os) ⇒ Object
Problems with the permissions declaration for one platform, as human-readable strings — unknown names, and camera/location missing the mandatory usage string. Empty means buildable.
Only the unknown-name half applies to Android: the usage string exists to satisfy iOS, which kills the app when a prompt has no Info.plist entry. Android just prompts, so requiring the sentence there would fail builds over a value nothing reads.
510 511 512 513 514 515 516 517 518 519 520 521 522 523 |
# File 'lib/everywhere/config.rb', line 510 def (os) .flat_map do |name, usage| unless MOBILE_PERMISSIONS.include?(name) next ["unknown permission #{name.inspect} — supported: #{MOBILE_PERMISSIONS.join(", ")}"] end if os == "ios" && IOS_USAGE_KEYS.key?(name) && usage["ios"].to_s.strip.empty? next ["#{name} needs a usage string — the sentence iOS shows when asking. " \ "In config/everywhere.yml:\n permissions:\n #{name}: \"Why the app needs #{name}.\""] end [] end end |
#permissions ⇒ Object
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 |
# File 'lib/everywhere/config.rb', line 479 def entries = @data["permissions"] return {} unless entries.is_a?(Hash) entries.each_with_object({}) do |(name, value), acc| next if value.nil? || value == false acc[name.to_s] = case value when String then { "ios" => value } when Hash then value.transform_keys(&:to_s).transform_values(&:to_s) else {} end end end |
#remote? ⇒ Boolean
326 |
# File 'lib/everywhere/config.rb', line 326 def remote? = mode == "remote" |
#remote_instances? ⇒ Boolean
Multi-instance apps (remote.instances: true): the mobile shell boots
into remote.url — a hosted instance-picker page — and lets that page
re-root the app onto a chosen instance via Everywhere.instance.set,
persisted across launches until Everywhere.instance.clear. Opt-in
because it lets page JS repoint the whole app: apps that aren't
multi-instance shouldn't carry that surface.
338 339 340 |
# File 'lib/everywhere/config.rb', line 338 def remote_instances? @data.dig("remote", "instances") == true end |
#remote_url ⇒ Object
328 329 330 |
# File 'lib/everywhere/config.rb', line 328 def remote_url @data.dig("remote", "url")&.chomp("/") end |
#splash ⇒ Object
Optional custom splash page (HTML file, path relative to the app root). Shown while the packaged server boots; the shell injects window.EVERYWHERE_CONFIG so it can use the app's name and colors.
277 278 279 280 |
# File 'lib/everywhere/config.rb', line 277 def splash path = app["splash"] File.(path, root) if path end |
#tabs ⇒ Object
Mobile tab bar, platform-neutral. Icons are per-platform (ios = SF
Symbol, android = Material icon later), with a shared icon: fallback:
tabs:
- name: Builds
path: /builds
icons:
ios: hammer
android: build
Tabs ship two ways from the same source: baked into the app's bundled
path-configuration.json at build time (offline/first-launch), and served
live from /everywhere/
400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/everywhere/config.rb', line 400 def tabs entries = @data["tabs"] return [] unless entries.is_a?(Array) entries.filter_map do |tab| next unless tab.is_a?(Hash) && tab["name"] && tab["path"] path = tab["path"].start_with?("/") ? tab["path"] : "/#{tab["path"]}" { "name" => tab["name"], "path" => path, "icon" => tab["icon"], "icons" => (tab["icons"] if tab["icons"].is_a?(Hash)) }.compact end end |
#tabs_for(os) ⇒ Object
Tabs with the icon resolved for one platform: icons.
415 416 417 418 419 420 |
# File 'lib/everywhere/config.rb', line 415 def tabs_for(os) tabs.map do |tab| { "title" => tab["name"], "path" => tab["path"], "icon" => tab.dig("icons", os.to_s) || tab["icon"] || DEFAULT_TAB_ICONS[os.to_s] }.compact end end |
#targets ⇒ Object
Build targets as BARE os-arch strings — any ":every publish derives S3 update-manifest key paths from the first one),
so a suffix leaking through here would poison those paths. Use
#build_target_specs when the channel matters.
315 |
# File 'lib/everywhere/config.rb', line 315 def targets = build_target_specs.map { |t| t.to_s.split(":", 2).first } |
#tint_color ⇒ Object
378 379 380 |
# File 'lib/everywhere/config.rb', line 378 def tint_color normalize_color(appearance["tint_color"]) end |
#to_shell_hash(target: nil) ⇒ Object
The subset the shell needs, shipped as JSON (env var in dev,
Resources/everywhere.json inside a bundled .app). Pass target: so the
packaged config reflects the platform being built (its bundle id / name).
1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 |
# File 'lib/everywhere/config.rb', line 1368 def to_shell_hash(target: nil) { "name" => name(target: target), "bundle_id" => bundle_id(target: target), "version" => version(target: target), "mode" => mode, "remote_url" => remote_url, "remote_instances" => (true if remote_instances?), "entry_path" => entry_path, "tint_color" => tint_color, "background_color" => background_color, "menu" => ( unless .empty?), "tray" => (tray unless tray.empty?), "window" => window_shell_hash, "lazy_load_tabs" => native_lazy_load_tabs(target: target), "splash_min_seconds" => native_splash_min_seconds(target: target), "updates" => updates_shell_hash, "permissions" => (.keys unless .empty?), "auth" => auth_shell_hash, # Hosts the shell treats as its own for incoming universal links, so a # tapped link to any associated domain opens in-app rather than Safari. "universal_link_hosts" => (deep_linking_domains if deep_linking? && !deep_linking_domains.empty?) }.compact end |
#to_shell_json(target: nil) ⇒ Object
1393 1394 1395 1396 |
# File 'lib/everywhere/config.rb', line 1393 def to_shell_json(target: nil) require "json" JSON.generate(to_shell_hash(target: target)) end |
#tray ⇒ Object
System tray: same entry shape as menu: plus action: quit | show.
tray:
- label: "Open My App"
action: show
- label: "New Note"
path: /notes/new
- separator
- label: Quit
action: quit
1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 |
# File 'lib/everywhere/config.rb', line 1141 def tray entries = @data["tray"] return [] unless entries.is_a?(Array) entries.filter_map do |item| if item == "separator" || (item.is_a?(Hash) && item["separator"]) { "separator" => true } elsif item.is_a?(Hash) && item["label"] && (item["path"] || item["action"]) { "label" => item["label"], "path" => item["path"], "action" => item["action"] }.compact end end end |
#updates ⇒ Object
The updates: section — self-hosted auto-updates. Two halves:
a client half (url/channel/public_key/auto/interval) that ships to the
shell, and a publish-side s3: half that NEVER leaves the dev machine.
channel here is a release channel (stable/beta) — not the receipt's
distribution_channel (direct vs app stores).
347 |
# File 'lib/everywhere/config.rb', line 347 def updates = @data.fetch("updates", nil) || {} |
#updates_auto ⇒ Object
off — never check; check — check + notify (default); download — also fetch/verify in the background; install — silent auto-update.
359 |
# File 'lib/everywhere/config.rb', line 359 def updates_auto = updates["auto"] || "check" |
#updates_channel ⇒ Object
351 |
# File 'lib/everywhere/config.rb', line 351 def updates_channel = updates["channel"] || "stable" |
#updates_interval ⇒ Object
361 |
# File 'lib/everywhere/config.rb', line 361 def updates_interval = [(updates["interval"] || 21_600).to_i, 300].max |
#updates_public_key ⇒ Object
355 |
# File 'lib/everywhere/config.rb', line 355 def updates_public_key = updates["public_key"] |
#updates_s3 ⇒ Object
349 |
# File 'lib/everywhere/config.rb', line 349 def updates_s3 = updates.fetch("s3", nil) || {} |
#updates_shell_hash ⇒ Object
The client subset for the shell; nil (and thus absent from everywhere.json) until both url and public_key are configured — an unsigned update feed is not a thing.
366 367 368 369 370 371 372 373 374 375 376 |
# File 'lib/everywhere/config.rb', line 366 def updates_shell_hash return nil unless updates_url && updates_public_key { "url" => updates_url, "channel" => updates_channel, "public_key" => updates_public_key, "auto" => updates_auto, "interval" => updates_interval } end |
#updates_url ⇒ Object
353 |
# File 'lib/everywhere/config.rb', line 353 def updates_url = updates["url"]&.chomp("/") |
#version(target: nil) ⇒ Object
Marketing/display version of the app (CFBundleShortVersionString, and the
version recorded in the build receipt). One shared app.version is the
default for every target; a platform may override it when a store forces a
different number. Defaults conservatively.
291 292 293 |
# File 'lib/everywhere/config.rb', line 291 def version(target: nil) resolved(target)["version"] || "0.1.0" end |
#window ⇒ Object
1176 1177 1178 1179 |
# File 'lib/everywhere/config.rb', line 1176 def window section = @data["window"] section.is_a?(Hash) ? section : {} end |
#window_drag_height ⇒ Object
How much of the top of the page drags the window, in CSS pixels. Only meaningful for overlay/frameless, where there's no system title bar left to grab. nil leaves the shell's 28px default (the height of the macOS traffic-light band); 0 turns dragging off for an app that wants to place its own drag regions.
1199 1200 1201 1202 |
# File 'lib/everywhere/config.rb', line 1199 def window_drag_height value = window["drag_height"] value.to_f if value.is_a?(Numeric) && !value.negative? end |
#window_errors ⇒ Object
1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 |
# File 'lib/everywhere/config.rb', line 1218 def window_errors raw = @data["window"] return [] if raw.nil? return ["window: must be a mapping"] unless raw.is_a?(Hash) errors = [] = raw["title_bar"] if && !WINDOW_TITLE_BARS.include?(.to_s) errors << "window.title_bar #{.to_s.inspect} must be one of " \ "#{WINDOW_TITLE_BARS.join(" / ")}" end errors += %w[size min_size].filter_map do |key| next if raw[key].nil? || window_dimensions(key) "window.#{key} must be two positive numbers, like [1100, 750]" end drag = raw["drag_height"] unless drag.nil? || (drag.is_a?(Numeric) && !drag.negative?) errors << "window.drag_height must be a number of pixels (0 turns dragging off)" end errors + %w[title resizable].filter_map do |key| next if raw[key].nil? || boolean_or_nil(raw[key]) == raw[key] "window.#{key} must be true or false" end end |
#window_min_size ⇒ Object
1192 |
# File 'lib/everywhere/config.rb', line 1192 def window_min_size = window_dimensions("min_size") |
#window_resizable ⇒ Object
1189 |
# File 'lib/everywhere/config.rb', line 1189 def window_resizable = boolean_or_nil(window["resizable"]) |
#window_shell_hash ⇒ Object
The window subset the shell needs. nil (absent from everywhere.json) when the app declares nothing, so the shell's own defaults stand untouched.
1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 |
# File 'lib/everywhere/config.rb', line 1206 def window_shell_hash hash = { "title_bar" => , "title" => window_title, "resizable" => window_resizable, "size" => window_size, "min_size" => window_min_size, "drag_height" => window_drag_height }.compact hash unless hash.empty? end |
#window_size ⇒ Object
1191 |
# File 'lib/everywhere/config.rb', line 1191 def window_size = window_dimensions("size") |
#window_title ⇒ Object
Tri-state, like native_ios_lazy_load_tabs: an absent key leaves the shell on its default rather than forcing one.
1188 |
# File 'lib/everywhere/config.rb', line 1188 def window_title = boolean_or_nil(window["title"]) |
#window_title_bar ⇒ Object
1181 1182 1183 1184 |
# File 'lib/everywhere/config.rb', line 1181 def value = window["title_bar"].to_s value if WINDOW_TITLE_BARS.include?(value) end |