fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11) - #1495

Merged
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd
Apr 17, 2026
Merged

fix: migrate to Expo SDK 54 stack (reanimated 4 + new arch + MapLibre v11)#1495
CraigBuckmaster merged 2 commits into
masterfrom
claude/migrate-expo-sdk-54-lrYNd

Conversation

@CraigBuckmaster

Copy link
Copy Markdown
Owner

Why

TestFlight 1.0.3/1.0.4 crashed on launch with a TurboModule registration failure. PR #1492 ("remove react-native-reanimated") was a misguided attempt to fix it by ripping reanimated out entirely. The real incompatibility was the combination of:

  • react-native-reanimated@3.19.5 under Expo SDK 54 (expects reanimated 4)
  • legacy React Native architecture (SDK 54 defaults to New Arch)
  • @maplibre/maplibre-react-native@10.2.0 (v10 doesn't fabric-register under New Arch → classic TurboModule crash)

This PR adopts the supported SDK 54 stack end-to-end and bumps to 1.0.5.

What

Deps (app/package.json):

  • react-native-reanimated~4.1.1 (new)
  • react-native-worklets~0.5.1 (new; reanimated 4 peer dep)
  • @maplibre/maplibre-react-native^11.0.0-beta.30 (from 10.2.0)

Build config:

  • babel.config.js: add "react-native-worklets/plugin" (canonical location for the worklet transform in reanimated 4; the old react-native-reanimated/plugin alias still resolves but the worklets plugin is the official path).
  • app.json: expo-build-properties.ios.newArchEnabled and .android.newArchEnabledtrue. Version bumped to 1.0.5.

Source files restored to pre-#1492 reanimated patterns:

  • LoadingSkeleton, ChapterSkeleton: useSharedValue/useAnimatedStyle/withRepeat/withTiming pulse (replacing the Animated.loop fallback).
  • ContentImageGallery: ZoomViewer pinch/pan/double-tap go back to reanimated shared values + useAnimatedStyle — the paired-ref shadow state is gone.
  • useTreeCamera: gesture callbacks go through runOnJS(...) again instead of .runOnJS(true) on the gesture builder.

Test mocks restored + extended:

  • jest.setup.js: re-adds react-native-reanimated/mock, and rewrites the @maplibre/maplibre-react-native mock to the v11 export surface (Map, GeoJSONSource, Layer, LogManager, NetworkManager, OfflineManager — no default export).
  • ContentImageGallery.test.tsx, useTreeCamera.test.ts, GenealogyTreeScreen.test.tsx: local reanimated mocks restored.

MapLibre v11 audit — mechanical rewrite of the map module:

v10v11
MapViewMap
ShapeSourceGeoJSONSource
CircleLayer, SymbolLayer, FillLayer, LineLayer<Layer type="..."/>
scrollEnableddragPan
zoomEnabledtouchZoom (+ doubleTapZoom, doubleTapHoldZoom)
pitchEnabledtouchPitch
rotateEnabledtouchRotate
attributionEnabledattribution
logoEnabledlogo
Camera defaultSettings={...}Camera initialViewState={...}
{ centerCoordinate, zoomLevel }{ center, zoom }
fitBounds(ne, sw, padding[], duration)fitBounds([w,s,e,n], { padding, duration })
setCamera({ animationMode: 'flyTo', ... })flyTo({ center, zoom, duration })
top-level setConnectedNetworkManager.setConnected
default exportnamed exports only
  • useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent (event.nativeEvent.zoom) instead of a GeoJSON feature (feature.properties.zoomLevel). Hook reads either shape so we don't break any callers still using the v10 synthetic during migration; v11 test covers the new path, a backwards-compat test pins the v10 branch.
  • isMapNativeAvailable: probe drops .default (removed in v11) and goes through NetworkManager.setConnected when available.
  • OfflineManager keeps its PascalCase name in v11 — useMapTileCache + its test were already aligned.

Validation

  • npm test3341 passing across 453 suites
  • npx expo export --platform ios → bundles 3932 modules into an 8.47 MB Hermes bytecode bundle ✅
  • <Light> component is not used in the codebase (no changes needed there); setAccessToken/getAccessToken never called.

Test plan

  • Run eas build --profile preview --platform ios and confirm the binary launches (no TurboModule crash).
  • Smoke-test the map on a real device: pan/zoom/era filter/story overlay/place detail/chapter deep-link all still work.
  • Smoke-test the genealogy tree: pan, pinch, centre on node, era jump.
  • Confirm ChapterSkeleton + LoadingSkeleton animate smoothly (no stuck opacity).
  • Confirm the fullscreen image viewer in ContentImageGallery still pinch/pans/double-taps cleanly.
  • Promote to TestFlight, verify it no longer crashes on launch.

https://claude.ai/code/session_01CvhqCQ1bXtVXMpF85UENW5

…h + MapLibre v11)
PR #1492 removed react-native-reanimated to try to stop a TestFlight launch crash
(TurboModule registration failure). That was the wrong fix — the real
incompatibility was reanimated 3 running under SDK 54 with the legacy
architecture and MapLibre v10. This change restores reanimated (v4), turns on
New Architecture, and upgrades MapLibre to the v11 beta so the whole native
stack matches what SDK 54 expects.
Deps (app/package.json):
- react-native-reanimated ~4.1.1 (new)
- react-native-worklets ~0.5.1 (new; reanimated 4 peer dep)
- @maplibre/maplibre-react-native ^11.0.0-beta.30 (from 10.2.0)
Build config:
- babel.config.js: add "react-native-worklets/plugin" (reanimated 4
requires the worklets plugin; the old "react-native-reanimated/plugin"
alias still resolves, but the canonical path lives in worklets now).
- app.json: flip newArchEnabled → true for both iOS and Android in the
expo-build-properties plugin; bump version to 1.0.5.
Source files restored to their pre-#1492 reanimated patterns:
- LoadingSkeleton + ChapterSkeleton: useSharedValue / useAnimatedStyle /
withRepeat / withTiming pulse again (RN Animated.loop was the #1492
fallback).
- ContentImageGallery: ZoomViewer pinch/pan/double-tap use reanimated
shared values + useAnimatedStyle transform. The paired-ref shadow state
from #1492 is gone.
- useTreeCamera: gesture worklet callbacks go through runOnJS(...)
again, removing the .runOnJS(true) gesture-builder workaround.
Test mocks:
- jest.setup.js: re-adds react-native-reanimated/mock; rewrites the
@maplibre/maplibre-react-native mock to the v11 export surface
(Map, GeoJSONSource, Layer, LogManager, NetworkManager, etc — no
default export).
- ContentImageGallery.test.tsx, useTreeCamera.test.ts, and
GenealogyTreeScreen.test.tsx each restore their local reanimated
mocks (jest.requireActual('react-native-reanimated/mock') + a runOnJS
passthrough so gesture callbacks execute synchronously under jest).
MapLibre v11 audit — mechanical API rewrite across the map module:
- Component renames: MapView → Map, ShapeSource → GeoJSONSource. All
the typed layer components (CircleLayer / SymbolLayer / FillLayer /
LineLayer) collapse into a single <Layer type="circle|symbol|fill|line"/>.
- Map prop renames: scrollEnabled → dragPan, zoomEnabled → touchZoom,
pitchEnabled → touchPitch, rotateEnabled → touchRotate,
attributionEnabled → attribution, logoEnabled → logo. Chip also opts
out of double-tap zoom explicitly.
- Camera: defaultSettings → initialViewState, and the { centerCoordinate,
zoomLevel } shape is now { center, zoom }. fitBounds(ne, sw, padding[],
duration) → fitBounds([west, south, east, north], { padding, duration }).
setCamera({ ..., animationMode: 'flyTo', animationDuration }) → flyTo(
{ center, zoom, duration }).
- useMapZoom: onRegionDidChange now receives a ViewStateChangeEvent
(event.nativeEvent.zoom) instead of a GeoJSON feature
(feature.properties.zoomLevel). The hook reads from either shape so
we don't break any callers still using the v10 synthetic during
migration; the v11 test covers the new path and a backwards-compat
test pins the v10 branch.
- isMapNativeAvailable: default export is removed in v11, so the probe
drops .default access. setConnected moved under NetworkManager —
we probe through NetworkManager.setConnected when available.
- OfflineManager keeps its PascalCase name in v11 (only the default
export and a handful of the original top-level helpers moved); the
hook + test were already aligned.
Validation:
- npm test → 3341 passing across 453 suites.
- npx expo export --platform ios → bundles 3932 modules into an
8.47 MB hermes bytecode bundle. Build green, matching the SDK 54
expectation of reanimated 4 + worklets + new arch.
@github-actions

Copy link
Copy Markdown

⚠️Tests: Could not parse results

CI lint+test jobs on PR #1495 failed at `npx tsc --noEmit`. The v11 API
types are stricter than v10 in a few places the first pass missed.
GeoJSONSource prop rename:
- v10's `<ShapeSource shape={fc} />` → v11 uses `<GeoJSONSource data={fc} />`.
Fixed in AncientBorderLayer, PersonArcLayer, PlaceMarkerList, StoryOverlays.
PlaceMarkerList hitbox:
- v10 accepted `{ width, height }`. v11 types hitbox as a `ViewPadding`
(`{top, right, bottom, left}`). Switched to per-side 11px padding so
the effective tap box stays 22x22.
MapChipNative camera:
- `InitialViewState` in v11 is a discriminated union — either `{center,
zoom}` or `{bounds, padding}`, never both. The local structural alias
had both optional, which fails the union check. Aliased directly to
the library's `InitialViewState` type and typed the bounds tuple
literally so the union narrows to the bounds variant.
useMapTileCache:
- v11 `OfflinePackCreateOptions` renamed `styleURL` → `mapStyle` and
expects flat `LngLatBounds = [west, south, east, north]` (no more
`{ne, sw}` nested tuple).
- Packs no longer accept a user-provided `name` — each pack gets a
UUID assigned by the native side. Looked up by `getPack(id)` only,
not by name. To preserve our idempotency pattern, tag via
`metadata: { name: PACK_NAME }` and scan `getPacks()` for a match.
- Test suite mirrors the change: mocks `getPacks` (not `getPack`),
asserts the flat bounds + new `mapStyle` field + `metadata.name`
tagging. Adds a coverage case for unrelated packs.
Verified: tsc clean, 3342 tests pass, `npx expo export --platform ios`
still bundles successfully.
@github-actions

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3342❌ 03342
Suites✅ 453❌ 0453

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 81.7s

@CraigBuckmaster
CraigBuckmaster merged commit d92d72a into masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the claude/migrate-expo-sdk-54-lrYNd branch April 17, 2026 16:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@CraigBuckmaster@claude