[SPIKE]: Test Bible Text Rendering Natively#46
Draft
bmanquen wants to merge 15 commits into
Draft
Conversation
Android react-native-webview delivers injectedJavaScriptBeforeContentLoaded via onPageStarted + evaluateJavascript, which can run after page scripts on fast file:///android_asset loads (expo/expo#41798). When the page wins that race, expo's dom-entry throws before mounting and its marshal locks IS_DOM=false permanently, leaving every DOM component WebView blank in Android release builds. dom-bridge-recovery detects the lost race before expo's entry runs, mounts with placeholder globals, traps the late injection for initial props, and rebuilds prop updates, native actions, and matchContents sizing on the raw postMessage/$$dom_event transport. Inert (unwrapped passthrough) whenever the race is won — dev builds, iOS, web. Lives in the SDK rather than an expo patch so consumers get the fix from the source we ship, with no changes to their own Expo install. Verified on Samsung A15 (Android 14) release build: reader, version and chapter picker commits, settings round trip, footnote sheet sizing, VerseOfTheDay, and BibleCard. See docs/adr/0008. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligns the example app with current Expo SDK 55 point releases (react/ reanimated/worklets pinned by expo install --fix) and adds build:android-release for reproducing release-only WebView behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The recovery module mirrors expo's wire-protocol message types ($$dom_event, $$native_action, $$native_action_result, $$match_contents_event, $$props) by value. A future Expo rename would silently re-blank DOM components in Android release builds — the broken constants would compile fine and fail only on-device. Export the constants from dom-bridge-recovery and add a drift-detection suite that reads the canonical values from expo/src/dom/injection.ts (and the inline $$props literal from webview-wrapper.tsx / dom-entry.tsx) and asserts each one still matches. Verified the guard catches drift via a negative test (corrupting DOM_EVENT fails the test loudly). The four injection.ts constants are extracted from expo source by regex so the test never restates them. PROPS_EVENT is derived as DOM_EVENT.slice(0, 2) + 'props' to avoid restating the dollar-sign prefix, and its presence in both the expo emit and listener files is asserted by quoted-token search. HOC active-path coverage (prop merge, $$props updates, action-proxy invoke/resolve/reject, spread order) remains on-device-validated per ADR 0008 — jest-expo's babel TS transform and React 19's act() do not cooperate reliably under the jsdom environment this package targets.
Adds the missing type to the UI tsconfig array so the dom-bridge-recovery drift test (which reads expo source via ) type- checks — matches the existing core tsconfig pattern (). Replaces in the drift test's constant extractor with quote-slicing. The regex captures a single-quoted literal, so is equivalent and removes the only directive, which was itself an unresolved-rule lint error in this config. Also lands the core jest moduleNameMapper additions for react/react-dom resolution and the core babel.config.js that were uncommitted on the prior branch.
…bview) Upgrades the monorepo to Expo SDK 56 to pick up the upstream fix for the Android release-build DOM WebView injection race (expo/expo#41798, closed 2026-05-14). SDK 56 ships @expo/dom-webview (~56.0.5) as the default backing WebView for DOM components, which does not use the onPageStarted + evaluateJavascript injection path that lost the race under react-native- webview. The dom-bridge-recovery workaround stays in place pending on-device release-build validation, but its drift test confirms all five wire constants still match expo's SDK 56 source. Version bumps (all aligned with bundledNativeModules for expo@56.0.12): expo 55.0.26 -> 56.0.12 react-native 0.83.6 -> 0.85.3 react/react-dom 19.2.0 -> 19.2.3 @expo/dom-webview ~55.0.6 -> ~56.0.5 @expo/metro-runtime ~55 -> ~56.0.15 expo-dev-client ~55 -> ~56.0.20 expo-router ~55 -> ~56.2.11 expo-linking ~55 -> ~56.0.14 expo-status-bar ~55 -> ~56.0.4 expo-application ~55 -> ~56.0.3 expo-crypto ~55 -> ~56.0.4 expo-web-browser ~55 -> ~56.0.5 expo-secure-store ^55 -> ^56.0.4 reanimated ~4.2.1 -> 4.3.1 worklets ~0.7.4 -> 0.8.3 gesture-handler ~2.30.1 -> ~2.31.1 safe-area-context ~5.6.2 -> ~5.7.0 screens ~4.23.0 -> 4.25.2 svg ^15.15.3 -> 15.15.4 typescript 5.9.3 -> 6.0.3 babel-preset-expo ~55 -> ~56.0.15 jest-expo ~55 -> ~56.0.5 eslint-config-expo ^55 -> ~56.0.4 @gorhom/bottom-sheet ^5.2.13 -> ^5.2.14 Peer dep ranges widened to >=56 <57 for expo, expo-secure-store; react-native floor raised to >=0.85.0. react-native-webview removed from example app deps (no longer needed for DOM components under @expo/dom-webview; the UI package retains its peer dep for consumers who opt back in via dom={{ useExpoDOMWebView: false }}). Code changes required by the bump: - StyleSheet.absoluteFillObject -> StyleSheet.absoluteFill (removed from RN 0.85 types; semantically identical, both are plain style objects) - native-sheet.tsx: lazy ref-init (useRef + render-phase assignment) -> useState lazy initializer (react-hooks/refs rule from SDK 56 lint config) - bible-version-picker-sheet.tsx: resetKey ref -> useState so the bumped value flows through render without a ref read (react-hooks/refs) - baseUrl removed from core/ui tsconfigs (deprecated in TS 6, vestigial) - Two scoped react-hooks/set-state-in-effect disables on the reset-on-prop- change effects; the React-sanctioned alternative (adjusting state during render) trips react-hooks/refs instead, and a key-remount would change the DOM component contract - One scoped react-hooks/globals disable in native-sheet test (intentional module-level mutable capture cell) Verified: pnpm typecheck (3/3), pnpm lint (clean), pnpm test (core 72, ui 149 passed / 1 skipped). Dev-client builds and on-device Android release validation are the remaining gate (see handoff).
Expo SDK 56 ships @expo/dom-webview as the default backing WebView for DOM components, which does not use the onPageStarted + evaluateJavascript injection path that lost the race under react-native-webview. The upstream issue (expo/expo#41798) was closed 2026-05-14. ADR 0008 said: 'retire this module entirely if expo ships an injection mechanism that cannot lose the race.' SDK 56 is that mechanism. Keeping the module under @expo/dom-webview was actively harmful: its bootstrap() runs at module-eval time and detects a 'lost race' whenever 34726EXPO_DOM_HOST_OS is unset at the moment the module evaluates — which happens routinely under @expo/dom-webview's different injection timing. It then overrides expo's real injection with placeholder globals and a transport rebuild built for the old react-native-webview primitives. On-device testing on a Samsung A15 (Android 14) confirmed DOM WebViews did not render with the recovery module still in place under SDK 56. Removed: - packages/ui/src/lib/dom-bridge-recovery.tsx (204 LOC) - packages/ui/src/lib/__tests__/dom-bridge-recovery.test.ts (12 tests) - withDomBridgeRecovery HOC wrapper from all 8 DOM component exports Each DOM component reverts to a direct 'export default function' declaration with no wrapper. ADR 0008 is marked Retired with the reasoning preserved. Verified: typecheck (3/3), lint (clean), test (core 72, ui 137 passed).
Update app and package manifests to align the example app and workspace with the required Expo native modules and remove the hard pinned Expo override.
Android DOM WebViews rendered blank under Expo SDK 56 because @expo/dom-webview's DomWebView.kt never sets WebSettings.domStorageEnabled, which defaults to false on Android. With DOM storage off, window.localStorage is null (not undefined), so the Web SDK's YouVersionProvider and our applySDKConfig throw on localStorage.getItem/setItem, the DOM error boundary catches it, and the component blanks. The native WebView was present and painting an empty page the whole time. iOS (WKWebView) and react-native-webview both default DOM storage on, so this only regressed when SDK 56 made @expo/dom-webview the default backing WebView. typeof null === 'object' is why the old `typeof localStorage !== 'undefined'` guard never caught it. - dom-local-storage.ts: in-memory Storage shim, installs only when window.localStorage == null; self-installs on import. Per-WebView and non-persistent, which is fine — DOM WebViews are long-lived (pre-warmed) and the installation id is re-supplied from native props on every mount. - web-yv-provider.ts: import the shim first (all DOM components route through this provider) so it's in place before the Web SDK module evaluates. - dom-apply.ts: harden the guard with `localStorage != null`. - dom-local-storage.test.ts: layer-1 coverage (installs when null, leaves a real localStorage untouched, no-ops when window is undefined). Also fold in SDK 56 investigation findings: keep expo.autolinking.buildFromSource for @expo/dom-webview as codegen hardening (device-disproven as the blank-render cause — kept for clean/CI codegen correctness), and correct the comments in content-sized-body.tsx, embed-dom-props.ts, and ADR 0008 to match how SDK 56 actually delivers host-OS globals and styles the DOM template. Verified on a Samsung A15 (SM-A156U1): full-screen reader, VOTD, and BibleCard matchContents embeds all render; 0 null-localStorage errors after a clean reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ader) The reader rendered blank on iOS (every load, home tab) with "Top OS ($$EXPO_DOM_HOST_OS) is not defined". @expo/dom-webview's iOS injection embeds the initial-props JSON in a JS template literal, which un-escapes the \" that JSON.stringify produced. The default stack '"Source Serif 4", serif' is the only SDK prop containing a ", so its JSON broke, JSON.parse threw, and the runtime globals were never set. Only the reader was affected; Android uses a real @JavascriptInterface and is fine. Fix: cross the native<->DOM bridge with quote-free font tokens and decode back to the canonical CSS stack inside the DOM components. Store, MMKV, and Web SDK stay canonical, so font-highlight parity is preserved and there's no migration. See docs/adr/0009-bridge-safe-font-tokens.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bmanquen
force-pushed
the
bm/YPE-3169-spike-rn-expo-sdk-investigate-the-rn-expo-route-forward
branch
from
June 24, 2026 17:24
30aa990 to
0d76890
Compare
New native reader: parses API HTML to a React tree (no WebView/DOM), maps USFM classes to RN styles, and bundles a native hanging-indent Expo module that autolinks as the default. Tested (JS layer, all green): - UI + example typecheck, lint, prettier — pass (also clears 2 pre-existing renderer type errors) - UI test suite — 191 passed, 1 skipped - Layer 1: parse-scripture-html (raw + transformed shapes, hidden-class drops, whitespace) and scripture-class-styles (USFM → RN style, poetry/list indent, words-of-Jesus red, small-caps, font cascade, light/dark) - Layer 3: verse text + onVersePress; footnote markers carry full cross-block verse context; super/subscript with body font + themed color threaded in; native hanging paragraph (serialized runs + geometry); footnote drawer opens with reference/verse/note and is off on web - Metro bundle resolves under Hermes (hooks pkg, node-html-parser, fonts) - Not covered: autolinking + on-device layout — needs `expo prebuild --clean`
bmanquen
force-pushed
the
bm/YPE-3169-spike-rn-expo-sdk-investigate-the-rn-expo-route-forward
branch
from
June 24, 2026 17:25
0d76890 to
c6aade7
Compare
The i18n module (locale-context.tsx, detect-device-locale.ts) imports expo-localization, but it was never declared or installed. The example build broke once a recent change pulled the i18n module into the app's Metro graph, surfacing "Unable to resolve module expo-localization". - Add expo-localization (~56.0.6) to the example app + its config plugin - Declare expo-localization as a UI peer dependency (>=56.0.0 <57.0.0), matching the convention for other Expo native modules
188 Gradle build outputs under packages/ui/android/build were committed. .npmignore already excluded them from the published package, but there was no matching .gitignore, so they leaked into git. - Add packages/ui/.gitignore (android/build, android/.gradle, android/.cxx) - git rm -r --cached the tracked build artifacts; Gradle regenerates them per app build
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.