<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Warren de Leon</title>
    <description>The latest articles on DEV Community by Warren de Leon (@warrendeleon).</description>
    <link>https://dev.to/warrendeleon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3849498%2F78ff4b91-2b34-4678-85ae-19aaf1642e95.png</url>
      <title>DEV Community: Warren de Leon</title>
      <link>https://dev.to/warrendeleon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/warrendeleon"/>
    <language>en</language>
    <item>
      <title>Fallback for federated remotes: the copy in the binary and the net in the session</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 19 Oct 2026 00:03:59 +0000</pubDate>
      <link>https://dev.to/warrendeleon/fallback-for-federated-remotes-the-copy-in-the-binary-and-the-net-in-the-session-3od</link>
      <guid>https://dev.to/warrendeleon/fallback-for-federated-remotes-the-copy-in-the-binary-and-the-net-in-the-session-3od</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-fallbacks" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/cdn-version-resolution-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-fallbacks" rel="noopener noreferrer"&gt;Post 15&lt;/a&gt; ended its break-it section on its own limit: "None of this gives the binary anywhere else to load from: a dead tab is honest, and it is not a working app." This post gives the binary somewhere else. Every release build now carries a copy of each remote, and the app runs from those copies when the content delivery network (CDN) cannot be reached, or when one remote fails to load while the other keeps loading from it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-fallbacks" rel="noopener noreferrer"&gt;Post 1&lt;/a&gt; said who builds that: "Module Federation gives you none of the offline story by itself: shipping a copy of each remote inside the binary, so the reviewed app works on its own with no network, is an architecture you build." It also set the requirement for a remote that will not load: "the app has to degrade to something safe instead of showing a blank screen." Post 15's build meets the requirement for a remote that will not load, and stops there. With the CDN out of reach it launches &lt;code&gt;unresolved&lt;/code&gt;, with an error state in either tab. When one remote fails, its tab shows the error state, and Try again never brings the tab back.&lt;/p&gt;

&lt;p&gt;The first half builds the copy: a build step that stages it, a phase on each platform that puts it in the binary, and a new launch mode, &lt;code&gt;bundled&lt;/code&gt;, that runs every remote from it. The second half builds two nets for a CDN launch. One catches a manifest the CDN cannot serve and drops that one remote to its copy; the other sits around each tab and catches a load that fails later. A Try again that actually retries comes with the second net.&lt;/p&gt;

&lt;p&gt;One rule shapes the copy: &lt;strong&gt;it is the version already known to run on this binary, frozen on the day the binary was built.&lt;/strong&gt; It is the guaranteed minimum: new versions still reach installed apps through the CDN, and the host uses the copy only when the CDN cannot deliver a version that loads.&lt;/p&gt;

&lt;p&gt;Start from post 15's finished state, the tag &lt;code&gt;post-15-cdn-flip&lt;/code&gt;; this post finishes at &lt;code&gt;post-16-fallbacks&lt;/code&gt;. As before, you type the configuration changes and the smaller edits, and copy the rest from the end tag: the build tool, the host's code and tests, the native files and one design-system test.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/offline-fallback-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-fallbacks" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The launch decides the mode once, before anything federated loads. Using the CDN's versions means a usable map arrived and its versions registered; when they cannot be used, the launch registers the copies instead. It ends unresolved only when that fails too. The two nets act inside a CDN launch, one remote at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a copy of each remote in the binary
&lt;/h2&gt;

&lt;p&gt;A copy is chosen by the same map that picks versions from the CDN. The version a binary carries is the one its own app version's map names, because that is the version known to run on that binary: for the 2.0.0 build here, list 1.2.0 and party 1.0.0.&lt;/p&gt;

&lt;p&gt;The copy holds that version's signed bundles and its manifest, byte for byte as the CDN serves them. The bytes stay as they are because every bundle is still verified against the signing key when it loads, and a change to its code fails the check.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tools/build-cdn.mjs&lt;/code&gt; already builds every version into &lt;code&gt;cdn-root/&lt;/code&gt;, and it now stages the copies as well. Fetch the end tag once, since every copy command in this post takes its files from it, and take the tool first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-16-fallbacks /tmp/pokedex-ref-16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/tools/build-cdn.mjs tools/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new stage runs after the CDN tree is built. For each platform it takes every version the map names and copies its bundles and manifest into &lt;code&gt;embed-root/&amp;lt;platform&amp;gt;/&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embeddedVersions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cdnDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Every script of the version sits at the top of its directory with its manifest, and the copy&lt;/span&gt;
  &lt;span class="c1"&gt;// is all of them, byte for byte: the container, its chunks, index.bundle, which the manifest&lt;/span&gt;
  &lt;span class="c1"&gt;// names among the shared modules' assets, and mf-manifest.json, which the federation runtime&lt;/span&gt;
  &lt;span class="c1"&gt;// reads from the disk the way it reads it from the CDN. The assets/ folder beside them holds&lt;/span&gt;
  &lt;span class="c1"&gt;// images the host's shared copies of those libraries already carry.&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mf-manifest.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;copyFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;bundledVersions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The copies keep the &lt;code&gt;&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code&gt; directories instead of sharing one folder, because two remotes can ship vendor chunks with the same file name. In one folder, the second would overwrite the first.&lt;/p&gt;

&lt;p&gt;The build tool also records the copied versions in &lt;code&gt;apps/host/src/shell/embedded-versions.ts&lt;/code&gt;, a generated file the host compiles in, so the host knows which copies it carries before it reads anything from the disk.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;embed-root/&lt;/code&gt; is build output, like &lt;code&gt;cdn-root/&lt;/code&gt;, so add it to &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# The copies build-cdn stages for the native embed phases.
embed-root/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the key generator first, then the tool for both platforms. The generator keeps any keys the checkout already has, and creates them on a fresh checkout of the tag, which has none because they never enter Git. After the CDN tree, the tool reports the copies and the generated file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/gen-signing-keys.mjs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node tools/build-cdn.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;embedded  -&amp;gt; embed-root/ios/listApp/1.2.0
embedded  -&amp;gt; embed-root/ios/partyApp/1.0.0
embedded  -&amp;gt; embed-root/android/listApp/1.2.0
embedded  -&amp;gt; embed-root/android/partyApp/1.0.0
wrote     -&amp;gt; apps/host/src/shell/embedded-versions.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated file records the version of each copy, per platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BUNDLED_VERSIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build tool stages the copy for the app version in &lt;code&gt;MF_APP_VERSION&lt;/code&gt;, or for the newest app version with a map when that is unset, so a build for another app version runs the tool with the same variable first.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS: the last build phase
&lt;/h3&gt;

&lt;p&gt;On iOS the copy goes inside the &lt;code&gt;.app&lt;/code&gt;, next to &lt;code&gt;main.jsbundle&lt;/code&gt;. A script does the copying, and a Run Script phase at the end of the Host target runs it. Copy the script from the end tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/host/scripts &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/scripts/embed-remotes-ios.sh apps/host/scripts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# --- The copy in the binary, iOS's half. The last Run Script phase of the Host target copies each&lt;/span&gt;
&lt;span class="c"&gt;# remote's embedded version from embed-root/ios into the app, at cdn/ios/&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/ next&lt;/span&gt;
&lt;span class="c"&gt;# to main.jsbundle, where the host reads it through absolute file:// URLs: the signed bundles and&lt;/span&gt;
&lt;span class="c"&gt;# the version's mf-manifest.json.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# The &amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/ directories are kept rather than flattened: two remotes can ship vendor&lt;/span&gt;
&lt;span class="c"&gt;# chunks with the same file name, and a flat copy would let one overwrite the other.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# It runs in every configuration. A Debug build loads the host's own bundle over http, so it cannot&lt;/span&gt;
&lt;span class="c"&gt;# read the copy, and there the copy only costs the time it takes. Its remotes come from the dev&lt;/span&gt;
&lt;span class="c"&gt;# servers when no CDN is configured, or from the CDN when one is.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# With embed-root missing it removes any copy an earlier build left behind, prints a warning and&lt;/span&gt;
&lt;span class="c"&gt;# exits 0, and the build succeeds with nothing embedded. That is the trap: run&lt;/span&gt;
&lt;span class="c"&gt;# `node tools/build-cdn.mjs` before a release build. ---&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;REPO_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/../../.."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO_DIR&lt;/span&gt;&lt;span class="s2"&gt;/embed-root/ios"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONFIGURATION_BUILD_DIR&lt;/span&gt;:?run&lt;span class="p"&gt; this from an Xcode build phase&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UNLOCALIZED_RESOURCES_FOLDER_PATH&lt;/span&gt;:?run&lt;span class="p"&gt; this from an Xcode build phase&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIGURATION_BUILD_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$UNLOCALIZED_RESOURCES_FOLDER_PATH&lt;/span&gt;&lt;span class="s2"&gt;/cdn/ios"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;rmdir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  echo&lt;/span&gt; &lt;span class="s2"&gt;"warning: &lt;/span&gt;&lt;span class="nv"&gt;$SOURCE&lt;/span&gt;&lt;span class="s2"&gt; does not exist, so no remotes are embedded. Run 'node tools/build-cdn.mjs' before a release build."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# --delete keeps the app to exactly what embed-root holds, so a version an earlier build embedded&lt;/span&gt;
&lt;span class="c"&gt;# does not linger. The bytes are copied as they are: each bundle is signed over them.&lt;/span&gt;
rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*/'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.bundle'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'mf-manifest.json'&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"embedded &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.bundle'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; remote bundles and &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'mf-manifest.json'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; manifests into &lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The phase is one change to the project file. In Xcode it is a new Run Script phase on the Host target, named "Embed federated remotes (offline fallback)", running &lt;code&gt;"$SRCROOT/../scripts/embed-remotes-ios.sh"&lt;/code&gt;, placed after every other phase, with "Based on dependency analysis" unchecked. It goes last so that the &lt;code&gt;.app&lt;/code&gt; it writes into is already assembled. With the box unchecked it runs on every build, which a phase that declares no outputs does anyway: Xcode skips a script phase only when it has outputs to check. Declaring the copied files under the &lt;code&gt;.app&lt;/code&gt;'s &lt;code&gt;cdn/ios&lt;/code&gt; as outputs, with &lt;code&gt;embed-root/&lt;/code&gt;'s files as inputs, is the alternative. The end tag's project file carries the phase with the box unchecked, so copying it makes the same change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/ios/Host.xcodeproj/project.pbxproj apps/host/ios/Host.xcodeproj/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Android: a Gradle task and a native module
&lt;/h3&gt;

&lt;p&gt;On Android the copy goes into the assets of the APK, the Android package an app is installed from. At the end of &lt;code&gt;apps/host/android/app/build.gradle&lt;/code&gt;, after post 15's changes, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- The copy in the binary, Android's half. build-cdn stages each remote's embedded version under&lt;/span&gt;
&lt;span class="c1"&gt;// embed-root/android, the signed bundles and the version's mf-manifest.json, and this task mirrors&lt;/span&gt;
&lt;span class="c1"&gt;// that tree into the APK's assets at cdn/android, where EmbeddedRemotesModule copies it out at&lt;/span&gt;
&lt;span class="c1"&gt;// launch. Sync rather than Copy, so a file an earlier build embedded and this one does not is&lt;/span&gt;
&lt;span class="c1"&gt;// removed instead of shipped.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// With embed-root missing, this task embeds nothing, warnIfNothingToEmbed prints a warning, and the&lt;/span&gt;
&lt;span class="c1"&gt;// build still succeeds. That is the trap: a successful release build that carries no copies. Run&lt;/span&gt;
&lt;span class="c1"&gt;// build-cdn before a release build. ---&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;embedSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$rootDir/../../../embed-root/android"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;embeddedAssetsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;buildDirectory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"generated/embedded-remotes"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;asFile&lt;/span&gt;

&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;embedRemotes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'embedRemotes'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sync&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="s1"&gt;'**/*.bundle'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'**/mf-manifest.json'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;into&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$embeddedAssetsDir/cdn/android"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --- A Sync with no source skips its actions, so a warning inside it would never print. Gradle&lt;/span&gt;
&lt;span class="c1"&gt;// still clears what the task embedded before when its source goes, so no stale copy ships. This&lt;/span&gt;
&lt;span class="c1"&gt;// task has no inputs or outputs, so it runs on every build, and it prints the warning when&lt;/span&gt;
&lt;span class="c1"&gt;// embed-root/android is missing. ---&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;warnIfNothingToEmbed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'warnIfNothingToEmbed'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;doLast&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;embedSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"warning: ${embedSource} does not exist, so no remotes are embedded. Run 'node tools/build-cdn.mjs' before a release build."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sourceSets&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcDirs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;embeddedAssetsDir&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'preBuild'&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;configure&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dependsOn&lt;/span&gt; &lt;span class="n"&gt;embedRemotes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warnIfNothingToEmbed&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An asset is not a file on disk, and Re.Pack reaches a copy's &lt;code&gt;&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code&gt; directory only through a real file path. So a small TurboModule copies &lt;code&gt;assets/cdn&lt;/code&gt; out to the app's files directory and resolves with the directory it used. Its spec is short enough to read whole:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TurboModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TurboModuleRegistry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- Android's half of the copy in the binary. A Gradle task packs each remote's embedded version&lt;/span&gt;
&lt;span class="c1"&gt;// into the APK's assets, and assets are not files on disk: Re.Pack's file:// loader needs a real&lt;/span&gt;
&lt;span class="c1"&gt;// path. prepare() copies them out to the app's files directory once per installed build, keeping&lt;/span&gt;
&lt;span class="c1"&gt;// the &amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/ layout, and resolves with the directory the host builds file:// URLs&lt;/span&gt;
&lt;span class="c1"&gt;// against.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// There is no iOS implementation, because iOS reads the copies straight from the .app. That is why&lt;/span&gt;
&lt;span class="c1"&gt;// this uses TurboModuleRegistry.get, which returns null where the module does not exist, rather&lt;/span&gt;
&lt;span class="c1"&gt;// than getEnforcing, which would throw on iOS the moment this file loads. ---&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Spec&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TurboModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** Copy the embedded remotes out, once per installed build, and return the directory. */&lt;/span&gt;
  &lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TurboModuleRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EmbeddedRemotesModule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kotlin side copies the tree once per install. A marker file holds the app version and the time the APK was installed or updated, so a relaunch reuses the extracted tree, and any new install replaces it, including a rebuilt APK that kept its version number. It copies the bytes as they are, since each bundle's signature covers them. Take the module, its spec and the updated &lt;code&gt;HostNativePackage&lt;/code&gt;, which registers it next to &lt;a href="https://warrendeleon.com/blog/native-handoff-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-fallbacks" rel="noopener noreferrer"&gt;post 13&lt;/a&gt;'s navigation module, from the end tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/specs/NativeEmbeddedRemotesModule.ts apps/host/specs/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/android/app/src/main/java/com/host/&lt;span class="o"&gt;{&lt;/span&gt;EmbeddedRemotesModule,HostNativePackage&lt;span class="o"&gt;}&lt;/span&gt;.kt apps/host/android/app/src/main/java/com/host/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where the copy goes&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;.app&lt;/code&gt;, at &lt;code&gt;cdn/ios/&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the APK's assets, at &lt;code&gt;cdn/android/&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code&gt;, copied out to the files directory at launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What puts it there&lt;/td&gt;
&lt;td&gt;the last Run Script phase, &lt;code&gt;embed-remotes-ios.sh&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;embedRemotes&lt;/code&gt; Sync task, before &lt;code&gt;preBuild&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How the host finds it&lt;/td&gt;
&lt;td&gt;the directory of &lt;code&gt;main.jsbundle&lt;/code&gt;, read from React Native's &lt;code&gt;SourceCode&lt;/code&gt; module&lt;/td&gt;
&lt;td&gt;the directory &lt;code&gt;EmbeddedRemotesModule.prepare()&lt;/code&gt; resolves with&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Without &lt;code&gt;embed-root/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a warning, a successful build, no copy&lt;/td&gt;
&lt;td&gt;a warning, a successful build, no copy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Load a copy from the disk
&lt;/h2&gt;

&lt;p&gt;The host changes in five files and their tests. Take them from the end tag. &lt;code&gt;mf-modules.d.ts&lt;/code&gt; goes, because nothing loads a remote with &lt;code&gt;import()&lt;/code&gt; any more (the Try again section says why), and the design system gains a contrast check for the banner's new colour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/src/shell/&lt;span class="o"&gt;{&lt;/span&gt;remoteLocator,scriptManager,federationErrors&lt;span class="o"&gt;}&lt;/span&gt;.ts /tmp/pokedex-ref-16/apps/host/src/shell/FederationBanner.tsx apps/host/src/shell/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/App.tsx /tmp/pokedex-ref-16/apps/host/jest.config.js apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/__mocks__/&lt;span class="o"&gt;{&lt;/span&gt;module-federation-runtime,partyApp-partySlice&lt;span class="o"&gt;}&lt;/span&gt;.js apps/host/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/apps/host/__tests__/&lt;span class="o"&gt;{&lt;/span&gt;remoteLocator,scriptManager&lt;span class="o"&gt;}&lt;/span&gt;.test.ts /tmp/pokedex-ref-16/apps/host/__tests__/&lt;span class="o"&gt;{&lt;/span&gt;App,RemoteBoundary,FederationBanner&lt;span class="o"&gt;}&lt;/span&gt;.test.tsx apps/host/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-16/packages/ui/src/tokens/__tests__/contrast.accessibility.ts packages/ui/src/tokens/__tests__/
&lt;span class="nb"&gt;rm &lt;/span&gt;apps/host/mf-modules.d.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host first has to know where the copies are on the device. On iOS the answer comes from React Native's own &lt;code&gt;SourceCode&lt;/code&gt; module: a release build loads &lt;code&gt;file:///…/Host.app/main.jsbundle&lt;/code&gt;, and the directory above that file is the &lt;code&gt;.app&lt;/code&gt;. A development build loads its bundle from the dev server over http, so there is no directory to derive and no copy to use. &lt;strong&gt;On iOS, everything that runs from a copy needs a release build.&lt;/strong&gt; On Android the directory is whatever &lt;code&gt;prepare()&lt;/code&gt; resolved with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sourceCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NativeModules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SourceCode&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scriptURL&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;getConstants&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scriptURL&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCRIPT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sourceCode&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;scriptURL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;sourceCode&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;getConstants&lt;/span&gt;&lt;span class="p"&gt;?.().&lt;/span&gt;&lt;span class="nx"&gt;scriptURL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SCRIPT_URL&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file://&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;SCRIPT_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^file:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/[^/]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;embeddedRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;APP_PATH&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every script of a remote still goes through post 15's resolver. It gains one branch: a remote that runs from its copy, in a launch that could not use the CDN's versions, or after that one remote failed to load from the CDN, resolves to a file inside its copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`file://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddedRoot&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cdn/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verifyScriptSignature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;absolute: true&lt;/code&gt; is the detail that is easy to miss. Without it, Re.Pack keeps only the script's file name and looks for it at the top level of the app: iOS asks the app bundle's resources through &lt;code&gt;URLForResource:withExtension:&lt;/code&gt;, and Android opens an asset of that name. Neither reaches a &lt;code&gt;&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/&lt;/code&gt; directory. With it, Re.Pack reads the file at the path as given.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification stays on.&lt;/strong&gt; Re.Pack's file loader on both platforms runs the same signature check as its download path, before the code runs, on every load. That makes the copy stricter than a cached download, which post 15 noted is run from the disk without a second check. It is also why the copy is never rewritten on its way into the app: change one byte of an installed copy's code and the tab refuses it, failing the same hash check as a tampered download. On iOS the file loader reports the error's full description, so the reason arrives behind the system's generic prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Error: The operation couldn’t be completed. The bundle verification failed because the bundle hash is invalid.]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Android the message reads as it does for a tampered download, with no prefix.&lt;/p&gt;

&lt;p&gt;The manifest needs no special handling. The copy carries &lt;code&gt;mf-manifest.json&lt;/code&gt; beside its bundles, and the federation runtime reads it from a &lt;code&gt;file://&lt;/code&gt; URL the way it reads one from the CDN, because React Native's &lt;code&gt;fetch&lt;/code&gt; opens local files: through its file request handler on iOS, and on Android through the blob module's handler, which takes any URL that is not http or https when the response type is blob, the type React Native's &lt;code&gt;fetch&lt;/code&gt; asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundled mode: the day the CDN is unreachable
&lt;/h2&gt;

&lt;p&gt;The launch now prepares the copies alongside the probe, since neither needs the other, and a launch with no usable map runs from them instead of giving up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;CDN_CONFIGURED&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;fetchVersionMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;prepareEmbeddedCopies&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runFromCopies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CDN_CONFIGURED&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no usable version map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no CDN configured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- The launch that could not use the CDN: no usable map, no CDN configured, or a registration&lt;/span&gt;
&lt;span class="c1"&gt;// that failed. With copies in the binary it runs from them, every remote registered at its copy's&lt;/span&gt;
&lt;span class="c1"&gt;// manifest, which the runtime reads from the disk. With none, or when registering them fails too,&lt;/span&gt;
&lt;span class="c1"&gt;// there is nothing to run. ---&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runFromCopies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FederationStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_NAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasEmbeddedCopy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unresolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bundledVersions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]]));&lt;/span&gt;
  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bundled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the copy in the binary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embedded&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;registerRemotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;embeddedManifestUrlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[federation] the copies could not be registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unresolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remotes could not be registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registering each remote at its copy's manifest, with post 15's &lt;code&gt;force&lt;/code&gt;, sends the runtime to the disk for everything about that remote. From there nothing else is special: the runtime fetches the manifest, the resolver sends every script to the copy, and each one is verified as it loads. &lt;code&gt;unresolved&lt;/code&gt; remains for a launch whose binary records no copy with a directory to load from, or where registering the copies fails.&lt;/p&gt;

&lt;p&gt;Try it by taking the CDN away. Start the server as post 15 did, then make the Release build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx http-server@14.1.1 cdn-root &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;-c-1&lt;/span&gt; &lt;span class="nt"&gt;--cors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The banner reads &lt;code&gt;cdn · listApp 1.2.0 · partyApp 1.0.0&lt;/code&gt;. Stop the server and check that the map is gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null http://localhost:8000/ios/maps/2.0.0/version-map.json&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;7&lt;/code&gt; means curl could not connect. Close the app and open it again. The probe fails within its 1.5 seconds, the banner turns purple and reads &lt;code&gt;bundled · listApp 1.2.0 · partyApp 1.0.0&lt;/code&gt;, and both tabs open from the copies, with nothing asked of the network except PokéAPI for the Pokémon and GitHub for their artwork.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7it67ntmfznlzs2ly88g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7it67ntmfznlzs2ly88g.webp" alt="The Release build with the CDN stopped: the Pokédex tab lists Pokémon from Bulbasaur onwards under the chip listApp 1.2.0, and the purple banner above the tab bar reads bundled, listApp 1.2.0, partyApp 1.0.0" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The copy is frozen at build time, and that is the trade-off. A binary built today and launched without the CDN next year runs today's versions, whatever the CDN has shipped since. That is the part of post 1's requirement a copy can meet: the reviewed code runs without the CDN. The Pokémon still come from PokéAPI, so with no network at all the list's request fails and the screen shows its own error, "Couldn't reach PokéAPI."&lt;/p&gt;

&lt;p&gt;Nothing about the failure is remembered either. The next launch asks the CDN again, and if a usable map arrives and its versions register, the app is back on the versions the map names.&lt;/p&gt;

&lt;h2&gt;
  
  
  A remote that fails in a CDN launch
&lt;/h2&gt;

&lt;p&gt;Bundled mode covers a launch that could not use the CDN's versions. The other failure is narrower: the launch uses the CDN's versions, and then one remote fails. The cause might be a version retired from the CDN while a map still names it, a chunk cut off halfway, or a release that throws while its module initialises, which was post 15's third break. In post 15's build each of those leaves one dead tab. Two nets turn them into a tab that runs from its copy, while the other remote stays on the CDN.&lt;/p&gt;

&lt;h3&gt;
  
  
  The manifest net
&lt;/h3&gt;

&lt;p&gt;Module Federation fetches a remote's &lt;code&gt;mf-manifest.json&lt;/code&gt; before any of its code loads. A tab's boundary would hear of that fetch failing, but only once the runtime's own fetch gives up, and the runtime sets no time limit on it. And not every load runs inside a boundary: the party's state and styles modules ask for its manifest at boot, from an effect outside every tab. So the first net sits where the runtime asks for every manifest: a runtime plugin's &lt;code&gt;fetch&lt;/code&gt; hook, which the runtime calls before its own fetch, and which may answer with a &lt;code&gt;Response&lt;/code&gt; or hand the request back by returning nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embeddedFallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModuleFederationRuntimePlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embedded-fallback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;manifestRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_NAMES&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;hasEmbeddedCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallbackRemotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embeddedManifestUrlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;cdnManifestOrEmbedded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nf"&gt;registerPlugins&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;embeddedFallback&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A CDN remote's manifest is fetched from the CDN, within the probe's 1.5-second limit. Any failed request (an error status such as a 404, a timeout or a dropped connection) drops that one remote to its copy and answers with the copy's manifest instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cdnManifestOrEmbedded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;PROBE_TIMEOUT_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[federation] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; manifest returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[federation] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; manifest could not be fetched`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;fallBack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embeddedManifestUrlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A manifest that arrives with a success status goes back to the runtime as it is, even one that cannot be read. The runtime fails on that afterwards, and for a tab's load the boundary net catches the failure.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fallBack&lt;/code&gt; adds the remote to a set and puts it on the banner, as &lt;code&gt;listApp 1.2.0 embedded&lt;/code&gt;. The resolver reads that same set for every script, so the remote's very next script resolves to its copy, while every other remote keeps its CDN URL. &lt;strong&gt;The set lives in memory on purpose&lt;/strong&gt;: a remote drops to its copy only until the next launch, which asks the CDN again, so a failure that was only the network does not keep it there. Remembering failures across launches, and rolling a bad version back permanently, belongs to post 17.&lt;/p&gt;

&lt;p&gt;Other tools already offer parts of this, and each would work at its own layer. Module Federation's &lt;code&gt;errorLoadRemote&lt;/code&gt; hook can return a fallback when a load fails, the manifest included. Its official retry plugin retries a failed load and can rotate between backup domains, and Re.Pack's script locator takes &lt;code&gt;retry&lt;/code&gt; and &lt;code&gt;retryDelay&lt;/code&gt;. The &lt;code&gt;fetch&lt;/code&gt; hook was the fit here for two reasons. &lt;code&gt;errorLoadRemote&lt;/code&gt; hears of a failure only after the runtime's fetch gives up, as the boundary does, while the &lt;code&gt;fetch&lt;/code&gt; hook makes the request itself and can put the probe's time limit on it. And retrying is a different policy: it spends time asking the same CDN again, where this net answers at once from a copy the binary already has and leaves the next attempt to the next launch. Retries could sit in front of the net, at the cost of a longer wait before the copy.&lt;/p&gt;

&lt;p&gt;Try the net with a version the CDN no longer holds. Start the server again, move the list's version out of it, and cold-start the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;cdn-root/ios/listApp/1.2.0 /tmp/listApp-1.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list's manifest comes back 404, the list runs from its copy, and the party still loads from the CDN, which the server's log shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-09-29T14:40:27.102Z]  "GET /ios/listApp/1.2.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-29T14:40:27.103Z]  "GET /ios/listApp/1.2.0/mf-manifest.json" Error (404): "Not found"
[2026-09-29T14:40:27.120Z]  "GET /ios/partyApp/1.0.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-29T14:40:27.159Z]  "GET /ios/partyApp/1.0.0/partyApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwhs2awdvfgsw9h1g1et.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwhs2awdvfgsw9h1g1et.webp" alt="The Release build with the list's version missing from the CDN: the Pokédex tab lists Pokémon from Bulbasaur onwards under the chip listApp 1.2.0, and the blue banner reads cdn, listApp 1.2.0 embedded, partyApp 1.0.0" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Move the directory back before going on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/listApp-1.2.0 cdn-root/ios/listApp/1.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;If every remote drops to its copy, check that the app can reach the server.&lt;/strong&gt; A banner that marks both remotes embedded, or reads &lt;code&gt;bundled&lt;/code&gt;, while the server is running usually means the app could not reach it: iOS's App Transport Security and Android's cleartext rules refuse plain http if the build does not allow it. This host allows local networking on iOS and, on Android, cleartext only to &lt;code&gt;localhost&lt;/code&gt;, &lt;code&gt;127.0.0.1&lt;/code&gt; and &lt;code&gt;10.0.2.2&lt;/code&gt;, with their subdomains.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The boundary net
&lt;/h3&gt;

&lt;p&gt;Some failures come after a manifest that arrived fine: a container or a chunk that does not arrive or does not verify, or a module that throws while it is evaluated. They fail the tab's load, and the tab's load runs inside the &lt;code&gt;RemoteBoundary&lt;/code&gt; that &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-fallbacks" rel="noopener noreferrer"&gt;post 11&lt;/a&gt; put around each tab. The boundary is the second net, and it stays a class component, because React has no way yet to write an error boundary as a function component. It now asks one question before it shows the error state: did the load fail, so the remote never produced a component, and is there a copy this remote has not dropped to yet?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;componentDidCatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropsToCopy&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;fallBackAndReload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextAttempt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A failure the copy in the binary can answer: the load failed, so the remote never produced a&lt;/span&gt;
&lt;span class="c1"&gt;// component, and this remote has a copy it has not dropped to yet.&lt;/span&gt;
&lt;span class="nf"&gt;dropsToCopy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;canFallBack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it can, the boundary drops the remote to its copy and loads the tab again, rendering the loading state in the meantime, so the error state never appears. &lt;code&gt;canFallBack&lt;/code&gt; allows it once per remote, only in a CDN launch, and only for a remote that has a copy. When it cannot, because the copy failed too, or there is none, the tab shows the error state as before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A remote whose load returned a component keeps its error state when that component throws.&lt;/strong&gt; That is a decision, and it rests on whether the remote produced a component. Before it does, the tab is still showing its loading state, so a swap to the copy is invisible. Once the load has returned a component, the boundary keeps that version, even when the component throws on its very first render. It records the attempt whose load arrived with a component, not whether the screen was ever shown, so a failure in that attempt counts as thrown by the remote's own code, which has already run in this session. Until the map names a different version, the copy is that same version anyway. So the boundary shows "This tab stopped working" and offers Try again, which renders the remote again and recovers an error that does not repeat. An error that repeats leaves the tab on its error state for the rest of the session, and after a relaunch for as long as the map names that version, even with a working copy in the binary.&lt;/p&gt;

&lt;p&gt;The boundary net does not cover the party's two boot modules: they load from an effect outside every tab, so the host only logs a failure the manifest net does not answer. When the state module fails, adding to the party stays disabled until the Party tab loads, because that tab injects the state itself; a failed styles module does not affect adding.&lt;/p&gt;

&lt;p&gt;Post 15's third break is now a failed load the copy can answer. Its release throws while its module initialises, and post 15's evaluation window turns that throw into a failed load instead of a fatal report. Build it again. Add the same line below the imports of &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokedexScreen failed to initialise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ship it as &lt;code&gt;1.4.0&lt;/code&gt;, the way post 15 did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_REMOTE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.4.0 npm run bundle:ios:prod &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s1"&gt;'*.map'&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; mf-stats.json apps/list/cdn/ios/listApp/1.4.0/ cdn-root/ios/listApp/1.4.0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the line, point the 2.0.0 map at &lt;code&gt;1.4.0&lt;/code&gt; and relaunch. The tab drops to the 1.2.0 copy: the chip says 1.2.0, the banner marks the list embedded, and the simulator's log has no &lt;code&gt;RCTFatal&lt;/code&gt; in it. Put the map back to &lt;code&gt;1.2.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The boundary net also catches a CDN that disappears mid-session. Launch with the server running and leave the Party tab unopened. Add a Pokémon from the Pokédex, stop the server, then open Party for the first time. Only the party's state and styles modules loaded at boot, so its screens are not on the device yet: the download fails, and the tab shows a spinner for a moment and then the party from its copy, with the Pokémon you added and the banner reading &lt;code&gt;partyApp 1.0.0 embedded&lt;/code&gt;. Removing the Pokémon there works too, because the copy's screens and the state module that loaded from the CDN share the host's one store.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Try again that retries
&lt;/h2&gt;

&lt;p&gt;In post 15's build, Try again on a tab whose load failed never brings the tab back. Three records of the failed load stand between Try again and a fresh attempt.&lt;/p&gt;

&lt;p&gt;The first is React's. A lazy component calls its load function once: the React documentation says "Both the returned Promise and the Promise's resolved value will be cached, so React will not call load more than once", and a rejection goes to the nearest error boundary. The boundary already handled that, by mounting a new lazy component under a new key for every attempt.&lt;/p&gt;

&lt;p&gt;The second is the host bundle's, and it is the one that kept post 15's tabs dead. An &lt;code&gt;import('listApp/ListStack')&lt;/code&gt; compiles into a module of the host's own bundle, and the bundle keeps each module it has evaluated for the rest of the session. After a failed load it keeps what the failure left behind, a module with nothing in it, and every later &lt;code&gt;import()&lt;/code&gt; settles with that, even when the retry downloads the remote successfully. Measured on an iOS Release build: the retry fetched the manifest and both chunks, and the import still resolved without a component. So the tabs, and the two modules the party loads at boot, no longer use &lt;code&gt;import()&lt;/code&gt;. They ask the federation runtime directly, every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadRemoteModule&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;loadRemote&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loadFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asks for the module's factory rather than its exports, with &lt;code&gt;loadFactory: false&lt;/code&gt;, which is how post 15's evaluation window gets to wrap it: the runtime hands back the window's wrapper, and calling it here evaluates the module inside the window.&lt;/p&gt;

&lt;p&gt;The third is the federation runtime's. In &lt;code&gt;@module-federation/runtime-core&lt;/code&gt; 2.9.0, the version this series installs, its record of a remote outlives a failed load: it keeps the remote's entry, the manifest it read, the container it loaded, and a container load that failed, which it hands back to every later request. The remote's chunk registry, a global array named after the remote's &lt;code&gt;uniqueName&lt;/code&gt;, also keeps every chunk its last container fetched, and a new container installs all of them before fetching anything. Try again clears both before it loads again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reloadRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RemoteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;registerRemotes&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[federation] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; could not be registered again`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;delete &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nx"&gt;CHUNK_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registering the remote again with &lt;code&gt;force&lt;/code&gt; removes the runtime's record, the container's global included, and points the next load at &lt;code&gt;entry&lt;/code&gt;. Try again reads &lt;code&gt;entry&lt;/code&gt; back from the runtime instead of building it, which works in every mode, including development, where only the build knows the dev server's URL. A remote that dropped to its copy still retries its copy, because the manifest net and the resolver both send any remote in the set to its copy.&lt;/p&gt;

&lt;p&gt;Re.Pack's script cache needs no clearing for a failed load: once a resolved script's load settles, Re.Pack drops its promise, and a download that fails verification is never written to its cache, on either platform. A refusal from the resolver is the exception. It comes before that cleanup, so Re.Pack keeps the rejection and hands it back to every later request for the same script. This host's resolver refuses only a remote it has no version or copy for.&lt;/p&gt;

&lt;p&gt;One gap stays open: Try again cannot cancel a download already in flight. A chunk the failed container was still fetching lands in whichever registry exists when it arrives, and when a script is still loading, Re.Pack answers a new request for it with the promise already outstanding. When the copy is the version the CDN was serving, the default, those are the same bytes either way; when the map names a different version, a late chunk from the CDN can land beside the copy's container.&lt;/p&gt;

&lt;p&gt;A render error shows Try again recovering a tab whose code has already loaded. The boundary net's mid-session demo stopped the server, so start it again and leave it running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx http-server@14.1.1 cdn-root &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;-c-1&lt;/span&gt; &lt;span class="nt"&gt;--cors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then make the list's screen throw while it renders, for its first eight seconds. Below the imports of &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt; add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOADED_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and as the first lines of &lt;code&gt;PokedexScreen()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;LOADED_AT&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokedexScreen render failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The throw has to last: React retries a render that fails, so an error that stops too soon recovers before the boundary's error state is ever shown. Ship it as its own version, the way 1.4.0 shipped, then delete the lines and point the 2.0.0 map at &lt;code&gt;1.5.0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_REMOTE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.5.0 npm run bundle:ios:prod &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s1"&gt;'*.map'&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; mf-stats.json apps/list/cdn/ios/listApp/1.5.0/ cdn-root/ios/listApp/1.5.0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relaunch and the tab shows "This tab stopped working". Wait out the eight seconds and press Try again: the list renders on 1.5.0, and the server's log shows no new request, because the code had loaded and only needed rendering again. Put the map back to &lt;code&gt;1.2.0&lt;/code&gt; afterwards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/offline-fallback-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-fallbacks#a-try-again-that-retries" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The failures and what each one costs, side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Caught by&lt;/th&gt;
&lt;th&gt;What the user sees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The CDN cannot be reached at launch&lt;/td&gt;
&lt;td&gt;the launch, in bundled mode&lt;/td&gt;
&lt;td&gt;every tab from its copy, and a purple banner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A remote's manifest fails: a 404, a timeout, a dropped connection&lt;/td&gt;
&lt;td&gt;the manifest net&lt;/td&gt;
&lt;td&gt;the tab from its copy, marked &lt;code&gt;embedded&lt;/code&gt; on the banner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A tab's remote code does not arrive, does not verify, or throws as its module initialises&lt;/td&gt;
&lt;td&gt;the boundary net&lt;/td&gt;
&lt;td&gt;a moment of loading, then the tab from its copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The party's state module fails at boot, once its manifest has arrived&lt;/td&gt;
&lt;td&gt;nothing: the host logs it&lt;/td&gt;
&lt;td&gt;adding to the party stays disabled until the Party tab loads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The party's styles module fails at boot, once its manifest has arrived&lt;/td&gt;
&lt;td&gt;nothing: the host logs it&lt;/td&gt;
&lt;td&gt;adding still works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The copy fails as well, or there is no copy&lt;/td&gt;
&lt;td&gt;the boundary&lt;/td&gt;
&lt;td&gt;"This tab could not load", with Try again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The remote's component throws as it renders&lt;/td&gt;
&lt;td&gt;the boundary&lt;/td&gt;
&lt;td&gt;"This tab stopped working", with Try again&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;Both embed steps succeed without &lt;code&gt;embed-root/&lt;/code&gt;, and that is the trap. Move it out and rebuild the Release build, this time with &lt;code&gt;--verbose&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;embed-root /tmp/embed-root &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build succeeds, and the phase's warning is the only sign that anything is missing. Without &lt;code&gt;--verbose&lt;/code&gt;, &lt;code&gt;npm run ios&lt;/code&gt; shows a spinner in place of the build's output (or passes the output through &lt;code&gt;xcbeautify&lt;/code&gt; or &lt;code&gt;xcpretty&lt;/code&gt;, when one is installed), then reports &lt;code&gt;success Successfully built the app&lt;/code&gt; and launches. With &lt;code&gt;--verbose&lt;/code&gt;, the warning sits among the build's own lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;debug warning: …/embed-root/ios does not exist, so no remotes are embedded. Run 'node tools/build-cdn.mjs' before a release build.
debug ** BUILD SUCCEEDED ** [11.716 sec]
success Successfully built the app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop the server and cold-start the app. The banner still says &lt;code&gt;bundled · listApp 1.2.0 · partyApp 1.0.0&lt;/code&gt;, because the host compiled in the versions it was told it carries, and both tabs say "The app's own copy of this remote could not be loaded".&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Build the CDN tree, then the release build, every time.&lt;/strong&gt; The embed phases copy whatever &lt;code&gt;embed-root/&lt;/code&gt; holds when they run, and when it is missing they let the build succeed with nothing more than a warning in the build's output. A release build made before &lt;code&gt;node tools/build-cdn.mjs&lt;/code&gt;, or after &lt;code&gt;embed-root/&lt;/code&gt; was cleaned, builds successfully with no copy in it, and nothing fails until the app needs a copy: when the launch cannot use the CDN's versions, or when one remote fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Move it back and rebuild before going on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/embed-root embed-root &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The copies and the CDN tree, then the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/gen-signing-keys.mjs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node tools/build-cdn.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx http-server@14.1.1 cdn-root &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;-c-1&lt;/span&gt; &lt;span class="nt"&gt;--cors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Release build on iOS, then Android with the emulator's address for the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://10.0.2.2:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run android &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With either running, stop the server and cold-start for bundled mode, or move one version out of &lt;code&gt;cdn-root&lt;/code&gt; for the manifest net. On Android the extracted copies sit in the app's files directory, under &lt;code&gt;files/cdn/android/&lt;/code&gt;, with the &lt;code&gt;.installed&lt;/code&gt; marker beside them in &lt;code&gt;files/cdn/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The suites, the smoke test and the key generator's tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;apps/host apps/list apps/party packages/ui&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx jest &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sh scripts/federation-smoke.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node &lt;span class="nt"&gt;--test&lt;/span&gt; tools/gen-signing-keys.test.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;The demos in this post ran on an iOS Release build, and the copies, their extraction and bundled mode on an Android release build as well.&lt;/p&gt;

&lt;p&gt;A remote that fell back is forgotten at the next launch, so a version that keeps failing fails again on every launch before its copy takes over. Remembering failures belongs with the other open problem: the map every binary trusts is still a plain text file anyone who can write to the bucket can change.&lt;/p&gt;

&lt;p&gt;Next: signing that map, a counter that makes the app reject an older map, and an app that rolls a failing version back by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://react.dev/reference/react/lazy" rel="noopener noreferrer"&gt;React: lazy&lt;/a&gt; — the load function called once, its promise and resolved value cached, and a rejection thrown to the nearest error boundary&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react/blob/v19.2.3/packages/react/src/ReactLazy.js" rel="noopener noreferrer"&gt;React source: ReactLazy at 19.2.3&lt;/a&gt; — a rejected load stored on the lazy component and thrown again on every later render&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary" rel="noopener noreferrer"&gt;React: Component, catching rendering errors with an error boundary&lt;/a&gt; — "There is currently no way to write an Error Boundary as a function component"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/guide/runtime/runtime-hooks" rel="noopener noreferrer"&gt;Module Federation: runtime hooks&lt;/a&gt; — the &lt;code&gt;fetch&lt;/code&gt; hook for manifest requests, returning &lt;code&gt;Promise&amp;lt;Response&amp;gt; | void | false&lt;/code&gt;, and &lt;code&gt;errorLoadRemote&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/module-federation/core/blob/v2.9.0/packages/runtime-core/src/plugins/snapshot/SnapshotHandler.ts" rel="noopener noreferrer"&gt;Module Federation source: runtime-core SnapshotHandler at 2.9.0&lt;/a&gt; — a manifest asked of the &lt;code&gt;fetch&lt;/code&gt; hook first, then of the global &lt;code&gt;fetch&lt;/code&gt;, then of &lt;code&gt;errorLoadRemote&lt;/code&gt; on failure&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/module-federation/core/blob/v2.9.0/packages/runtime-core/src/remote/index.ts" rel="noopener noreferrer"&gt;Module Federation source: runtime-core remote/index.ts&lt;/a&gt; and &lt;a href="https://github.com/module-federation/core/blob/v2.9.0/packages/runtime-core/src/utils/load.ts" rel="noopener noreferrer"&gt;utils/load.ts at 2.9.0&lt;/a&gt; — &lt;code&gt;registerRemotes&lt;/code&gt; with &lt;code&gt;force&lt;/code&gt; calling &lt;code&gt;removeRemote&lt;/code&gt;, which deletes the remote's entry, manifest, container global and container-loading promise; a failed container load's promise kept and returned to later loads until then&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/plugin/plugins/retry-plugin" rel="noopener noreferrer"&gt;Module Federation: retry plugin&lt;/a&gt; — retries and backup domains for failed loads&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/modules/ScriptManager/types.ts" rel="noopener noreferrer"&gt;Re.Pack source: ScriptManager types at 5.2.5&lt;/a&gt; — the locator's &lt;code&gt;absolute&lt;/code&gt;, &lt;code&gt;retry&lt;/code&gt; and &lt;code&gt;retryDelay&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/modules/ScriptManager/ScriptManager.ts" rel="noopener noreferrer"&gt;Re.Pack source: ScriptManager.ts at 5.2.5&lt;/a&gt; — a resolved script's promise deleted once its load settles, failed or not; a resolver's rejection comes before that cleanup and is kept&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/ios/ScriptManager.mm" rel="noopener noreferrer"&gt;Re.Pack source: ScriptManager.mm at 5.2.5&lt;/a&gt; — a file:// script read at its absolute path, or by name through &lt;code&gt;URLForResource:withExtension:&lt;/code&gt;, and verified before it runs; a download written to the cache only after it verifies&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/android/src/main/java/com/callstack/repack/FileSystemScriptLoader.kt" rel="noopener noreferrer"&gt;Re.Pack source: FileSystemScriptLoader.kt&lt;/a&gt; and &lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/android/src/main/java/com/callstack/repack/RemoteScriptLoader.kt" rel="noopener noreferrer"&gt;RemoteScriptLoader.kt at 5.2.5&lt;/a&gt; — the same on Android, with an asset opened by its file name when the path is not absolute, and a download verified before it is written&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/foundation/bundle/url(forresource:withextension:)" rel="noopener noreferrer"&gt;Apple: URLForResource:withExtension:&lt;/a&gt; — a resource looked up by name in the bundle, beside the variant that takes a subdirectory&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swiftlang/swift-build/blob/swift-6.2.1-RELEASE/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ShellScriptTaskProducer.swift" rel="noopener noreferrer"&gt;swift-build source: ShellScriptTaskProducer.swift at swift-6.2.1-RELEASE&lt;/a&gt; — a Run Script phase that declares no outputs, or is set to run always, runs on every build&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/react-native-community/cli/blob/v20.2.0/packages/cli-platform-apple/src/commands/buildCommand/buildProject.ts" rel="noopener noreferrer"&gt;React Native CLI source: buildProject.ts at 20.2.0&lt;/a&gt; — without &lt;code&gt;--verbose&lt;/code&gt;, the build's output piped through &lt;code&gt;xcbeautify&lt;/code&gt; or &lt;code&gt;xcpretty&lt;/code&gt; when one is installed, and a spinner otherwise&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/React/CoreModules/RCTSourceCode.mm" rel="noopener noreferrer"&gt;React Native source: RCTSourceCode.mm at 0.85.3&lt;/a&gt; — &lt;code&gt;scriptURL&lt;/code&gt;, the URL the JavaScript bundle loaded from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm" rel="noopener noreferrer"&gt;React Native source: RCTFileRequestHandler.mm&lt;/a&gt; and &lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm" rel="noopener noreferrer"&gt;RCTAppSetupUtils.mm at 0.85.3&lt;/a&gt; — file:// requests answered on iOS, and the handler registered with the app's networking&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt" rel="noopener noreferrer"&gt;React Native source: BlobModule.kt at 0.85.3&lt;/a&gt; — any URL that is not http or https, read on Android when the response type is &lt;code&gt;blob&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/reference/android/content/res/AssetManager" rel="noopener noreferrer"&gt;Android: AssetManager&lt;/a&gt; — assets opened and listed from the APK, not as files&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Sync.html" rel="noopener noreferrer"&gt;Gradle: Sync&lt;/a&gt; — a copy that also removes files the source no longer has&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-16-fallbacks&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>CDN delivery: the version map, the resolver, and the live flip</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 12 Oct 2026 00:03:52 +0000</pubDate>
      <link>https://dev.to/warrendeleon/cdn-delivery-the-version-map-the-resolver-and-the-live-flip-3i1c</link>
      <guid>https://dev.to/warrendeleon/cdn-delivery-the-version-map-the-resolver-and-the-live-flip-3i1c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/production-build-three-modes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;Post 14&lt;/a&gt; ended on a promise: "versioned remotes on the CDN, a version map, a per-launch resolver, and an old binary that never downloads code it cannot run." This post builds that, then uses it. An app sits installed with its dev servers off, showing version 1.1.0 of the Pokédex list. You ship it 1.2.0 with one directory and one edited line of JSON, watch the change arrive at its next launch, and roll it back the same way.&lt;/p&gt;

&lt;p&gt;The operation needs three pieces. A content delivery network (CDN) keeps every published version of each remote, so a new version never replaces files an installed app is loading. A map for each released app version tells that binary which of those versions to load. A resolver in the host turns every chunk request into a versioned, signature-verified URL. Together they deliver what &lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;post 1&lt;/a&gt; said federation gives you: "A bug in one remote is a re-upload of that remote, not a store submission."&lt;/p&gt;

&lt;p&gt;One rule shapes all of it: &lt;strong&gt;an old binary must keep working for as long as anyone has it installed.&lt;/strong&gt; The CDN keeps old remote versions the way a backend keeps old endpoints, and each binary is handed only the versions its own map names. Nothing on the device checks that those versions work with the binary: choosing versions it has been tested with is the operator's job, and the map is where that choice is written down.&lt;/p&gt;

&lt;p&gt;Start from post 14's finished state, the tag &lt;code&gt;post-14-production-build&lt;/code&gt;; this post finishes at the tag &lt;code&gt;post-15-cdn-flip&lt;/code&gt;. You type the configuration changes and the smaller edits yourself. The host's new launch code and a handful of other files change in more places than a post can usefully retype, so those come from the end tag, and the copy commands name each one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/cdn-version-resolution-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The map is fetched before anything federated is imported, and every request after it carries the version the map named.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versions on the shelf
&lt;/h2&gt;

&lt;p&gt;A version has to reach three places in a remote's build, and they have to agree: the directory the artefacts are written to, the directory the chunks are written to, and the string the running screen prints about itself. One constant feeds all three. In &lt;code&gt;apps/list/rspack.config.mjs&lt;/code&gt;, above &lt;code&gt;defineRspackConfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Which version of this remote the build produces. It decides two things at once: the&lt;/span&gt;
&lt;span class="c1"&gt;// directory the artefacts are written to, and the string the running code reports about itself.&lt;/span&gt;
&lt;span class="c1"&gt;// Both come from one variable, so a build cannot write 1.2.0's files and claim to be 1.1.0:&lt;/span&gt;
&lt;span class="c1"&gt;//   MF_REMOTE_VERSION=1.2.0 npm run bundle:ios:prod&lt;/span&gt;
&lt;span class="c1"&gt;// A version directory is written once and never edited afterwards. Rebuilding a version that&lt;/span&gt;
&lt;span class="c1"&gt;// installed apps are already loading replaces code those apps treat as fixed, which is the one&lt;/span&gt;
&lt;span class="c1"&gt;// move this layout exists to make unnecessary: ship a new version instead. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MF_REMOTE_VERSION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post 14's two production paths each gain the segment. In &lt;code&gt;output&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A production build writes the tree the CDN serves, laid out as the URL path it is served&lt;/span&gt;
&lt;span class="c1"&gt;// at: cdn/&amp;lt;platform&amp;gt;/listApp/&amp;lt;version&amp;gt;/. The version segment is what lets one CDN hold&lt;/span&gt;
&lt;span class="c1"&gt;// several releases of this remote at once, each at its own URL. A development build keeps&lt;/span&gt;
&lt;span class="c1"&gt;// writing to build/, where the dev server reads it from, and carries no version: there is&lt;/span&gt;
&lt;span class="c1"&gt;// only ever one build there, and it is whatever was saved last.&lt;/span&gt;
&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cdn/[platform]/listApp/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/build/[platform]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in the &lt;code&gt;RepackPlugin&lt;/code&gt;'s &lt;code&gt;extraChunks&lt;/code&gt; entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The chunks land beside the container and the manifest, inside the same version&lt;/span&gt;
&lt;span class="c1"&gt;// directory, because the host will ask for them at URLs relative to the manifest it&lt;/span&gt;
&lt;span class="c1"&gt;// loaded. This entry copies them there: Rspack has already written them under&lt;/span&gt;
&lt;span class="c1"&gt;// output.path, so a missing version segment here breaks nothing at runtime, and&lt;/span&gt;
&lt;span class="c1"&gt;// instead leaves a second, unversioned copy of every chunk beside the versions.&lt;/span&gt;
&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`cdn/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/listApp/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`build/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/remote`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third place is a literal compiled into the bundle. Add &lt;code&gt;DefinePlugin&lt;/code&gt; to the &lt;code&gt;@rspack/core&lt;/code&gt; import beside the minimiser, and a plugin entry before &lt;code&gt;ModuleFederationPluginV2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DefinePlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SwcJsMinimizerRspackPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@rspack/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The version, compiled into the bundle as a literal so the running screen can print the&lt;/span&gt;
&lt;span class="c1"&gt;// build it came from. In production it is read from the same constant the output path uses,&lt;/span&gt;
&lt;span class="c1"&gt;// so the chip on screen and the directory on the CDN can never disagree. A development&lt;/span&gt;
&lt;span class="c1"&gt;// build says 'dev' instead: it was never published anywhere, and a number on it would be a&lt;/span&gt;
&lt;span class="c1"&gt;// version claim about a file that is rebuilt on every save.&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefinePlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;__REMOTE_VERSION__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mirror the two paths in &lt;code&gt;apps/party/rspack.config.mjs&lt;/code&gt; with &lt;code&gt;partyApp&lt;/code&gt;. Its constant carries a comment of its own, because the party has no chip and so needs no &lt;code&gt;DefinePlugin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Which version of this remote the build produces, and the directory it is written to. Same&lt;/span&gt;
&lt;span class="c1"&gt;// variable, same rule as listApp's: a version directory is written once and never edited after&lt;/span&gt;
&lt;span class="c1"&gt;// installed apps have started loading it. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MF_REMOTE_VERSION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back in the list app, TypeScript needs a declaration for &lt;code&gt;__REMOTE_VERSION__&lt;/code&gt;, because the name has no module behind it. A new &lt;code&gt;apps/list/src/globals.d.ts&lt;/code&gt; provides one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Names the bundler replaces with literals at build time, declared for the compiler. They&lt;/span&gt;
&lt;span class="c1"&gt;// are not imports and there is no module behind them: rspack.config.mjs substitutes each one&lt;/span&gt;
&lt;span class="c1"&gt;// during the build, so the shipped bundle contains the value and never the name. ---&lt;/span&gt;

&lt;span class="cm"&gt;/** The version of this remote the build produced, and the CDN directory it was written to. */&lt;/span&gt;
&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;__REMOTE_VERSION__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chip is what makes a deploy visible, because two builds of the same remote are otherwise the same screen. In &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt;, above &lt;code&gt;EMPTY_TYPES&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- The version this bundle was built at, compiled in by DefinePlugin. Showing it is what&lt;/span&gt;
&lt;span class="c1"&gt;// makes a deploy visible: two builds of this remote are otherwise the same screen, so without&lt;/span&gt;
&lt;span class="c1"&gt;// it there is no way to tell from the app which one is running. The fallback covers Jest, where&lt;/span&gt;
&lt;span class="c1"&gt;// no bundler runs and the name is never substituted. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;__REMOTE_VERSION__&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;__REMOTE_VERSION__&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header row becomes the chip on the left and the party counter on the right. The chip is its own accessible element, so a screen reader can reach it without hearing the party count first. The live region and its label move off the row onto a group of their own around post 12's label and pill, so the row no longer swallows the chip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;ListHeaderComponent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Box&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex-row items-center justify-between px-1.5 py-2.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Which build of this remote is on screen. Its own element rather than part of the
        counter's group, so a screen reader can reach it without it being read out every
        time the party count changes. */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded-full bg-offGrey px-2 py-0.5 dark:bg-white/10"&lt;/span&gt;
      &lt;span class="na"&gt;accessible&lt;/span&gt;
      &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`Pokédex remote, version &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"xs"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"font-semi text-darkGrey dark:text-lightGrey"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        listApp &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_VERSION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The count changes when the user adds a member from a screen away, without focus
        moving here. A sighted user sees the number tick; a screen-reader user is told
        nothing unless this is a live region (SC 4.1.3). The label spells the ratio out,
        because "3/6" is read as "three slash six" or as a date, depending on the reader. */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-row items-center gap-2"&lt;/span&gt;
      &lt;span class="na"&gt;accessible&lt;/span&gt;
      &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
      &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`My Party, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sm"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"font-semi text-darkGrey dark:text-lightGrey"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        My Party
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded-full bg-lightGreen px-2.5 py-0.5 dark:bg-white/10"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* darkGrey, not darkGreen: darkGreen is #A6D3A0, the grass fill, and on lightGreen
            it measures 1.53:1. darkGrey is the colour the label beside it already uses. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"xs"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"font-head text-darkGrey dark:text-pokemonGreen"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post 14's &lt;code&gt;tools/build-cdn.mjs&lt;/code&gt; built one version of each remote into a flat tree. Replace it with one that starts from two lists. &lt;code&gt;REMOTE_VERSIONS&lt;/code&gt; says which versions of each remote the CDN holds. &lt;code&gt;APP_VERSION_MAPS&lt;/code&gt; says which of those versions each released app version loads. A version can sit in the first list with no map pointing at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Assembles the directory a CDN would serve.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The layout is the URL layout, and it now has a version in it:&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//   cdn-root/&amp;lt;platform&amp;gt;/&amp;lt;remote&amp;gt;/&amp;lt;version&amp;gt;/     the container, its chunks and mf-manifest.json&lt;/span&gt;
&lt;span class="c1"&gt;//   cdn-root/&amp;lt;platform&amp;gt;/maps/&amp;lt;appVersion&amp;gt;/      version-map.json, one per released app version&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// A file at cdn-root/ios/listApp/1.2.0/mf-manifest.json is served at&lt;/span&gt;
&lt;span class="c1"&gt;// &amp;lt;base&amp;gt;/ios/listApp/1.2.0/mf-manifest.json, which is the URL the host builds at launch out of&lt;/span&gt;
&lt;span class="c1"&gt;// the version the map gave it.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Two lists below, and the difference between them is the whole idea. REMOTE_VERSIONS is what the&lt;/span&gt;
&lt;span class="c1"&gt;// CDN holds: every version ever published, kept until nobody is running it. APP_VERSION_MAPS is&lt;/span&gt;
&lt;span class="c1"&gt;// what each released binary is allowed to load out of that. Shipping a remote to installed apps&lt;/span&gt;
&lt;span class="c1"&gt;// is a new entry in the first list and one edited line in the second.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: node tools/build-cdn.mjs [ios|android]     (no argument builds both)&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Then serve it and point the host at it (an Android emulator reaches this machine at 10.0.2.2,&lt;/span&gt;
&lt;span class="c1"&gt;// so the Android build gets that address instead of localhost):&lt;/span&gt;
&lt;span class="c1"&gt;//   npx http-server@14.1.1 cdn-root -p 8000 -c-1 --cors&lt;/span&gt;
&lt;span class="c1"&gt;//   ( cd apps/host &amp;amp;&amp;amp; MF_CDN_BASE=http://localhost:8000 MF_APP_VERSION=2.0.0 npm start )&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cpSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rmSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileURLToPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repoRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// --- Every version of each remote the CDN holds. A version directory is written once and then&lt;/span&gt;
&lt;span class="c1"&gt;// left alone: installed apps are loading those exact files, so a rebuild of a published version&lt;/span&gt;
&lt;span class="c1"&gt;// is a silent change to code somebody is already running. New work gets a new number.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// listApp has two: 1.0.0 and 1.1.0 are the same screen with different stamps on it, which is&lt;/span&gt;
&lt;span class="c1"&gt;// what the two-binaries demo needs. The flip to 1.2.0 adds the third. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_VERSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// --- What each released app version may load. The host asks for its own entry by name at every&lt;/span&gt;
&lt;span class="c1"&gt;// launch, so an old binary keeps being handed the versions it was built against, however far the&lt;/span&gt;
&lt;span class="c1"&gt;// newest release has moved on. An entry is retired when nobody is left on that app version, the&lt;/span&gt;
&lt;span class="c1"&gt;// same way an old API endpoint is.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Editing a line here rebuilds the whole tree, which is the wrong tool for shipping a version:&lt;/span&gt;
&lt;span class="c1"&gt;// the operation the post performs is an edit to the map file already sitting in cdn-root,&lt;/span&gt;
&lt;span class="c1"&gt;// because that file is what a running app reads. This is the seeding of a CDN, not an operation&lt;/span&gt;
&lt;span class="c1"&gt;// against one.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// There is nothing else in the map. No signature over it, no counter, nothing that would let the&lt;/span&gt;
&lt;span class="c1"&gt;// app tell a map written here from one written by anybody else who can reach the bucket. That is&lt;/span&gt;
&lt;span class="c1"&gt;// a real hole and it is left open on purpose: it is the subject of the last post in the series. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP_VERSION_MAPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// --- Never published: source maps are four fifths of the built tree, they are a debugging&lt;/span&gt;
&lt;span class="c1"&gt;// artefact for a crash reporter rather than something a client downloads, and on a public bucket&lt;/span&gt;
&lt;span class="c1"&gt;// they hand the whole readable source to anyone who asks for it. mf-stats.json is build analysis&lt;/span&gt;
&lt;span class="c1"&gt;// in the same position. The remote's own index.bundle stays, because mf-manifest.json names it&lt;/span&gt;
&lt;span class="c1"&gt;// among the shared assets and nothing here has established that no path fetches it. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NEVER_PUBLISHED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;map$|^mf-stats&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;json$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown platform "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". Use one of: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platformArg&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- A map naming a version the CDN does not hold is the failure the post demonstrates by hand,&lt;/span&gt;
&lt;span class="c1"&gt;// and it is worth catching here rather than at a user's launch. Checked before anything is built,&lt;/span&gt;
&lt;span class="c1"&gt;// so a typo costs a second instead of two bundle runs. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unknownRemotes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_VERSIONS&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_APPS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unknownRemotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`\nNo app to build for: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;unknownRemotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;. Add it to REMOTE_APPS, or remove it from REMOTE_VERSIONS.\n`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;APP_VERSION_MAPS&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_VERSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`  app &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; asks for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;These versions are mapped but not published:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;Add them to REMOTE_VERSIONS, or point the map at a version that exists.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Wipe this platform only, so building one does not delete the other's tree.&lt;/span&gt;
  &lt;span class="nf"&gt;rmSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REMOTE_VERSIONS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_APPS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n=== building &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) ===`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Named once: the directory that is cleared, written and then read is one place, so no&lt;/span&gt;
      &lt;span class="c1"&gt;// later edit can move the build's output out from under the copy that follows it.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;built&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Cleared first, because Rspack writes into a directory rather than replacing it, so a file&lt;/span&gt;
      &lt;span class="c1"&gt;// an earlier build emitted and this one does not would survive and be published beside the&lt;/span&gt;
      &lt;span class="c1"&gt;// real ones, for no reason anyone could work out from the source.&lt;/span&gt;
      &lt;span class="nf"&gt;rmSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;built&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="c1"&gt;// MF_REMOTE_VERSION decides both what the bundle says about itself and where it is written,&lt;/span&gt;
      &lt;span class="c1"&gt;// so one variable cannot produce a build that is stamped one version and filed under another.&lt;/span&gt;
      &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`npm run bundle:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:prod`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;MF_REMOTE_VERSION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="c1"&gt;// The build reported success, so a missing directory here means its output path and this&lt;/span&gt;
      &lt;span class="c1"&gt;// path have drifted apart, which is worth saying in one line rather than as a stack trace.&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;built&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; built but wrote nothing to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;built&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check the output path in that app's rspack.config.mjs.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nf"&gt;cpSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;built&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;NEVER_PUBLISHED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`published -&amp;gt; cdn-root/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;APP_VERSION_MAPS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;maps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;version-map.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`wrote     -&amp;gt; cdn-root/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/maps/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/version-map.json`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOST_ADDRESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://10.0.2.2:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appVersions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;APP_VERSION_MAPS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\nCDN assembled at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Serve it:             npx http-server@14.1.1 cdn-root -p 8000 -c-1 --cors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`Point the host at it: ( cd apps/host &amp;amp;&amp;amp; MF_CDN_BASE=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;HOST_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; MF_APP_VERSION=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appVersions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; npm start )   # &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`App versions with a map: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appVersions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source maps stop being published: they are four fifths of a built remote (16 MB of the 20 MB listApp writes on iOS), a crash reporter's artefact rather than a client's download, and on a public bucket they hand the readable source to anyone who asks. Build the tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/build-cdn.mjs ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdn-root/ios/listApp/1.0.0/      cdn-root/ios/maps/1.0.0/version-map.json
cdn-root/ios/listApp/1.1.0/      cdn-root/ios/maps/2.0.0/version-map.json
cdn-root/ios/partyApp/1.0.0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each map is the whole of what a binary is told:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partyApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines and nothing else. &lt;strong&gt;The chunks it points at are signed; the file that chooses between them is not.&lt;/strong&gt; Post 17 closes that; this post leaves it open and says so each time it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask before you load
&lt;/h2&gt;

&lt;p&gt;The host needs two facts at build time: where the CDN is, and which app version this binary is. In &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;, post 14's comment and &lt;code&gt;const CDN_BASE = process.env.MF_CDN_BASE;&lt;/code&gt; become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Where the remotes are served from. Set MF_CDN_BASE and the host looks to the content&lt;/span&gt;
&lt;span class="c1"&gt;// delivery network instead of the dev servers; leave it unset and nothing changes. The value is&lt;/span&gt;
&lt;span class="c1"&gt;// read at BUILD time and baked into the bundle, so a build that forgot it ships the dev URLs:&lt;/span&gt;
&lt;span class="c1"&gt;//   MF_CDN_BASE=http://localhost:8000 npm start      (the local CDN, on a development build)&lt;/span&gt;
&lt;span class="c1"&gt;//   MF_CDN_BASE=https://cdn.example.com npm run …    (a real one, on a release build)&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// What changed in this post is who uses the value. It still shapes the remotes map below, but&lt;/span&gt;
&lt;span class="c1"&gt;// that map is now a placeholder: the URLs it holds carry no version segment, so against a&lt;/span&gt;
&lt;span class="c1"&gt;// versioned CDN tree they resolve to nothing. The value that matters is the one handed to the&lt;/span&gt;
&lt;span class="c1"&gt;// running code through DefinePlugin, where src/shell/scriptManager.ts reads it, asks the CDN&lt;/span&gt;
&lt;span class="c1"&gt;// which versions this binary may run, and re-registers every remote at a versioned URL before&lt;/span&gt;
&lt;span class="c1"&gt;// the first import fires.&lt;/span&gt;
&lt;span class="c1"&gt;// Trailing slashes are trimmed, because every URL built from this value adds its own separator&lt;/span&gt;
&lt;span class="c1"&gt;// and a base written with one produces a double slash in the middle of every path. Most servers&lt;/span&gt;
&lt;span class="c1"&gt;// forgive that; a downloaded script is cached against the URL that fetched it, so it is not worth&lt;/span&gt;
&lt;span class="c1"&gt;// finding out which ones do not.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CDN_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MF_CDN_BASE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// --- This binary's own version, the question it asks the CDN at launch. The CDN answers with the&lt;/span&gt;
&lt;span class="c1"&gt;// remote versions this binary is allowed to run, which is how a two-year-old install keeps&lt;/span&gt;
&lt;span class="c1"&gt;// working: it keeps being handed the versions it shipped against. A real app reads this from the&lt;/span&gt;
&lt;span class="c1"&gt;// version it was released under; here it is a variable, so one checkout can produce two binaries&lt;/span&gt;
&lt;span class="c1"&gt;// that ask different questions:&lt;/span&gt;
&lt;span class="c1"&gt;//   MF_APP_VERSION=1.0.0 npm run ios -- --mode Release&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MF_APP_VERSION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both facts reach the running code as literals: add &lt;code&gt;DefinePlugin&lt;/code&gt; to the host's &lt;code&gt;@rspack/core&lt;/code&gt; import, and this entry to &lt;code&gt;plugins&lt;/code&gt; before &lt;code&gt;ModuleFederationPluginV2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The two build-time facts the operational layer needs as literals in the bundle: where the&lt;/span&gt;
&lt;span class="c1"&gt;// CDN is, and which version this binary is. An empty base is the signal that no CDN was&lt;/span&gt;
&lt;span class="c1"&gt;// configured, which is what keeps a plain development build on the dev servers.&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefinePlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;__MF_CDN_BASE__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;__APP_VERSION__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;APP_VERSION&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post 14's &lt;code&gt;remoteUrl&lt;/code&gt; function and the &lt;code&gt;remotes&lt;/code&gt; map stay, and their job changes. Module Federation wants a name and an entry for every remote declared at build time, so the map stays; its URLs carry no version, so against a versioned tree they resolve to nothing. The comment above the function says so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The build-time remotes map, in one function: dev server or CDN, same manifest filename either&lt;/span&gt;
&lt;span class="c1"&gt;// way. In CDN mode what it produces is a placeholder and nothing loads from it: the versioned&lt;/span&gt;
&lt;span class="c1"&gt;// URL the app really uses is decided at launch. It is left pointing somewhere plausible rather&lt;/span&gt;
&lt;span class="c1"&gt;// than removed, because Module Federation wants a name and an entry for every remote declared&lt;/span&gt;
&lt;span class="c1"&gt;// at build time, and because in dev mode this is still the whole story.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remoteUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mf-manifest.json`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DEV_REMOTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mf-manifest.json`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In CDN mode the build-time map is a placeholder.&lt;/strong&gt; The URL the app really uses is decided at launch, by host code you copy from the end tag rather than type. Fetch the tag once; the copy commands take that code with its tests and Jest stand-ins, plus the other files later sections use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-15-cdn-flip /tmp/pokedex-ref-15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/apps/host/src/shell/&lt;span class="o"&gt;{&lt;/span&gt;remoteLocator,scriptManager,federationErrors&lt;span class="o"&gt;}&lt;/span&gt;.ts /tmp/pokedex-ref-15/apps/host/src/shell/FederationBanner.tsx apps/host/src/shell/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/apps/host/__mocks__/&lt;span class="o"&gt;{&lt;/span&gt;repack-client,module-federation-runtime&lt;span class="o"&gt;}&lt;/span&gt;.js apps/host/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/apps/host/__tests__/&lt;span class="o"&gt;{&lt;/span&gt;remoteLocator,scriptManager,federationErrors&lt;span class="o"&gt;}&lt;/span&gt;.test.ts /tmp/pokedex-ref-15/apps/host/__tests__/&lt;span class="o"&gt;{&lt;/span&gt;App,RemoteBoundary&lt;span class="o"&gt;}&lt;/span&gt;.test.tsx apps/host/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/apps/host/App.tsx apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/scripts/federation-smoke.sh scripts/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/packages/ui/src/tokens/__tests__/contrast.accessibility.ts packages/ui/src/tokens/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/tools/gen-signing-keys.mjs /tmp/pokedex-ref-15/tools/gen-signing-keys.test.mjs tools/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read them in the order they run. &lt;code&gt;src/shell/scriptManager.ts&lt;/code&gt; is the launch, and its first job is fetching the map. Every federated import waits for that answer, so the request needs a bounded wait: without one, a slow CDN would hold the app at its splash screen for as long as the network let it. The wait is a second and a half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROBE_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;AbortController&lt;/code&gt; and a timer enforce it, wired by hand because React Native offers no shortcut. &lt;code&gt;AbortSignal.timeout()&lt;/code&gt; does not exist there: 0.85 installs &lt;code&gt;AbortController&lt;/code&gt; and &lt;code&gt;AbortSignal&lt;/code&gt; from the &lt;code&gt;abort-controller&lt;/code&gt; package, which has no &lt;code&gt;timeout&lt;/code&gt;. The request has no timeout of its own either: React Native builds Android's HTTP client with every timeout at zero, and on iOS passes the request's &lt;code&gt;timeout&lt;/code&gt; through, which defaults to zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Fetch and read the version map for this app version. Returns null for every kind of&lt;/span&gt;
&lt;span class="c1"&gt;// failure, because the caller treats them all the same way: an unreachable CDN, a 404 for an app&lt;/span&gt;
&lt;span class="c1"&gt;// version nobody published a map for, a timeout, and a map that does not parse all end with this&lt;/span&gt;
&lt;span class="c1"&gt;// binary running no remotes. ---&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchVersionMap&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;PROBE_TIMEOUT_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;versionMapUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APP_VERSION&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// The map is the one file on the CDN that must never be served from a cache: it is the&lt;/span&gt;
      &lt;span class="c1"&gt;// record of what is current, and a stale copy is a silently undone deploy. Every other file&lt;/span&gt;
      &lt;span class="c1"&gt;// the app fetches carries its version in the URL and can be cached forever.&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[federation] version map returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseVersionMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_NAMES&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// The one failure that is nobody's network and nobody's outage: the file was served and&lt;/span&gt;
      &lt;span class="c1"&gt;// could not be believed. Said out loud, because the alternative is an app that launches,&lt;/span&gt;
      &lt;span class="c1"&gt;// loads nothing and offers no reason.&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[federation] version map was served but could not be read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[federation] version map could not be fetched&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the versions in hand, every remote is pointed at the manifest inside its version directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Point every remote at the versioned manifest this launch resolved.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// force is not optional. Each remote is already registered under its name from the build-time&lt;/span&gt;
&lt;span class="c1"&gt;// remotes map, and registerRemotes leaves an already-registered name alone unless it is told&lt;/span&gt;
&lt;span class="c1"&gt;// otherwise: without the flag this call returns quietly, changes nothing, and the app loads the&lt;/span&gt;
&lt;span class="c1"&gt;// unversioned placeholder URLs instead. With it, Module Federation logs a warning about&lt;/span&gt;
&lt;span class="c1"&gt;// re-registering a remote on every launch, which is the expected cost of doing this. ---&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerCdnRemotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;registerRemotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;REMOTE_NAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;remoteManifestUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code&gt;registerRemotes&lt;/code&gt; without &lt;code&gt;force&lt;/code&gt; is a silent no-op for a name that is already registered.&lt;/strong&gt; In Module Federation's runtime core at 2.9.0, the branch for an existing name does nothing at all when &lt;code&gt;force&lt;/code&gt; is absent: no error, no warning, no change. The build-time map registered both remotes long before the probe ran, so every launch re-registers with &lt;code&gt;{ force: true }&lt;/code&gt;, and the runtime prints &lt;code&gt;[ Federation Runtime ]: The remote "listApp" is already registered. Please note that overriding it may cause unexpected errors.&lt;/code&gt; once per remote in the development console. That line is the arrangement working. A launch that does not print it loaded the placeholders.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The launch is one function, held as a promise so that a second caller arriving mid-probe waits for the same answer instead of reading the state from before it started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initializeFederation&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FederationStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;initialization&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;resolveFederation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;initialization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveFederation&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FederationStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CDN_CONFIGURED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;__DEV__&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev servers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unresolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no CDN configured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchVersionMap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Worded to cover every way this fails, because the banner is a claim the app makes about&lt;/span&gt;
    &lt;span class="c1"&gt;// itself: a map that was served and refused is not an unreachable one, and the log line&lt;/span&gt;
    &lt;span class="c1"&gt;// beside it already says which of the two happened.&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unresolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no usable version map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// The status is set before the registration because the resolver reads it, and then rolled back&lt;/span&gt;
  &lt;span class="c1"&gt;// if the registration throws. Claiming CDN mode after a failed re-registration would put the&lt;/span&gt;
  &lt;span class="c1"&gt;// versions on the banner while every load went to the build-time placeholder URL: an app that&lt;/span&gt;
  &lt;span class="c1"&gt;// says it is running 1.2.0 and is running nothing.&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;versions&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;registerCdnRemotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[federation] remotes could not be re-registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unresolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remotes could not be registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three modes come out of it: &lt;code&gt;dev&lt;/code&gt; is post 14's world, &lt;code&gt;cdn&lt;/code&gt; is the one this post exists for, and &lt;code&gt;unresolved&lt;/code&gt; is a failure with a name: no usable map, so no remote at all, refused rather than loaded unverified. &lt;code&gt;FederationBanner.tsx&lt;/code&gt; paints the mode and the resolved versions as a pill above the tab bar, so a demo can be photographed rather than believed.&lt;/p&gt;

&lt;p&gt;The gate is in &lt;code&gt;App.tsx&lt;/code&gt;. The navigator's tabs are &lt;code&gt;React.lazy&lt;/code&gt; federated imports, so mounting the navigator starts the first download, and that must wait for the re-registration. The state and the effect sit in &lt;code&gt;App&lt;/code&gt; above &lt;code&gt;SafeAreaProvider&lt;/code&gt;, because that provider renders no children until it has measured the insets: below it, the gate would wait on that measurement, and under Jest, where nothing measures, it would never open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;federationReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFederationReady&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;initializeFederation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;federation initialisation failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setFederationReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;federationReady&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shell&lt;/span&gt; &lt;span class="na"&gt;navTheme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;navTheme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onReady&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setNavReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FederationBanner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post 8's boot import of &lt;code&gt;partyApp/partySlice&lt;/code&gt; moves behind the same flag, because it is a federated load like any other. After that move, nothing federated loads before the launch has its answer.&lt;/p&gt;

&lt;p&gt;A failed probe does not keep the app at its splash screen. The gate opens either way, at most one probe timeout later, and the mode records the difference: after a failed probe the banner shows &lt;code&gt;unresolved&lt;/code&gt;, the resolver refuses every remote, and each tab shows its error state.&lt;/p&gt;

&lt;p&gt;The host's tests need two more entries in &lt;code&gt;apps/host/jest.config.js&lt;/code&gt;, because &lt;code&gt;ScriptManager.shared&lt;/code&gt; reaches for a bundler runtime the moment it is touched, and a Jest process has none. The map and the comment above it become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reanimated drives animations through the JSI (the JavaScript Interface React Native uses to&lt;/span&gt;
&lt;span class="c1"&gt;// call native code), which a Jest process has no runtime for, so it is replaced by the stand-in&lt;/span&gt;
&lt;span class="c1"&gt;// in __mocks__. One entry covers the whole federation: @pokedex/ui and @pokedex/detail import&lt;/span&gt;
&lt;span class="c1"&gt;// the same module specifier, so their animated components resolve to the same mock. The&lt;/span&gt;
&lt;span class="c1"&gt;// federated state module has no resolvable source under Jest either; its mock repeats the real&lt;/span&gt;
&lt;span class="c1"&gt;// module's side effect (reducer injection) so boot readiness is testable.&lt;/span&gt;
&lt;span class="c1"&gt;// The Re.Pack client and Module Federation runtime entries exist because Re.Pack's ScriptManager&lt;/span&gt;
&lt;span class="c1"&gt;// and Module Federation's registerRemotes both reach into a bundler runtime that a Jest process&lt;/span&gt;
&lt;span class="c1"&gt;// does not have, and the host touches both at module scope, so the resolver is in place before&lt;/span&gt;
&lt;span class="c1"&gt;// any federated import can fire. Their stand-ins record what was asked of them.&lt;/span&gt;
&lt;span class="nx"&gt;moduleNameMapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^react-native-reanimated$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;rootDir&amp;gt;/__mocks__/react-native-reanimated.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^partyApp/partySlice$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;rootDir&amp;gt;/__mocks__/partyApp-partySlice.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^partyApp/styles$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;rootDir&amp;gt;/__mocks__/partyApp-styles.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^@callstack/repack/client$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;rootDir&amp;gt;/__mocks__/repack-client.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^@module-federation/runtime$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;rootDir&amp;gt;/__mocks__/module-federation-runtime.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One resolver, every chunk
&lt;/h2&gt;

&lt;p&gt;The map names a version per remote. Something still has to turn every script the federation loads into a URL inside that version's directory: the container when the remote is first imported, then each chunk the container asks for. Re.Pack's &lt;code&gt;ScriptManager&lt;/code&gt; asks its resolvers in priority order, and the first to return a locator wins. This post adds one. Its decisions live in &lt;code&gt;src/shell/remoteLocator.ts&lt;/code&gt; as plain functions, testable without a device, and each script gets one of three answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** The resolver's decision for one script. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Resolution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;defer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RemoteLocator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refuse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which one depends on the mode, the remote and the map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Which remote a script belongs to. A container announces itself: its script id IS the remote&lt;/span&gt;
&lt;span class="c1"&gt;// name. A chunk does not, so the caller is the only thing that says where it came from, and it is&lt;/span&gt;
&lt;span class="c1"&gt;// why the resolver takes both arguments rather than pattern-matching the id. ---&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;remoteFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;remoteNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remoteNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caller&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;remoteNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --- Decide one script: locate it inside its version's directory, refuse it, or defer it to&lt;/span&gt;
&lt;span class="c1"&gt;// Re.Pack's own resolution.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Deferring hands the script to the next resolver, and for one of this host's remotes the next&lt;/span&gt;
&lt;span class="c1"&gt;// one is Re.Pack's per-remote resolver: it answers with a URL built from whichever manifest was&lt;/span&gt;
&lt;span class="c1"&gt;// registered last and no signature check at all. That is the right answer in development, where&lt;/span&gt;
&lt;span class="c1"&gt;// the dev servers own everything, and for any script that is not one of this host's remotes.&lt;/span&gt;
&lt;span class="c1"&gt;// Outside development it is never the right answer for a remote: with no version map, or a map&lt;/span&gt;
&lt;span class="c1"&gt;// that named no version for this remote, deferring would load code from an unversioned URL,&lt;/span&gt;
&lt;span class="c1"&gt;// unverified. So those loads are refused, and the refusal surfaces as the tab's error state. ---&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveRemoteLocator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResolveInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Resolution&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;defer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remoteName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;remoteFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remoteNames&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;defer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refuse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`the version map named no version for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`no version map was read at launch, so &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has no version to load`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scriptId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;remoteName&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.container.js.bundle`&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.chunk.bundle`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cdnBase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Caching is per URL, and a URL here carries its version, so a cached file can only ever be&lt;/span&gt;
      &lt;span class="c1"&gt;// served for the version it was fetched for. A new version is a new URL and a fresh download.&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;verifyScriptSignature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/cdn-version-resolution-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip#one-resolver-every-chunk" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;remoteLocator.test.ts&lt;/code&gt; walks this tree, including a chunk resolved by its caller rather than its id, the case that proves the resolver reads its second argument at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scriptManager.ts&lt;/code&gt; registers it at module scope, so it is in place before anything federated can be imported, whatever order the launch runs in: once webpack and the federation runtime have loaded a container or a chunk they never ask for it again, so a resolver added after a script's first load never sees that script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ScriptManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveRemoteLocator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;remoteNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_NAMES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cdnBase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VERIFY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refuse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Thrown, not returned. Returning nothing passes the script to the next resolver, which for a&lt;/span&gt;
      &lt;span class="c1"&gt;// remote is Re.Pack's own and would load it unverified. Re.Pack's resolveScript stops at the&lt;/span&gt;
      &lt;span class="c1"&gt;// first resolver that throws, so the load fails and the tab shows its error state.&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[federation] refused &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;scriptId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__signed_resolver__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The priority is the line that decides whether any of this runs.&lt;/strong&gt; Re.Pack's &lt;code&gt;ResolverPlugin&lt;/code&gt; registers a resolver of its own for each remote as it is registered, with a key and no priority, so it takes &lt;code&gt;ScriptManager&lt;/code&gt;'s default, which is 2 in 5.2.5's source; resolvers run highest first. Once the launch re-registers the remotes, that built-in resolver is rebuilt from the versioned manifest URL, but its locator carries no verification setting, so a custom resolver below 2 loses to it, and loses quietly: the right versions load, and every chunk loads unverified. 100 is far above 2, and &lt;code&gt;scriptManager.test.ts&lt;/code&gt; asserts it against the stand-in.&lt;/p&gt;

&lt;p&gt;The same built-in resolver is why returning nothing is unsafe when a remote has no version. Returning nothing hands the load to it, and it would fetch the script from the unversioned build-time URL with no signature check. Throwing ends the search instead, because Re.Pack's &lt;code&gt;resolveScript&lt;/code&gt; stops at the first resolver that throws.&lt;/p&gt;

&lt;p&gt;The map arrives over the network, so &lt;code&gt;parseVersionMap&lt;/code&gt; reads it as a stranger's JSON. A version becomes a path segment in a URL the app downloads code from, so each one is checked against &lt;code&gt;/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/&lt;/code&gt; first: a slash would climb out of the version directory, and the leading character rules out &lt;code&gt;..&lt;/code&gt;. One bad version refuses the whole map, because a half-read map would launch the app on versions nobody published; remote names this binary has never heard of are ignored, since one CDN can serve several apps. &lt;code&gt;remoteLocator.test.ts&lt;/code&gt; covers the shapes the parser refuses and the names it ignores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the key
&lt;/h2&gt;

&lt;p&gt;Post 14 signed every production chunk and read none of the signatures: "a signature nobody verifies is a stamp, not a lock." The lock is one field on the locator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Signature verification is only meaningful where there is a public key to verify against:&lt;/span&gt;
&lt;span class="c1"&gt;// the key is embedded in the iOS Info.plist and in Android's strings.xml, and nowhere else. On&lt;/span&gt;
&lt;span class="c1"&gt;// those two platforms it is strict, which means a chunk whose signature does not match the key,&lt;/span&gt;
&lt;span class="c1"&gt;// or which carries no signature at all, is rejected before it executes. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SIGNED_PLATFORMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VERIFY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VerifyMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SIGNED_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re.Pack accepts three values. &lt;code&gt;lax&lt;/code&gt; verifies only when a token is present and lets an unsigned chunk through; &lt;code&gt;off&lt;/code&gt; verifies nothing; &lt;code&gt;strict&lt;/code&gt; refuses a mismatched signature and a missing one alike, which is what makes the stamp a lock.&lt;/p&gt;

&lt;p&gt;Each platform reads the key by name: iOS from &lt;code&gt;Info.plist&lt;/code&gt; under &lt;code&gt;RepackPublicKey&lt;/code&gt;, Android from &lt;code&gt;res/values/strings.xml&lt;/code&gt; under the same name. Both files are committed and compiled into the binary, so the generator you copied from the tag now writes the key into them.&lt;/p&gt;

&lt;p&gt;The two platforms take it in different shapes. iOS parses PEM, the base64 text between BEGIN and END lines, so it gets the file as saved. Android uses only the base64 body, stripping those lines and the line breaks before it decodes, so it gets that body alone, on one line. Before building either shape, the generator turns Windows line endings (CRLF, a carriage return and a line feed) into Unix ones (LF), so a key saved by any editor embeds the same value. Its check against the private key compares DER, the binary form both shapes encode, so line endings cannot break it either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Put the public half where the app reads it. The two platforms take the key in two shapes:&lt;/span&gt;
&lt;span class="c1"&gt;// iOS parses PEM, so it gets the file verbatim; Android strips the header, the footer and the line&lt;/span&gt;
&lt;span class="c1"&gt;// breaks before it decodes, so it is given the base64 body alone, on one line. ---&lt;/span&gt;
&lt;span class="c1"&gt;// Line endings are normalised before either shape is built, so a public key that has been through&lt;/span&gt;
&lt;span class="c1"&gt;// a tool or an editor that writes CRLF embeds exactly the values one saved with LF does. It still&lt;/span&gt;
&lt;span class="c1"&gt;// matches its private key either way: the check above compares DER bytes, not text.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicPem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicBase64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;publicPem&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUBLIC KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the generator's change replaces the value of an existing entry in each file. It treats a missing entry as an error rather than a warning, because strict verification fails closed: a key that never reached the app surfaces later as every remote refusing to load, with a message that says nothing about the generator. So add both entries now, empty, for the generator to fill. In &lt;code&gt;apps/host/ios/Host/Info.plist&lt;/code&gt;, inside the top-level &lt;code&gt;&amp;lt;dict&amp;gt;&lt;/code&gt; and above &lt;code&gt;RCTNewArchEnabled&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- The public half of the chunk-signing keypair. Re.Pack's native verifier reads it from
     this key by name, and strict verification fails closed without it. Written here by
     tools/gen-signing-keys.mjs; left empty in the repository because the key is generated per
     checkout, and a public key is safe to commit but a stale one is not useful. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RepackPublicKey&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in &lt;code&gt;apps/host/android/app/src/main/res/values/strings.xml&lt;/code&gt;, after &lt;code&gt;app_name&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- The public half of the chunk-signing keypair, read by name by Re.Pack's native verifier.
     The base64 body alone, on one line: the verifier strips any PEM header, footer and line
     breaks before it decodes. Written by tools/gen-signing-keys.mjs; empty in the repository,
     because the key is generated per checkout. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"RepackPublicKey"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the generator. Post 14's private key is kept, and the public half lands in both files; the first three lines of its output say so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/gen-signing-keys.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chunk-signing keypair already present, kept
embedded the public key -&amp;gt; apps/host/ios/Host/Info.plist
embedded the public key -&amp;gt; apps/host/android/app/src/main/res/values/strings.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Verification happens at download time.&lt;/strong&gt; On both platforms the check runs inside the download-and-cache path, and a script already on disk is executed without being verified again. In this build that window is one session: the locator cache lives in memory (no &lt;code&gt;setStorage&lt;/code&gt;), so every launch downloads and verifies every chunk afresh, and the server logs in this post show it. A build that persists that cache extends the window to the life of the cached file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two binaries, one CDN
&lt;/h2&gt;

&lt;p&gt;The map is per app version, rather than one file for everyone, because compatibility cannot be negotiated on the device. The host's copy of each shared singleton is the one copy in the runtime, loaded before any remote, which is &lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;post 3&lt;/a&gt;'s contract; a remote built against a different version cannot bring its own. So the choice is made on the server, per binary. Two builds from one checkout show it. Serve the tree, then build the Release scheme twice, once per app version, onto two simulators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx http-server@14.1.1 cdn-root &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;-c-1&lt;/span&gt; &lt;span class="nt"&gt;--cors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="nt"&gt;--udid&lt;/span&gt; &amp;lt;simulator A&amp;gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.0.0 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="nt"&gt;--udid&lt;/span&gt; &amp;lt;simulator B&amp;gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both boot on the Pokédex tab with no dev server running. The 2.0.0 build's chip reads &lt;code&gt;listApp 1.1.0&lt;/code&gt; and its banner &lt;code&gt;cdn · listApp 1.1.0 · partyApp 1.0.0&lt;/code&gt;; the 1.0.0 build's chip reads &lt;code&gt;listApp 1.0.0&lt;/code&gt;. The server's terminal says why (trimmed for length: the vendor chunks, the party's chunks and the 1.0.0 launch's lines after its container are cut):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-09-21T15:51:21.029Z]  "GET /ios/maps/2.0.0/version-map.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:51:21.069Z]  "GET /ios/listApp/1.1.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:51:21.076Z]  "GET /ios/partyApp/1.0.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:51:21.083Z]  "GET /ios/listApp/1.1.0/listApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:51:21.095Z]  "GET /ios/partyApp/1.0.0/partyApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:51:21.152Z]  "GET /ios/listApp/1.1.0/__federation_expose_ListStack.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:53:56.057Z]  "GET /ios/maps/1.0.0/version-map.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:53:56.091Z]  "GET /ios/listApp/1.0.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:53:56.116Z]  "GET /ios/listApp/1.0.0/listApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The map first, then the manifests, then every chunk inside the directory the map named. &lt;strong&gt;Same CDN, two binaries, two answers.&lt;/strong&gt; The skews this arrangement handles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What changes on the CDN&lt;/th&gt;
&lt;th&gt;What each installed binary loads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A fix to a remote, for everyone&lt;/td&gt;
&lt;td&gt;A new version directory, then the line in every map that should take it&lt;/td&gt;
&lt;td&gt;Its own map's version, at its next launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A remote that needs a newer shared singleton&lt;/td&gt;
&lt;td&gt;A new version directory, and a line in the new app version's map only&lt;/td&gt;
&lt;td&gt;Old binaries keep their old line; the new binary gets the new version once it ships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A host release with no remote change&lt;/td&gt;
&lt;td&gt;A new map, copied from the last one&lt;/td&gt;
&lt;td&gt;The same remote versions the last app version ran&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A bad version already shipped&lt;/td&gt;
&lt;td&gt;The map line put back&lt;/td&gt;
&lt;td&gt;The previous version, at the next launch, with nothing rebuilt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The detail screen has no row because it is not a remote of its own. Since &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;post 5&lt;/a&gt; it has been a package compiled into whichever remote installs it, so a detail fix ships as a new version of the list and of the party. &lt;strong&gt;The unit of a flip is the remote, and everything installed into it moves with it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Android needs one more declaration: &lt;code&gt;MF_APP_VERSION&lt;/code&gt; joins &lt;code&gt;MF_CDN_BASE&lt;/code&gt; as an input of the bundle task, for the reason post 14 gave, and the block post 14 added to &lt;code&gt;apps/host/android/app/build.gradle&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- MF_CDN_BASE and MF_APP_VERSION are read by rspack.config.mjs when the bundle task runs, and&lt;/span&gt;
&lt;span class="c1"&gt;// Gradle cannot see that: an environment variable is not one of the task's declared inputs, so a&lt;/span&gt;
&lt;span class="c1"&gt;// build where only the value changed leaves createBundleReleaseJsAndAssets UP-TO-DATE and ships&lt;/span&gt;
&lt;span class="c1"&gt;// the previous bundle, dev-server URLs and all. Declaring them as inputs makes a changed value&lt;/span&gt;
&lt;span class="c1"&gt;// rebuild the bundle and an unchanged one keep the cache.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// MF_APP_VERSION matters here for the same reason, and more: the two-binaries demo, run on&lt;/span&gt;
&lt;span class="c1"&gt;// Android, builds the same source twice with nothing different but that variable, so without this&lt;/span&gt;
&lt;span class="c1"&gt;// line the second build silently ships the first one's bundle and asks the CDN the first one's&lt;/span&gt;
&lt;span class="c1"&gt;// question. ---&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;configureEach&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"createBundle"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"JsAndAssets"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MF_CDN_BASE"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MF_CDN_BASE"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MF_APP_VERSION"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MF_APP_VERSION"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/build-cdn.mjs android &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://10.0.2.2:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run android &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ship 1.2.0
&lt;/h2&gt;

&lt;p&gt;The installed 2.0.0 app shows list 1.1.0. Ship it a change a user would see: when the party reaches six members, the counter in the Pokédex header changes its label from "My Party" to "Party full", and its pill deepens. In &lt;code&gt;PokedexScreen.tsx&lt;/code&gt;, after &lt;code&gt;partyCount&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyFull&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;partyCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counter group reads it three times: for the visible label, for the label a screen reader speaks, and for the pill's fill. Its comments say why the full state is marked by the word and by the colour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The count changes when the user adds a member from a screen away, without focus
    moving here. A sighted user sees the number tick; a screen-reader user is told
    nothing unless this is a live region (SC 4.1.3). The label spells the ratio out,
    because "3/6" is read as "three slash six" or as a date, depending on the reader.
    At six it reads the full state too, which is the word a colour alone cannot say. */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-row items-center gap-2"&lt;/span&gt;
  &lt;span class="na"&gt;accessible&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;partyFull&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Party full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sm"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"font-semi text-darkGrey dark:text-lightGrey"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;partyFull&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Party full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Full is the party's one state worth marking, and it is marked twice: the label
      beside this pill changes, and the pill deepens. The label carries the state for
      everyone, including anyone who cannot use colour (SC 1.4.1); the deeper pill
      makes it visible at a glance, in both themes.
      The dark half deepens by alpha rather than by hue, because the pill over navy is
      translucent white in the first place and a green fill there would be a different
      component wearing the same shape.
      darkGrey, not darkGreen: darkGreen is #A6D3A0, the grass fill, and on lightGreen
      it measures 1.53:1. darkGrey is the colour the label beside it already uses, and
      it clears the bar on both greens. All four pairs are in the contrast matrix. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`rounded-full px-2.5 py-0.5 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
      &lt;span class="nx"&gt;partyFull&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-pokemonGreen dark:bg-white/20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-lightGreen dark:bg-white/10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"xs"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"font-head text-darkGrey dark:text-pokemonGreen"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every pair this post paints is measured in the contrast matrix &lt;a href="https://warrendeleon.com/blog/accessibility-testing-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;post 12&lt;/a&gt; built; the four new rows are in the design-system test you copied. The list's own accessibility tests check the full state's label and its pill, in both themes, and the chip beside them; copy them from the tag now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-15/apps/list/__tests__/ListStack.accessibility.tsx apps/list/__tests__/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now build the change as a version and put its directory on the CDN. Not through &lt;code&gt;build-cdn&lt;/code&gt;, which seeds a tree from its lists and would rewrite everything the server is serving: the operation is two commands against the tree already there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_REMOTE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.2.0 npm run bundle:ios:prod &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s1"&gt;'*.map'&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; mf-stats.json apps/list/cdn/ios/listApp/1.2.0/ cdn-root/ios/listApp/1.2.0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing has changed for anybody. The directory is there and no map points at it. Then the second step, one line of &lt;code&gt;cdn-root/ios/maps/2.0.0/version-map.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partyApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relaunch the installed 2.0.0 app. Nothing was rebuilt and nothing was reinstalled. A phone takes the change the same way, at its next launch, and a user who already has the app open keeps the code they loaded until then. The server's log, with the list's vendor chunk lines cut for length:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-09-21T15:52:00.957Z]  "GET /ios/maps/2.0.0/version-map.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:52:00.977Z]  "GET /ios/listApp/1.2.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:52:00.981Z]  "GET /ios/partyApp/1.0.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:52:00.987Z]  "GET /ios/listApp/1.2.0/listApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:52:00.995Z]  "GET /ios/partyApp/1.0.0/partyApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:52:01.038Z]  "GET /ios/listApp/1.2.0/__federation_expose_ListStack.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:52:01.185Z]  "GET /ios/partyApp/1.0.0/__federation_expose_styles.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:52:01.185Z]  "GET /ios/partyApp/1.0.0/__federation_expose_partySlice.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chip reads &lt;code&gt;listApp 1.2.0&lt;/code&gt;, the banner agrees, and with six Pokémon added the header says Party full on a deeper pill. The 1.0.0 binary on the other simulator, launched after the flip, still loaded listApp 1.0.0: its own map never changed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/cdn-version-resolution-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip#ship-120" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Upload the version directory first. Flip the map last. The map is the commit.&lt;/strong&gt; Nothing in this build gives a binary anywhere else to load a remote from, so a map that points at a directory not yet on the CDN is a 404 for every user who launches in between, and what they get is a tab that will not open. The missing-version break-it in Now break it, three times is what the other order looks like.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Record the release in the tool too, so its lists match what the CDN now holds. In &lt;code&gt;tools/build-cdn.mjs&lt;/code&gt; the two lists and their comments become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Every version of each remote the CDN holds. A version directory is written once and then&lt;/span&gt;
&lt;span class="c1"&gt;// left alone: installed apps are loading those exact files, so a rebuild of a published version&lt;/span&gt;
&lt;span class="c1"&gt;// is a silent change to code somebody is already running. New work gets a new number.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// listApp has three because this post ships two releases of it. 1.0.0 and 1.1.0 are the same&lt;/span&gt;
&lt;span class="c1"&gt;// screen with different stamps on it, which is what the two-binaries demo needs; 1.2.0 is the&lt;/span&gt;
&lt;span class="c1"&gt;// build that added the party counter's full state, and it is the one the flip ships. This tool&lt;/span&gt;
&lt;span class="c1"&gt;// builds every version from the source in front of it, so rebuilt from the finished tree all three&lt;/span&gt;
&lt;span class="c1"&gt;// carry that state: the post builds 1.0.0 and 1.1.0 before making the change, which is what keeps&lt;/span&gt;
&lt;span class="c1"&gt;// them without it. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_VERSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// --- What each released app version may load. The host asks for its own entry by name at every&lt;/span&gt;
&lt;span class="c1"&gt;// launch, so an old binary keeps being handed the versions it was built against, however far the&lt;/span&gt;
&lt;span class="c1"&gt;// newest release has moved on. An entry is retired when nobody is left on that app version, the&lt;/span&gt;
&lt;span class="c1"&gt;// same way an old API endpoint is.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// 2.0.0 pointed at listApp 1.1.0 until the flip; the line below is what changed, and changing it&lt;/span&gt;
&lt;span class="c1"&gt;// back is the rollback. Editing it here rebuilds the whole tree, which is the wrong tool for that&lt;/span&gt;
&lt;span class="c1"&gt;// job: the operation the post performs is an edit to the map file already sitting in cdn-root,&lt;/span&gt;
&lt;span class="c1"&gt;// because that file is what a running app reads. This is the seeding of a CDN, not an operation&lt;/span&gt;
&lt;span class="c1"&gt;// against one.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// There is nothing else in the map. No signature over it, no counter, nothing that would let the&lt;/span&gt;
&lt;span class="c1"&gt;// app tell a map written here from one written by anybody else who can reach the bucket. That is&lt;/span&gt;
&lt;span class="c1"&gt;// a real hole and it is left open on purpose: it is the subject of the last post in the series. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP_VERSION_MAPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That edit is bookkeeping; the deploy already happened. It also means a fresh seeding from the finished tree builds all three versions from one source, so 1.0.0 and 1.1.0 would carry the full-party state too: the order you just followed is what keeps them without it.&lt;/p&gt;

&lt;p&gt;Two caching rules make the operation safe to repeat. Each chunk URL includes its version, so the CDN, a proxy or the device can keep that file forever without confusing it with a later release. The version map changes with every deploy, so it gets the opposite rule: the probe sends &lt;code&gt;cache-control: no-cache&lt;/code&gt;, and locally &lt;code&gt;-c-1&lt;/code&gt; makes http-server send &lt;code&gt;cache-control: no-cache, no-store, must-revalidate&lt;/code&gt;. A production CDN gets the same two rules as configuration.&lt;/p&gt;

&lt;p&gt;This host still downloads the party's unchanged 1.0.0 chunks after a relaunch, as the log shows, because it keeps the locator cache in memory only. Persisting that cache would save those downloads, and would also extend how long a file on disk runs without another signature check, for the reason in the "Verification happens at download time" callout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll it back
&lt;/h2&gt;

&lt;p&gt;The version that shipped is wrong. Put the line back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partyApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relaunch, and the log moves back to &lt;code&gt;1.1.0&lt;/code&gt; with nothing rebuilt, because the old directory was never removed. The two lists in &lt;code&gt;build-cdn&lt;/code&gt; make this possible: the CDN keeps &lt;code&gt;1.1.0&lt;/code&gt; while nothing points at it, so a rollback is an edit and never a build.&lt;/p&gt;

&lt;p&gt;The same plain file that makes rollback a one-line edit lets anyone who can write to the bucket steer every installed app to whichever version they choose, including an old one with a known fault. The chunks are guarded; the map is naked. Post 17 is about that file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it, three times
&lt;/h2&gt;

&lt;p&gt;A refused chunk and a missing version look the same on the screen and mean opposite things. A release whose module throws while it initialises looks no different. Run all three on the Release build, with the map pointing at &lt;code&gt;1.2.0&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;First, the system working. Change one byte inside the exposed chunk on the CDN and relaunch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'X'&lt;/span&gt; | &lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cdn-root/ios/listApp/1.2.0/__federation_expose_ListStack.chunk.bundle &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;seek&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="nv"&gt;conv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;notrunc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server serves the chunk, the device downloads it, and verification refuses it before a line of it runs. A Release build has no Metro console, so read the simulator's own log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun simctl spawn &amp;lt;simulator A&amp;gt; log show &lt;span class="nt"&gt;--last&lt;/span&gt; 1m &lt;span class="nt"&gt;--predicate&lt;/span&gt; &lt;span class="s1"&gt;'process == "Host"'&lt;/span&gt; &lt;span class="nt"&gt;--style&lt;/span&gt; compact | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A17&lt;/span&gt; &lt;span class="s1"&gt;'Failed to load script'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The match and the lines that matter in the seventeen after it, with the log's own prefix cut:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'[ScriptManager] Failed to load script:', '[ScriptDownloadFailure]', { scriptId: '__federation_expose_ListStack',
  caller: 'listApp',
     url: 'http://localhost:8000/ios/listApp/1.2.0/__federation_expose_ListStack.chunk.bundle',
   { [Error: The bundle verification failed because the bundle hash is invalid.]
     code: 'ScriptDownloadFailure',
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pokédex tab shows the design system's error state, "This tab could not load" with a Try again button; the banner still reads &lt;code&gt;cdn · listApp 1.2.0 · partyApp 1.0.0&lt;/code&gt;; the Party tab opens and works. The line under the title follows the launch's mode: from the CDN it says the remote could not be downloaded, verified or started and suggests a relaunch, where &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;post 11&lt;/a&gt;'s wording pointed at a dev server this build does not have.&lt;/p&gt;

&lt;p&gt;Append bytes after the end of the file instead and the logged error becomes &lt;code&gt;no token for the bundle was found&lt;/code&gt;: the verifier reads the file's last 1,280 bytes and expects them to begin with the signature's marker, and the appended bytes moved that window past it. Strict mode refuses that too. Restore the chunk from the built copy before going on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fooeaqb7zzsyq0mwhl9wf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fooeaqb7zzsyq0mwhl9wf.webp" alt="The Release build after a tampered chunk: the Pokédex tab shows This tab could not load, a line saying the remote could not be downloaded, verified or started, and a Try again button, while the federation banner above the tab bar still reads cdn, listApp 1.2.0, partyApp 1.0.0 and the Party tab is available" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Second, the operational failure. Point the 2.0.0 map at a version whose directory does not exist, &lt;code&gt;1.3.0&lt;/code&gt;, and relaunch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-09-21T15:54:25.541Z]  "GET /ios/listApp/1.3.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-21T15:54:25.542Z]  "GET /ios/listApp/1.3.0/mf-manifest.json" Error (404): "Not found"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same error state on the same tab, and the banner reports &lt;code&gt;listApp 1.3.0&lt;/code&gt;, the version it was told to load. Nothing was tampered with; the map was written before the directory it names, which is the wrong order the "map is the commit" callout warns about, seen from the user's side. Copy any built version into &lt;code&gt;cdn-root/ios/listApp/1.3.0/&lt;/code&gt; and the next launch loads it. Put the map back to &lt;code&gt;1.2.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Third, a release with a bug in it. Add one line below the imports of &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokedexScreen failed to initialise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship it the way 1.2.0 shipped, as a version of its own, then point the 2.0.0 map at &lt;code&gt;1.4.0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_REMOTE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.4.0 npm run bundle:ios:prod &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s1"&gt;'*.map'&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; mf-stats.json apps/list/cdn/ios/listApp/1.4.0/ cdn-root/ios/listApp/1.4.0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The requested JavaScript bundles download and pass signature verification, but the tab shows the same error state again, with the banner reporting &lt;code&gt;listApp 1.4.0&lt;/code&gt;. Nothing failed to load this time: the list's code arrived intact and threw while its module initialised. Delete the line and put the map back to &lt;code&gt;1.2.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of the three error states appeared the first time its failure ran on an iOS Release build: the app died before any of them could be drawn. Android had already shown the same death in &lt;a href="https://warrendeleon.com/blog/production-build-three-modes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;post 14&lt;/a&gt;, where a release build's refused manifest request ended the boot.&lt;/p&gt;

&lt;p&gt;For the first two, the cause is the order in which the failure is reported. When a remote's module fails to load, webpack's remote runtime records the error, appends &lt;code&gt;while loading "./ListStack" from …&lt;/code&gt; to its message, and replaces the module's factory with one that throws. Re.Pack replaces webpack's &lt;code&gt;require&lt;/code&gt; with a guarded version in every bundle it builds: when a require throws, the guard catches the error, reports it to React Native's global error handler as fatal, and returns nothing. That report comes first, before React has tried to render the tab.&lt;/p&gt;

&lt;p&gt;In a development build the fatal report is a red box over a working app. In a Release build the handler passes it to React Native's native error handling, where the fatal path ends the process: on iOS the exceptions module calls &lt;code&gt;RCTFatal&lt;/code&gt;, which throws an exception nothing catches, and on Android it throws a &lt;code&gt;JavascriptException&lt;/code&gt; that the default host rethrows.&lt;/p&gt;

&lt;p&gt;If the process survives that report, the tab still reaches its error state. The guarded require's empty return lets the tab's import settle without a component, React refuses to render it (&lt;code&gt;Element type is invalid&lt;/code&gt;), and &lt;code&gt;RemoteBoundary&lt;/code&gt;, which &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-cdn-flip" rel="noopener noreferrer"&gt;post 11&lt;/a&gt; put around each tab, catches that render error and shows the error state. The boundary never sees the original loading error; it handles the failure that follows from it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/shell/federationErrors.ts&lt;/code&gt; lets the process survive. It wraps React Native's global handler and, for the first two breaks, drops the report whose message ends with the suffix the remote runtime appended, which the runtime writes at exactly one place and always as the message's last line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HANDLED_BY_REMOTE_RUNTIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;while loading "&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n]&lt;/span&gt;&lt;span class="sr"&gt;+" from &lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isHandledRemoteLoadError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;HANDLED_BY_REMOTE_RUNTIME&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two earlier versions of that matcher were written, measured and removed, and both are now test cases. The first matched &lt;code&gt;ChunkLoadError&lt;/code&gt; by name and also required the container half of the suffix to name the remote. In a development build that half reads &lt;code&gt;webpack/container/reference/listApp&lt;/code&gt;; in a Release build that module is minified to its numeric id, &lt;code&gt;77469&lt;/code&gt;, so the check passed in development and failed exactly where it mattered. The second dropped the container check but still matched &lt;code&gt;ChunkLoadError&lt;/code&gt; by name, which is what a bad signature arrives as; a version the CDN does not hold arrives as &lt;code&gt;[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003&lt;/code&gt; with the same suffix, and it still killed the app. For a failed load, the guard matches the suffix alone.&lt;/p&gt;

&lt;p&gt;The suffix covers the first two breaks and not the third. The list's code downloaded and verified, then threw while its module was being evaluated. The remote's container is a bundle Re.Pack built too, so the outermost &lt;code&gt;require&lt;/code&gt; inside it is guarded as well, and it reports the throw as fatal before anything returns. The remote runtime saw a successful load, so nothing appended a suffix, and with the matcher alone the Release build died at launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*** Terminating app due to uncaught exception 'RCTFatalException: Unhandled JS Exception: Error: PokedexScreen failed to initialise'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard can recognise the third break's report by its timing instead: it arrives while a remote module is being evaluated. When the host imports a remote module, the remote runtime asks Module Federation for the module's factory unexecuted, with &lt;code&gt;loadFactory: false&lt;/code&gt;, and Module Federation passes that factory to every runtime plugin's &lt;code&gt;onLoad&lt;/code&gt; hook before anything calls it. A function returned from the hook replaces the factory. &lt;code&gt;src/shell/scriptManager.ts&lt;/code&gt; installs a plugin that returns one running the real factory inside &lt;code&gt;evaluateRemoteModule&lt;/code&gt;, so every remote module the host imports, for the tabs and at boot, is evaluated there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evaluationWindow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModuleFederationRuntimePlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;evaluation-window&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;onLoad&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;exposeModuleFactory&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;exposeModuleFactory&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;evaluateRemoteModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exposeModuleFactory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nf"&gt;registerPlugins&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;evaluationWindow&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;evaluateRemoteModule&lt;/code&gt;, in &lt;code&gt;federationErrors.ts&lt;/code&gt;, opens a window around the factory. Any fatal report raised while the window is open comes from evaluating that module, so the guard holds it instead of passing it on. Once the factory returns, &lt;code&gt;evaluateRemoteModule&lt;/code&gt; throws the held error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evaluateRemoteModule&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;globals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EVALUATING&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Evaluation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EVALUATING&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EVALUATING&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window is exact because evaluation is synchronous: nothing else runs between opening it and closing it. Only fatal reports are held, because only a fatal report ends the process.&lt;/p&gt;

&lt;p&gt;The error thrown from the window then reaches the host's own guarded require, which reports it as fatal a second time, outside the window. &lt;code&gt;remember&lt;/code&gt; keeps every error thrown from the window, and the guard drops that second report the way it drops one that ends with the suffix. The import settles without the module, and the tab reaches the same error state as the other two breaks.&lt;/p&gt;

&lt;p&gt;The installed guard puts the two paths together. It holds a fatal report raised inside a window, drops a report that ends with the suffix or repeats an error thrown from a window, and passes everything else to the handler that was there before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Where the guard keeps the handler it wraps. Fast Refresh can evaluate this module again in a&lt;/span&gt;
&lt;span class="c1"&gt;// running app, and each evaluation starts with fresh module state, so a flag in this file cannot&lt;/span&gt;
&lt;span class="c1"&gt;// tell a second installation from a first. The wrapper carries the handler underneath it instead,&lt;/span&gt;
&lt;span class="c1"&gt;// on a property every evaluation of the module knows by name. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WRAPPED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__federationGuardWrapped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GuardHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GlobalErrorHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;WRAPPED&lt;/span&gt;&lt;span class="p"&gt;]?:&lt;/span&gt; &lt;span class="nx"&gt;GlobalErrorHandler&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;guardHandledRemoteLoadErrors&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorUtils&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ErrorUtils&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ErrorUtilsShape&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;ErrorUtils&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;errorUtils&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GuardHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;errorUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getGlobalHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;WRAPPED&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GuardHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFatal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evaluation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="nx"&gt;EVALUATING&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Evaluation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evaluation&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isFatal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failure&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[federation] a remote module threw while it was evaluated; the import that asked for it settles without it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isHandledRemoteLoadError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isThrownFromEvaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Logged, not swallowed: the reason behind a tab's error state belongs in the console of&lt;/span&gt;
      &lt;span class="c1"&gt;// whoever is looking at it.&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[federation] a remote failed to load; its tab will show the error state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFatal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;WRAPPED&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;errorUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setGlobalHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing the guard again replaces the one it finds rather than wrapping it. Fast Refresh can run the module again during development, and a second wrapper would leave the first one's checks running underneath; &lt;code&gt;federationErrors.test.ts&lt;/code&gt; re-evaluates the module to hold it to one guard. The window and the record &lt;code&gt;remember&lt;/code&gt; keeps are stored on the global object for the same reason, so every evaluation of the module shares them.&lt;/p&gt;

&lt;p&gt;None of this gives the binary anywhere else to load from: a dead tab is honest, and it is not a working app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the stores permit
&lt;/h2&gt;

&lt;p&gt;Everything in this post downloads code into an installed app, so the platform rules apply, and both vendors have reworded these clauses before. Every quotation in this section comes from the current pages.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Governing text&lt;/th&gt;
&lt;th&gt;What it says&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apple, review&lt;/td&gt;
&lt;td&gt;App Review Guideline 2.5.2&lt;/td&gt;
&lt;td&gt;Apps may not "download, install, or execute code which introduces or changes features or functionality of the app, including other apps"&lt;/td&gt;
&lt;td&gt;The reviewed binary is what the app does; downloaded code may not add to or change that&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple, licence&lt;/td&gt;
&lt;td&gt;Developer Program License Agreement 3.3.1(B)&lt;/td&gt;
&lt;td&gt;"Interpreted code may be downloaded to an Application but only so long as" three conditions hold&lt;/td&gt;
&lt;td&gt;It keeps the app's intended and advertised purpose; it does not bypass the operating system's signing, sandbox or other security features; in an App Store app, it creates no store or storefront for other apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Play&lt;/td&gt;
&lt;td&gt;Device and Network Abuse policy&lt;/td&gt;
&lt;td&gt;An app "may not download executable code (such as dex, JAR, .so files) from a source other than Google Play"&lt;/td&gt;
&lt;td&gt;The restriction "does not apply to code that runs in a virtual machine or an interpreter where either provides indirect access to Android APIs"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apple's two documents draw different lines, and both apply. The licence agreement's clause 3.3.1(B) permits downloaded interpreted code only so long as it "does not change the primary purpose of the Application by providing features or functionality that are inconsistent with the intended and advertised purpose of the Application", "does not bypass signing, sandbox, or other security features of the OS", and, "for Applications distributed on the App Store, does not create a store or storefront for other Applications". Guideline 2.5.2 is stricter: the licence's first condition protects the app's purpose, while the guideline rules out downloaded code that introduces or changes the app's features or functionality. So meeting the licence's conditions does not establish compliance with review. The reading over-the-air services operate on is to keep what ships this way to fixes and adjustments within the functionality Apple already reviewed, accepting that the wording leaves Apple room to disagree.&lt;/p&gt;

&lt;p&gt;Google's rule names an exception rather than a condition: the restriction "does not apply to code that runs in a virtual machine or an interpreter where either provides indirect access to Android APIs (such as JavaScript in a webview or browser)". The policy does not name React Native or Hermes (the JavaScript engine React Native runs by default), so whether Hermes fits is an inference. It is a sound one: JavaScript under Hermes reaches Android APIs only through the natively compiled modules the binary already contains, which is indirect access in the clause's own terms. The chunks on this CDN are plain JavaScript rather than Hermes bytecode, because no remote in the series compiles to bytecode; the policy's line is about how code runs, not the form it ships in. The same page adds that interpreted code "loaded at run time (for example, not packaged with the app) must not allow potential violations of Google Play policies", so what a remote does is held to the same rules as the app it runs in.&lt;/p&gt;

&lt;p&gt;For React Native in particular, the closest guidance comes from the vendors who ship over-the-air updates for it. Microsoft's CodePush served that market for years and was retired on 31 March 2025. EAS Update, the over-the-air service in Expo Application Services, still runs, and its documentation holds updates to the stores' rules: "you need to follow the rules of the platforms and app stores you are building for", updates "need to follow the App Store and Play Store guidelines, including the content of the updates and how you use them", and "This usually means changes to your app's behavior need to be reviewed". Its table of when to use an update marks "Change to native code or native dependencies" and "Anything that requires a new app binary version" as cases for a new binary instead.&lt;/p&gt;

&lt;p&gt;The practical rule: &lt;strong&gt;ship fixes and improvements to features the store already reviewed, never a new primary purpose, and keep the reviewed binary able to work on its own.&lt;/strong&gt; The flip in this post is the kind of change the first half describes: it changes how an existing feature, the party counter, shows a state, and adds nothing new. Whether a particular change stays inside what was reviewed is the store's judgement, and no build can establish it. The second half needs the copy in the binary that post 16 adds, because today a binary that cannot reach the CDN has nothing to show.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Keys, tree, server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/gen-signing-keys.mjs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node tools/build-cdn.mjs ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx http-server@14.1.1 cdn-root &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;-c-1&lt;/span&gt; &lt;span class="nt"&gt;--cors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Release build, told where the CDN is and which binary it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A development build works the same way, with the two variables on the dev server's command instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm start &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Android, with the emulator's address for the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/build-cdn.mjs android &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://10.0.2.2:8000 &lt;span class="nv"&gt;MF_APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 npm run android &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suites, the smoke test and the generator's tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;apps/host apps/list apps/party packages/ui&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx jest &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sh scripts/federation-smoke.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node &lt;span class="nt"&gt;--test&lt;/span&gt; tools/gen-signing-keys.test.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The smoke test now builds both remotes at &lt;code&gt;9.9.9&lt;/code&gt;, a version no config defaults to, and follows the manifest's own chunk list into the version directory, so a version segment missing from &lt;code&gt;output.path&lt;/code&gt; fails at build time rather than at a user's launch.&lt;/p&gt;

&lt;p&gt;Run from the finished tree, &lt;code&gt;build-cdn&lt;/code&gt; builds all three list versions from the same source and seeds the 2.0.0 map with 1.2.0, so the three versions differ only in the chip. To watch a flip here, edit &lt;code&gt;cdn-root/ios/maps/2.0.0/version-map.json&lt;/code&gt; to name listApp 1.1.0 and relaunch, then put 1.2.0 back and relaunch again. The full-party state arriving with the flip belongs to the build-along, the only route that builds 1.0.0 and 1.1.0 before the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;An installed binary now asks the CDN for its own map at launch, loads the signed versions that map names, and refuses any remote the map gives no version. Shipping a remote is one directory and one line, and a rollback is the line put back; each binary takes either at its next launch. When a remote fails to load, or its module throws while it initialises, the Release build keeps running with one dead tab: measured on iOS with a refused chunk, a missing version and a module that throws as it initialises, and on Android with a missing version.&lt;/p&gt;

&lt;p&gt;Two limits remain. The map is unsigned, so anyone who can write to the bucket can steer every install; post 17 signs it, adds a counter that makes yesterday's map worthless, and has the app roll a failing version back on its own. The nearer limit is reach: a binary that cannot reach the CDN, or cannot read its map, launches in &lt;code&gt;unresolved&lt;/code&gt; mode with nothing to show in either tab.&lt;/p&gt;

&lt;p&gt;Next: the day the CDN is unreachable. An offline fallback baked into the binary, and an in-session net for the remote that fails mid-use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/guide/runtime/runtime-hooks" rel="noopener noreferrer"&gt;Module Federation: runtime hooks&lt;/a&gt; — &lt;code&gt;onLoad&lt;/code&gt;, the hook that receives a remote module's factory before anything calls it when that factory was requested with &lt;code&gt;loadFactory: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/guide/runtime/runtime-api" rel="noopener noreferrer"&gt;Module Federation: runtime API&lt;/a&gt; — &lt;code&gt;registerRemotes&lt;/code&gt; and its &lt;code&gt;force&lt;/code&gt; option: an already-registered module is overwritten and its cache deleted, and a warning says the operation is risky&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/module-federation/core/blob/v2.9.0/packages/runtime-core/src/remote/index.ts" rel="noopener noreferrer"&gt;Module Federation source: runtime-core remote/index.ts at 2.9.0&lt;/a&gt; — the branch for an already-registered name, which does nothing without &lt;code&gt;force&lt;/code&gt; and warns with it, and &lt;code&gt;loadRemote&lt;/code&gt;, which passes the &lt;code&gt;onLoad&lt;/code&gt; hook a factory nothing has run when &lt;code&gt;loadFactory&lt;/code&gt; is false, and returns a function the hook hands back in its place&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/module-federation/core/blob/v2.9.0/packages/webpack-bundler-runtime/src/remotes.ts" rel="noopener noreferrer"&gt;Module Federation source: webpack-bundler-runtime remotes.ts at 2.9.0&lt;/a&gt; — the error handler that appends &lt;code&gt;while loading "…" from …&lt;/code&gt; and replaces the failed module with one that throws, and the request for each remote module's factory with &lt;code&gt;loadFactory: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/module-federation/core/blob/v2.9.0/packages/runtime-core/src/module/index.ts" rel="noopener noreferrer"&gt;Module Federation source: runtime-core module/index.ts at 2.9.0&lt;/a&gt; — &lt;code&gt;get&lt;/code&gt;, which runs a module's factory before returning unless &lt;code&gt;loadFactory&lt;/code&gt; is false&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://re-pack.dev/api/runtime/script-manager" rel="noopener noreferrer"&gt;Re.Pack: ScriptManager&lt;/a&gt; — &lt;code&gt;addResolver&lt;/code&gt; with its &lt;code&gt;priority&lt;/code&gt; and &lt;code&gt;key&lt;/code&gt; options, the default priority of 2, and &lt;code&gt;verifyScriptSignature&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://re-pack.dev/api/plugins/code-signing" rel="noopener noreferrer"&gt;Re.Pack: CodeSigningPlugin&lt;/a&gt; — the plugin post 14 configured and this post verifies against&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/modules/ScriptManager/ScriptManager.ts" rel="noopener noreferrer"&gt;Re.Pack source: ScriptManager.ts at 5.2.5&lt;/a&gt; — &lt;code&gt;DEFAULT_RESOLVER_PRIORITY = 2&lt;/code&gt;, resolvers sorted highest first, &lt;code&gt;resolveScript&lt;/code&gt; asking the next resolver whenever one returns nothing and stopping when one throws, and the in-memory locator cache&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/modules/FederationRuntimePlugins/ResolverPlugin.ts" rel="noopener noreferrer"&gt;Re.Pack source: ResolverPlugin.ts at 5.2.5&lt;/a&gt; — the per-remote resolver registered with a key and no priority&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/plugins/OutputPlugin/OutputPlugin.ts" rel="noopener noreferrer"&gt;Re.Pack source: OutputPlugin.ts at 5.2.5&lt;/a&gt; — remote chunks copied to &lt;code&gt;extraChunks.outputPath&lt;/code&gt; from where Rspack wrote them, not moved&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/plugins/RepackTargetPlugin/implementation/guardedRequire.ts" rel="noopener noreferrer"&gt;Re.Pack source: guardedRequire.ts at 5.2.5&lt;/a&gt; — the &lt;code&gt;require&lt;/code&gt; Re.Pack builds into every bundle, which catches a module that throws, reports the error to &lt;code&gt;ErrorUtils&lt;/code&gt; as fatal and returns nothing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/modules/ScriptManager/types.ts" rel="noopener noreferrer"&gt;Re.Pack source: ScriptManager types at 5.2.5&lt;/a&gt; — &lt;code&gt;verifyScriptSignature&lt;/code&gt; as &lt;code&gt;'strict' | 'lax' | 'off'&lt;/code&gt;, and what &lt;code&gt;cache&lt;/code&gt; compares&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/ios/ScriptManager.mm" rel="noopener noreferrer"&gt;Re.Pack source: ScriptManager.mm at 5.2.5&lt;/a&gt; — verification inside the iOS download path, for &lt;code&gt;strict&lt;/code&gt; always and for &lt;code&gt;lax&lt;/code&gt; only when a token is present&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/android/src/main/java/com/callstack/repack/RemoteScriptLoader.kt" rel="noopener noreferrer"&gt;Re.Pack source: RemoteScriptLoader.kt at 5.2.5&lt;/a&gt; — the same check on Android, and a cached file executed without it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/ios/CodeSigningErrors.swift" rel="noopener noreferrer"&gt;Re.Pack source: CodeSigningErrors.swift&lt;/a&gt;, &lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/ios/CodeSigningUtils.swift" rel="noopener noreferrer"&gt;CodeSigningUtils.swift&lt;/a&gt; and &lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/android/src/main/java/com/callstack/repack/CodeSigningUtils.kt" rel="noopener noreferrer"&gt;CodeSigningUtils.kt at 5.2.5&lt;/a&gt; — the verification failures quoted in this post, the token read from the last 1,280 bytes, and &lt;code&gt;RepackPublicKey&lt;/code&gt; read from &lt;code&gt;Info.plist&lt;/code&gt; and from &lt;code&gt;strings.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/Libraries/Core/setUpXHR.js" rel="noopener noreferrer"&gt;React Native source: setUpXHR.js at 0.85.3&lt;/a&gt; — &lt;code&gt;AbortController&lt;/code&gt; and &lt;code&gt;AbortSignal&lt;/code&gt; installed from the &lt;code&gt;abort-controller&lt;/code&gt; package, which has no &lt;code&gt;timeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/polyfills/error-guard.js" rel="noopener noreferrer"&gt;React Native source: error-guard.js at 0.85.3&lt;/a&gt; — &lt;code&gt;ErrorUtils.reportFatalError&lt;/code&gt;, which calls the global handler with its fatal flag set, and &lt;code&gt;setGlobalHandler&lt;/code&gt; and &lt;code&gt;getGlobalHandler&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/Libraries/Core/ExceptionsManager.js" rel="noopener noreferrer"&gt;React Native source: ExceptionsManager.js at 0.85.3&lt;/a&gt; — LogBox in development; otherwise the error handed to the native error handler, which the new architecture installs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm" rel="noopener noreferrer"&gt;React Native source: RCTInstance.mm&lt;/a&gt; and &lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/React/CoreModules/RCTExceptionsManager.mm" rel="noopener noreferrer"&gt;RCTExceptionsManager.mm at 0.85.3&lt;/a&gt; — an error no host delegate claims forwarded to the exceptions module, whose fatal path calls &lt;code&gt;RCTFatal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/React/Base/RCTAssert.m" rel="noopener noreferrer"&gt;React Native source: RCTAssert.m at 0.85.3&lt;/a&gt; — &lt;code&gt;RCTFatal&lt;/code&gt; throwing an exception that only a debug build catches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.kt" rel="noopener noreferrer"&gt;React Native source: ExceptionsManagerModule.kt at 0.85.3&lt;/a&gt; — a fatal report thrown as a &lt;code&gt;JavascriptException&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt" rel="noopener noreferrer"&gt;React Native source: ReactInstance.kt&lt;/a&gt; and &lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultReactHost.kt" rel="noopener noreferrer"&gt;DefaultReactHost.kt at 0.85.3&lt;/a&gt; — that throw routed to the host's exception handler, which rethrows by default&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/app-store/review/guidelines/#software-requirements" rel="noopener noreferrer"&gt;App Store Review Guidelines, 2.5.2&lt;/a&gt; — the self-contained-apps constraint, quoted from the current text&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/support/terms/apple-developer-program-license-agreement/" rel="noopener noreferrer"&gt;Apple Developer Program License Agreement, 3.3.1(B)&lt;/a&gt; — the interpreted-code permission and its three conditions, quoted from the public copy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://support.google.com/googleplay/android-developer/answer/9888379" rel="noopener noreferrer"&gt;Google Play: Device and Network Abuse policy&lt;/a&gt; — the executable-code restriction, its interpreter exception and its rule for interpreted code loaded at run time, quoted from the current text&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.expo.dev/eas-update/introduction/" rel="noopener noreferrer"&gt;Expo: EAS Update introduction&lt;/a&gt; — the answer on store guidelines and the table of when to use an update, quoted from the current page&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/microsoft/react-native-code-push" rel="noopener noreferrer"&gt;microsoft/react-native-code-push&lt;/a&gt; — the archived repository, with its notice that CodePush was retired on 31 March 2025&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/http-party/http-server" rel="noopener noreferrer"&gt;http-server&lt;/a&gt; — the &lt;code&gt;-c&lt;/code&gt; option, where &lt;code&gt;-c-1&lt;/code&gt; disables caching&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-15-cdn-flip&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>The production build and the three modes of a federated React Native app</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 05 Oct 2026 00:03:42 +0000</pubDate>
      <link>https://dev.to/warrendeleon/the-production-build-and-the-three-modes-of-a-federated-react-native-app-4pp5</link>
      <guid>https://dev.to/warrendeleon/the-production-build-and-the-three-modes-of-a-federated-react-native-app-4pp5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/native-handoff-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;Post 13&lt;/a&gt; ended on a promise: "the build gets real: a production bundle, and the first federated release artefact." This is that post. The larger promise is &lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;post 1&lt;/a&gt;'s, that each feature "can be deployed and updated on its own instead of riding in a single App Store release", and the same post listed the price: a CDN to run, a URL that is an attack surface, code to sign and to verify. Thirteen posts in, both remotes still arrive from two dev servers on your machine. This post builds them for production, signs every chunk, and serves them from a content delivery network (CDN) with those dev servers switched off.&lt;/p&gt;

&lt;p&gt;The honest scope first. The CDN in this post is a directory and a port, holding one version of each remote, with nothing yet that verifies a signature, guards the choice of version, or falls back when the CDN is unreachable. The next three posts add all of that, and each of them handles the artefacts this one produces.&lt;/p&gt;

&lt;p&gt;Start from post 13's finished state, the start tag &lt;code&gt;post-13-native-handoff&lt;/code&gt;. The end tag is &lt;code&gt;post-14-production-build&lt;/code&gt;, and everything between the two you type.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/production-build-three-modes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Three channels, three cadences.&lt;/strong&gt; The registry's arrow ends at &lt;code&gt;node_modules&lt;/code&gt; and never reaches the running app: a package change ships inside the next build of whoever installs it. A remote change lands on the CDN and reaches the app at its next launch. A host change goes through the store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three modes, one table
&lt;/h2&gt;

&lt;p&gt;The series has lived in the first row since &lt;a href="https://warrendeleon.com/blog/your-first-federated-remote-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;post 2&lt;/a&gt; and used the second since &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;post 5&lt;/a&gt;, without naming either. Named, with the third beside them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;What it serves&lt;/th&gt;
&lt;th&gt;Who reads it, and when&lt;/th&gt;
&lt;th&gt;What a change costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dev servers, &lt;code&gt;:8082&lt;/code&gt; and &lt;code&gt;:8083&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A remote's manifest, container and chunks, rebuilt on every save&lt;/td&gt;
&lt;td&gt;The host app, at runtime, whenever &lt;code&gt;MF_CDN_BASE&lt;/code&gt; is unset: development builds, in practice&lt;/td&gt;
&lt;td&gt;A save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry, Verdaccio on &lt;code&gt;:4873&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;npm packages: &lt;code&gt;@pokedex/contracts&lt;/code&gt;, &lt;code&gt;detail&lt;/code&gt;, &lt;code&gt;ui&lt;/code&gt; and &lt;code&gt;a11y-testing&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm install&lt;/code&gt;, at build time, in every app that depends on the package&lt;/td&gt;
&lt;td&gt;A publish, then a reinstall and a rebuild of each consumer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN, &lt;code&gt;cdn-root/&lt;/code&gt; on &lt;code&gt;:8000&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A remote's manifest, container and chunks, built once and signed&lt;/td&gt;
&lt;td&gt;The host app, at runtime, development or release build&lt;/td&gt;
&lt;td&gt;A rebuild of the remote and a copy; every installed app picks it up at its next launch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The question the table exists to answer is the one most teams ask first: &lt;strong&gt;which changes still cost an app-store release?&lt;/strong&gt; Anything the binary carries: native code, the host shell, and every shared singleton the host provides, because the host's bundle is where the one copy of each lives. Anything a remote owns costs a CDN deploy and a launch. Packages sit between the two, and the middle row is the one to read twice.&lt;/p&gt;

&lt;p&gt;A registry is where versioned artefacts usually live, so it is a reasonable place to expect the remotes, and it is where a team new to federation tends to put them first. It does not hold them: &lt;strong&gt;the registry serves packages, never remotes.&lt;/strong&gt; A package reaches users inside whichever bundle compiles it in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/accessibility-testing-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;Post 12&lt;/a&gt; shipped &lt;code&gt;@pokedex/detail&lt;/code&gt; 4.0.12 as a publish and a reinstall in list and party; under this post's modes that same fix is two rebuilt remotes on the CDN and no store release, because the host never bundles the detail package. A fix to &lt;code&gt;@pokedex/contracts&lt;/code&gt; or &lt;code&gt;@pokedex/ui&lt;/code&gt; is different. The host provides both as eager singletons, so the host bundle changes, and the host bundle ships in the binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a remote for real
&lt;/h2&gt;

&lt;p&gt;Every bundle the series has run was a development build, served on save. A production build is the same command with the development switch off, and each remote gets it as two scripts. In &lt;code&gt;apps/list/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"bundle:ios:prod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-native bundle --platform ios --dev false --entry-file src/index.js --config rspack.config.mjs"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"bundle:android:prod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-native bundle --platform android --dev false --entry-file src/index.js --config rspack.config.mjs"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same two go in &lt;code&gt;apps/party/package.json&lt;/code&gt;. &lt;code&gt;react-native bundle&lt;/code&gt; reaches Re.Pack because post 2's &lt;code&gt;react-native.config.js&lt;/code&gt; registered its commands with the CLI (&lt;code&gt;webpack-bundle&lt;/code&gt; is an alias). &lt;code&gt;--entry-file src/index.js&lt;/code&gt; names the container entry post 2 gave each remote; the template's &lt;code&gt;index.js&lt;/code&gt; beside it registers the remote as a standalone app, which the CDN never serves.&lt;/p&gt;

&lt;p&gt;The config gains one boolean and reads it in two places that have to agree: where the compilation writes, and where the chunks the host will fetch are laid out. In &lt;code&gt;apps/list/rspack.config.mjs&lt;/code&gt;, after &lt;code&gt;const { mode, platform } = env;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Production builds write somewhere else and gain a signature; development is untouched.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A production build writes the tree the CDN serves, laid out as the URL path it is served&lt;/span&gt;
  &lt;span class="c1"&gt;// at: cdn/&amp;lt;platform&amp;gt;/listApp/. A development build keeps writing to build/, where the dev&lt;/span&gt;
  &lt;span class="c1"&gt;// server reads it from.&lt;/span&gt;
  &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cdn/[platform]/listApp`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/build/[platform]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ListApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A third block has nothing to do with federation and everything to do with trusting a green build. Re.Pack's default minimiser is &lt;code&gt;terser-webpack-plugin&lt;/code&gt;, and under Rspack that plugin has done nothing since its 5.6.0 release (&lt;a href="https://github.com/callstack/repack/issues/1390" rel="noopener noreferrer"&gt;callstack/repack#1390&lt;/a&gt;): the build reports success and every production chunk keeps its comments and whitespace. Name Rspack's own SWC minimiser beside it. Re.Pack merges your &lt;code&gt;optimization.minimizer&lt;/code&gt; into its own list rather than replacing it, so the default stays in the array and goes on doing nothing while the SWC one does the minifying. Add the import at the top of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SwcJsMinimizerRspackPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@rspack/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and, after &lt;code&gt;output&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;optimization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Re.Pack's default minimiser is terser-webpack-plugin, and under Rspack that plugin has done&lt;/span&gt;
  &lt;span class="c1"&gt;// nothing since terser-webpack-plugin 5.6.0 (callstack/repack#1390): the build succeeds and&lt;/span&gt;
  &lt;span class="c1"&gt;// every production chunk ships with its comments and whitespace intact. Re.Pack merges this&lt;/span&gt;
  &lt;span class="c1"&gt;// array into its own, so the default still sits in the list beside Rspack's SWC minimiser;&lt;/span&gt;
  &lt;span class="c1"&gt;// the SWC one does the minifying the default skips.&lt;/span&gt;
  &lt;span class="nl"&gt;minimizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SwcJsMinimizerRspackPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;js&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;bundle&lt;/span&gt;&lt;span class="se"&gt;(\?&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;minimizerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host gets the same import and block in &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;, because its release bundle is built the same way. Then, still in the remote's config, the chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RepackPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;extraChunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/.*/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// The chunks land beside the container and the manifest, because the host will ask&lt;/span&gt;
      &lt;span class="c1"&gt;// for them at URLs relative to the manifest it loaded.&lt;/span&gt;
      &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isProd&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`cdn/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/listApp`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`build/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/remote`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mirror all of it in &lt;code&gt;apps/party/rspack.config.mjs&lt;/code&gt; with &lt;code&gt;partyApp&lt;/code&gt;. Both output trees are build products, so they join &lt;code&gt;.gitignore&lt;/code&gt; next to &lt;code&gt;apps/*/build/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/*/cdn/
cdn-root/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build one and look at what comes out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run bundle:ios:prod &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ls &lt;/span&gt;apps/list/cdn/ios/listApp | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'\.map$'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assets
__federation_expose_ListStack.chunk.bundle
index.bundle
listApp.container.js.bundle
mf-manifest.json
mf-stats.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sixteen more chunks follow, most of them the remote's own copies of the shared singletons: fallbacks the host never requests, because it provides every one. The server log later in this post shows the few it does ask for. Three files matter here. The container is what the host loads first; the exposed chunk carries &lt;code&gt;./ListStack&lt;/code&gt;; and &lt;code&gt;mf-manifest.json&lt;/code&gt; is the address book, emitted by default alongside its &lt;code&gt;mf-stats.json&lt;/code&gt; twin. One field in it looks wrong on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"publicPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noop:///"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re.Pack's base config sets it, and nothing treats it as a location. The host fetched the manifest from a URL, and Re.Pack's resolver plugin rebases every chunk URL for that remote onto it, so each chunk is fetched from the directory the manifest came from. &lt;strong&gt;Where a remote was built has no bearing on where it is served&lt;/strong&gt;, which is what lets one build move from a laptop to a real CDN unchanged.&lt;/p&gt;

&lt;p&gt;One script assembles the tree a CDN would serve. Create &lt;code&gt;tools/build-cdn.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Assembles the directory a CDN would serve. For each remote it runs that remote's production&lt;/span&gt;
&lt;span class="c1"&gt;// bundle script, then copies the output — container, chunks and mf-manifest.json — into&lt;/span&gt;
&lt;span class="c1"&gt;// cdn-root/&amp;lt;platform&amp;gt;/&amp;lt;remote&amp;gt;/. That layout is the URL layout: a file at&lt;/span&gt;
&lt;span class="c1"&gt;// cdn-root/ios/listApp/mf-manifest.json is served at &amp;lt;base&amp;gt;/ios/listApp/mf-manifest.json, which is&lt;/span&gt;
&lt;span class="c1"&gt;// exactly the URL the host's remotes map asks for.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// One version of each remote lives here, in one flat directory. Versioned releases, and the map&lt;/span&gt;
&lt;span class="c1"&gt;// that decides which version a given app may load, arrive in the next post.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: node tools/build-cdn.mjs [ios|android]     (no argument builds both)&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Then serve it and point the host at it (an Android emulator reaches this machine at 10.0.2.2,&lt;/span&gt;
&lt;span class="c1"&gt;// so the Android build gets that address instead of localhost):&lt;/span&gt;
&lt;span class="c1"&gt;//   npx http-server@14.1.1 cdn-root -p 8000 -c-1 --cors&lt;/span&gt;
&lt;span class="c1"&gt;//   ( cd apps/host &amp;amp;&amp;amp; MF_CDN_BASE=http://localhost:8000 npm start )&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cpSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rmSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileURLToPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repoRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REMOTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown platform "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". Use one of: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platformArg&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platformArg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ALL_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cdnRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Wipe this platform only, so building one does not delete the other's tree.&lt;/span&gt;
  &lt;span class="nf"&gt;rmSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cdnRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cdnRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REMOTES&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n=== building &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) ===`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`npm run bundle:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:prod`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;cpSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cdnRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`copied -&amp;gt; cdn-root/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOST_ADDRESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://10.0.2.2:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\nCDN assembled at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cdnRoot&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Serve it:             npx http-server@14.1.1 cdn-root -p 8000 -c-1 --cors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Point the host at it: ( cd apps/host &amp;amp;&amp;amp; MF_CDN_BASE=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;HOST_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; npm start )   # &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sign what you ship
&lt;/h2&gt;

&lt;p&gt;The chunks now leave the machine, and post 1's integrity cost starts here. The private key signs every production chunk and must never reach the repository, and this repository is public, so the ignore rule goes in before the generator exists. In &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Signing keys. The private key signs every production chunk and must never be committed:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;# this directory is ignored before `node tools/gen-signing-keys.mjs` is ever run.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;code-signing/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Ignore &lt;code&gt;code-signing/&lt;/code&gt; before you generate a key, not after.&lt;/strong&gt; A private key that reaches a public repository is compromised the moment it is pushed. In this post the repair is a new key and a rebuild of every chunk the old one signed; once apps verify signatures, from post 15, it also means replacing the public key those apps trust, and with an embedded key that is an app update. Check that &lt;code&gt;git status&lt;/code&gt; shows nothing under &lt;code&gt;code-signing/&lt;/code&gt; before every commit from here on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Create &lt;code&gt;tools/gen-signing-keys.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Generates the keypair that signs every production remote chunk.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//   RSA-2048   signs each chunk Re.Pack's CodeSigningPlugin emits (an RS256 JWT of the chunk's&lt;/span&gt;
&lt;span class="c1"&gt;//              hash, appended to the bundle). The private half signs at build time; the public&lt;/span&gt;
&lt;span class="c1"&gt;//              half is what a host verifies against before it executes downloaded code.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The private key lives in code-signing/, inside the checkout but git-ignored before this script&lt;/span&gt;
&lt;span class="c1"&gt;// is ever run, and it is written readable by its owner only: .gitignore keeps it out of commits,&lt;/span&gt;
&lt;span class="c1"&gt;// the file mode keeps it from other users of the machine. A pair is generated only when neither&lt;/span&gt;
&lt;span class="c1"&gt;// half exists. An existing private key is kept, never rotated: a chunk signed with a new key&lt;/span&gt;
&lt;span class="c1"&gt;// would not verify against a public key already embedded in installed apps, and a signature&lt;/span&gt;
&lt;span class="c1"&gt;// stays valid only for the key that made it. A missing public half is derived from the private&lt;/span&gt;
&lt;span class="c1"&gt;// one; a public half on its own, or a pair that does not match, stops the script.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: node tools/gen-signing-keys.mjs&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPrivateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generateKeyPairSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chmodSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileURLToPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repoRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code-signing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privatePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private-key.pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public-key.pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OWNER_ONLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Keys are compared as DER bytes, never as PEM text: line endings and wrapping are not identity.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;der&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spki&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;der&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privatePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The mode is re-applied on every run, because a copy or an earlier tool may have left it wider.&lt;/span&gt;
  &lt;span class="nf"&gt;chmodSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privatePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OWNER_ONLY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;derived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createPrivateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privatePath&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spki&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunk-signing private key present; public key derived from it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;der&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;der&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not match &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;privatePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Neither key's contents were changed: decide which key the installed apps trust before touching either file.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunk-signing keypair already present, kept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A public key with no private key is the one state this script must not repair on its own:&lt;/span&gt;
  &lt;span class="c1"&gt;// generating a fresh pair here would silently change the signing identity behind a public key&lt;/span&gt;
  &lt;span class="c1"&gt;// that may already be embedded in installed apps.&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; exists but &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;privatePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not. Nothing was changed: restore the private key that made it, or plan a deliberate rotation and remove the public key by hand first.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPairSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rsa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;modulusLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privatePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pkcs1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OWNER_ONLY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spki&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generated an RSA-2048 chunk-signing keypair&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\nprivate key: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;privatePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  (signs the chunks; never commit it, never ship it)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`public key:  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  (what a host checks the signature against)\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/gen-signing-keys.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of the script's choices are about the key's life after this post. The private half is written readable by its owner only, since &lt;code&gt;.gitignore&lt;/code&gt; keeps it out of commits and the file mode keeps it from other users of the machine. And an existing pair is kept rather than rotated, and a public key on its own is never replaced: a chunk signed with a new key would not verify against a public key already embedded in installed apps, and a signature stays valid only for the key that made it. Once apps verify, rotating a key is a coordinated trust migration, with the old and new keys both trusted for a while or separate releases routed to each, and the posts that add verification and versioning own that design. This script refuses to start one by accident.&lt;/p&gt;

&lt;p&gt;The remotes' configs add the plugin in production mode only. In both &lt;code&gt;rspack.config.mjs&lt;/code&gt; files, at the end of &lt;code&gt;plugins&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Each production chunk gets an RS256 signature of its own hash, appended to the file. The&lt;/span&gt;
&lt;span class="c1"&gt;// private key sits in code-signing/, git-ignored inside the checkout, and never ships.&lt;/span&gt;
&lt;span class="c1"&gt;// Development builds stay unsigned by choice: their chunks are served from this machine&lt;/span&gt;
&lt;span class="c1"&gt;// and rebuilt on every save, and this post signs only what leaves it.&lt;/span&gt;
&lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;isProd&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Repack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CodeSigningPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;privateKeyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../code-signing/private-key.pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild and read the end of the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run bundle:ios:prod &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1300 apps/list/cdn/ios/listApp/listApp.container.js.bundle | xxd | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000000: 646c 652e 6d61 703f 706c 6174 666f 726d  dle.map?platform
00000010: 3d69 6f73 2f2a 2052 4353 5342 202a 2f65  =ios/* RCSSB */e
00000020: 794a 6862 4763 694f 694a 5355 7a49 314e  yJhbGciOiJSUzI1N
00000030: 6949 7349 6e52 3563 4349 3649 6b70 5856  iIsInR5cCI6IkpXV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/* RCSSB */&lt;/code&gt; is the marker, and what follows it is a JSON Web Token: the chunk's SHA-256 hash, signed with the private key using RS256. The plugin reserves the last 1,280 bytes of the file for the marker and the token, and pads the rest with zeros. The container and every chunk carry one. &lt;code&gt;index.bundle&lt;/code&gt; does not, because the plugin skips the main bundle as always local.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A signature nobody verifies is a stamp, not a lock.&lt;/strong&gt; Nothing in this build reads the token. Verification is &lt;code&gt;ScriptManager&lt;/code&gt;'s job, against a public key the app embeds as &lt;code&gt;RepackPublicKey&lt;/code&gt;, and it arrives with the resolver in post 15. Re.Pack 5.2.5, the version this series runs, configures the plugin with &lt;code&gt;enabled&lt;/code&gt;, &lt;code&gt;privateKeyPath&lt;/code&gt; and &lt;code&gt;excludeChunks&lt;/code&gt; and nothing else; 5.3.0, released on 5 August 2026, adds a &lt;code&gt;publicKeyPath&lt;/code&gt; that embeds the key for you. The series stays on 5.2.5 and embeds it in the post that reads it, so the key and its reader arrive together. The arc from here: &lt;strong&gt;14 signs, 15 verifies, 17 guards.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Serve it like production
&lt;/h2&gt;

&lt;p&gt;A CDN, for everything this post does, is a server that returns static files at stable URLs. Build both remotes into one tree and serve it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/build-cdn.mjs ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx http-server@14.1.1 cdn-root &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;-c-1&lt;/span&gt; &lt;span class="nt"&gt;--cors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-c-1&lt;/code&gt; turns the cache header off, so a rebuilt chunk is never served stale while you iterate. The host is the only app in the workspace with a remotes map: post 5 made the detail screen a package, and neither remote has consumed the other since. So the switch between the two worlds is one function in one file. In &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;, above &lt;code&gt;defineRspackConfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Where the remotes are served from. Set MF_CDN_BASE and every remote URL below points at the&lt;/span&gt;
&lt;span class="c1"&gt;// content delivery network instead of the dev servers; leave it unset and nothing changes. The&lt;/span&gt;
&lt;span class="c1"&gt;// value is read at BUILD time and baked into the bundle, so a build that forgot it ships the dev&lt;/span&gt;
&lt;span class="c1"&gt;// URLs:&lt;/span&gt;
&lt;span class="c1"&gt;//   MF_CDN_BASE=http://localhost:8000 npm start      (the local CDN, on a development build)&lt;/span&gt;
&lt;span class="c1"&gt;//   MF_CDN_BASE=https://cdn.example.com npm run …    (a real one, on a release build)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CDN_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEV_REMOTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8082&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8083&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside it, before the returned config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The whole switch between the two worlds, in one function: dev server or CDN, same manifest&lt;/span&gt;
&lt;span class="c1"&gt;// filename either way. Nothing else in the workspace changes, because the host is the only app&lt;/span&gt;
&lt;span class="c1"&gt;// here that holds a remotes map.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remoteUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CDN_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mf-manifest.json`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DEV_REMOTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mf-manifest.json`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;listApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;remoteUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;partyApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;remoteUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop the two remote dev servers. Start the host's with the variable set, and build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 npm start &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app looks exactly as it did at the end of post 13, which is the point. The proof is in the server's terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-09-17T22:34:47.686Z]  "GET /ios/partyApp/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.107Z]  "GET /ios/listApp/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.283Z]  "GET /ios/partyApp/partyApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.283Z]  "GET /ios/listApp/listApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.461Z]  "GET /ios/partyApp/__federation_expose_partySlice.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.461Z]  "GET /ios/partyApp/__federation_expose_styles.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.473Z]  "GET /ios/listApp/vendors-node_modules_pokedex_detail_src_index_ts-node_modules_graphql-request_build_entrypoin-878711.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.475Z]  "GET /ios/partyApp/vendors-node_modules_react-navigation_native-stack_lib_module_index_js.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-17T22:34:48.550Z]  "GET /ios/listApp/__federation_expose_ListStack.chunk.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two manifests, two containers, and the chunks the host could not provide: the detail package, which the host never bundles, and the native stack, which the host does not share, so the first remote to load supplies it for both. Not one request for React, React Native, Redux or the design system: the host provides all of those, as &lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-production-build" rel="noopener noreferrer"&gt;post 3&lt;/a&gt;'s singleton contract requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;Every break-it in the series so far has broken a running app. This one happens before the app exists. Build the Release scheme with the tree exactly as it is now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Welcome to Metro v0.84.5
              Fast - Scalable - Integrated

UnableToResolveError: Unable to resolve module ./global.css from …/react-native-module-federation/apps/host/index.js:

None of these files exist:
  * global.css(.ios.js|.native.js|.js|.ios.jsx|.native.jsx|.jsx|.ios.json|.native.json|.json|.ios.ts|.native.ts|.ts|.ios.tsx|.native.tsx|.tsx)
  * global.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the first line, not the last. &lt;strong&gt;Metro started.&lt;/strong&gt; Xcode's "Bundle React Native code and images" phase runs React Native's &lt;code&gt;react-native-xcode.sh&lt;/code&gt;, and that script's default &lt;code&gt;CLI_PATH&lt;/code&gt; is &lt;code&gt;scripts/bundle.js&lt;/code&gt;: Metro's bundle command, called directly. The React Native CLI never gets to choose the bundler: &lt;code&gt;bundle.js&lt;/code&gt; calls &lt;code&gt;cli.js config&lt;/code&gt; only to read &lt;code&gt;react-native.config.js&lt;/code&gt;'s project config, then invokes Metro's bundle function directly, so the &lt;code&gt;commands&lt;/code&gt; entry that points &lt;code&gt;bundle&lt;/code&gt; at Re.Pack is never consulted. Metro then meets &lt;code&gt;import './global.css'&lt;/code&gt; on line 6 of &lt;code&gt;index.js&lt;/code&gt; and stops at the first thing it cannot do. The error names a stylesheet; the fault is which bundler ran. The fix is three variables the script already reads. Append to &lt;code&gt;apps/host/ios/.xcode.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# --- The release bundle is built by Re.Pack, not Metro. Xcode's "Bundle React Native code and&lt;/span&gt;
&lt;span class="c"&gt;# images" phase calls React Native's bundle script directly: that script reads&lt;/span&gt;
&lt;span class="c"&gt;# react-native.config.js for the project config, through `cli.js config`, but never the&lt;/span&gt;
&lt;span class="c"&gt;# `commands` entry that hands `bundle` to Re.Pack, so left alone it runs Metro, which knows&lt;/span&gt;
&lt;span class="c"&gt;# nothing about global.css and stops there. CLI_PATH points the phase at the React Native CLI,&lt;/span&gt;
&lt;span class="c"&gt;# which does honour that entry; BUNDLE_COMMAND names the command Re.Pack registers there;&lt;/span&gt;
&lt;span class="c"&gt;# EXTRA_PACKAGER_ARGS hands it the Rspack config. $SRCROOT is this ios/ directory, so the app&lt;/span&gt;
&lt;span class="c"&gt;# root is its parent. ---&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLI_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRCROOT&lt;/span&gt;&lt;span class="s2"&gt;/../node_modules/react-native/cli.js"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BUNDLE_COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bundle
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;EXTRA_PACKAGER_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--config &lt;/span&gt;&lt;span class="nv"&gt;$SRCROOT&lt;/span&gt;&lt;span class="s2"&gt;/../rspack.config.mjs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the Release build again. It boots from the CDN with no dev server of any kind running, and the URLs sit in the bundle inside the app for anyone to check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-ao&lt;/span&gt; &lt;span class="s1"&gt;'http://[^"]*mf-manifest\.json'&lt;/span&gt; ~/Library/Developer/Xcode/DerivedData/Host-&lt;span class="k"&gt;*&lt;/span&gt;/Build/Products/Release-iphonesimulator/Host.app/main.jsbundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8000/ios/listApp/mf-manifest.json
http://localhost:8000/ios/partyApp/mf-manifest.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forget the variable on a release build and nothing complains: the dev-server URLs bake in instead, and this grep is how you find out before a tester does.&lt;/p&gt;

&lt;p&gt;Android needs no change to reach Re.Pack. The Gradle plugin's &lt;code&gt;bundleCommand&lt;/code&gt; defaults to &lt;code&gt;bundle&lt;/code&gt; and its &lt;code&gt;cliFile&lt;/code&gt; to &lt;code&gt;react-native/cli.js&lt;/code&gt;, the route through the config file that Xcode's script skips, and &lt;code&gt;android (Rspack 2.0.5) compiled&lt;/code&gt; appears inside &lt;code&gt;:app:createBundleReleaseJsAndAssets&lt;/code&gt;. Three things differ, though. The emulator reaches your machine at &lt;code&gt;10.0.2.2&lt;/code&gt;, not &lt;code&gt;localhost&lt;/code&gt;, so the variable changes with the platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://10.0.2.2:8000 npm run android &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gradle decides whether to run the bundle task at all by comparing the task's declared inputs with the previous run, and an environment variable is not one of them. Change only &lt;code&gt;MF_CDN_BASE&lt;/code&gt; and the task reports &lt;code&gt;UP-TO-DATE&lt;/code&gt;, and the previous bundle ships. Declare the variable as an input, at the end of &lt;code&gt;apps/host/android/app/build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- MF_CDN_BASE is read by rspack.config.mjs when the bundle task runs, and Gradle cannot see&lt;/span&gt;
&lt;span class="c1"&gt;// that: an environment variable is not one of the task's declared inputs, so a build where only&lt;/span&gt;
&lt;span class="c1"&gt;// the value changed leaves createBundleReleaseJsAndAssets UP-TO-DATE and ships the previous&lt;/span&gt;
&lt;span class="c1"&gt;// bundle, dev-server URLs and all. Declaring it as an input makes a changed value rebuild the&lt;/span&gt;
&lt;span class="c1"&gt;// bundle and an unchanged one keep the cache. ---&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;configureEach&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"createBundle"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"JsAndAssets"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MF_CDN_BASE"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MF_CDN_BASE"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a release APK refuses plain http. React Native's Gradle plugin sets &lt;code&gt;usesCleartextTraffic&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; for release builds, and the boot dies on &lt;code&gt;[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003&lt;/code&gt;, with &lt;code&gt;Network request failed&lt;/code&gt; underneath. A production CDN is served over https and never meets this. The local one is not, so a network security config permits the loopback addresses and nothing else. Create &lt;code&gt;apps/host/android/app/src/main/res/xml/network_security_config.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- The local stand-in for a CDN is served over plain http on the build machine, which a release
     build refuses to talk to. This permits it for the loopback addresses only: localhost, 127.0.0.1
     and 10.0.2.2, the address an emulator reaches the machine on. A production CDN is served over
     https, where this exception does nothing at all. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;network-security-config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;domain-config&lt;/span&gt; &lt;span class="na"&gt;cleartextTrafficPermitted=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&lt;/span&gt; &lt;span class="na"&gt;includeSubdomains=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;localhost&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&lt;/span&gt; &lt;span class="na"&gt;includeSubdomains=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;127.0.0.1&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&lt;/span&gt; &lt;span class="na"&gt;includeSubdomains=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;10.0.2.2&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/domain-config&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/network-security-config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and name it on the &lt;code&gt;&amp;lt;application&amp;gt;&lt;/code&gt; element in &lt;code&gt;AndroidManifest.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;android:networkSecurityConfig="@xml/network_security_config"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS needed no equivalent: the template's &lt;code&gt;Info.plist&lt;/code&gt; already carries &lt;code&gt;NSAllowsLocalNetworking&lt;/code&gt;, and the Release build reached &lt;code&gt;localhost:8000&lt;/code&gt; with no change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Three terminals, and two are missing. The remotes' dev servers, one per remote in every post since post 2, do not start. Keys, build, serve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/gen-signing-keys.mjs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node tools/build-cdn.mjs ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx http-server@14.1.1 cdn-root &lt;span class="nt"&gt;-p&lt;/span&gt; 8000 &lt;span class="nt"&gt;-c-1&lt;/span&gt; &lt;span class="nt"&gt;--cors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 npm start &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The release builds use the same tree and carry the URL on the build command, since that is where it is read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000 npm run ios &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; Release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/build-cdn.mjs android &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;MF_CDN_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://10.0.2.2:8000 npm run android &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; release &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in this post touches the code the suites cover, and they say so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;apps/host apps/list apps/party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx jest &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add two Pokémon, open the Party tab, run a Quick Battle. Everything post 13 built still works, and every request that made it work is a line in the server's log.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;Two remotes built for production, each chunk carrying a signature, laid out as the directory a CDN serves; a host that moves between the dev servers and that directory on one environment variable, read at build time; and a release build on each platform that boots from it with no dev server running. The app looks the same as it did in post 13. The terminals do not.&lt;/p&gt;

&lt;p&gt;The limits, each with its owner. The CDN holds one version of each remote, so a new build replaces the old one for every installed binary at once, whether or not that binary can run it; post 15 adds versioned directories, a version map and a resolver that chooses at each launch. Nothing verifies the signatures; that resolver does, against a public key embedded beside it. An app that cannot reach the CDN has nothing to fall back to; post 16 puts a copy of each remote in the binary. And the map that will decide which version an app loads is itself unsigned until post 17.&lt;/p&gt;

&lt;p&gt;Next: versioned remotes on the CDN, a version map, a per-launch resolver, and an old binary that never downloads code it cannot run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://re-pack.dev/api/cli/bundle" rel="noopener noreferrer"&gt;Re.Pack: bundle&lt;/a&gt; — the production bundle command and its &lt;code&gt;--dev&lt;/code&gt;, &lt;code&gt;--entry-file&lt;/code&gt;, &lt;code&gt;--platform&lt;/code&gt; and &lt;code&gt;--config&lt;/code&gt; flags&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://re-pack.dev/api/plugins/code-signing" rel="noopener noreferrer"&gt;Re.Pack: CodeSigningPlugin&lt;/a&gt; — the plugin's purpose and options; the page now documents the &lt;code&gt;publicKeyPath&lt;/code&gt; that 5.3.0 added&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://re-pack.dev/api/runtime/script-manager" rel="noopener noreferrer"&gt;Re.Pack: ScriptManager&lt;/a&gt; — &lt;code&gt;verifyScriptSignature&lt;/code&gt; and the embedded &lt;code&gt;RepackPublicKey&lt;/code&gt; it falls back to, the next post's work&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://re-pack.dev/api/plugins/module-federation-v2" rel="noopener noreferrer"&gt;Re.Pack: ModuleFederationV2Plugin&lt;/a&gt; — the &lt;code&gt;name@url&lt;/code&gt; remotes format and the resolver runtime plugin the plugin adds by default&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/plugins/CodeSigningPlugin/CodeSigningPlugin.ts" rel="noopener noreferrer"&gt;Re.Pack source: CodeSigningPlugin.ts at 5.2.5&lt;/a&gt; — the &lt;code&gt;/* RCSSB */&lt;/code&gt; marker, the SHA-256 hash signed as an RS256 token, the 1,280-byte reservation, and the main bundle skipped as always local&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/modules/FederationRuntimePlugins/ResolverPlugin.ts" rel="noopener noreferrer"&gt;Re.Pack source: ResolverPlugin.ts at 5.2.5&lt;/a&gt; — every asset URL rebased onto the manifest URL the host fetched&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/issues/1390" rel="noopener noreferrer"&gt;callstack/repack#1390&lt;/a&gt; — the default minimiser doing nothing under Rspack from terser-webpack-plugin 5.6.0, with the build still green&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/blob/f669cb5800a2286bb59ecd319647a2676f012b96/packages/repack/src/commands/common/config/getMinimizerConfig.ts" rel="noopener noreferrer"&gt;Re.Pack source: getMinimizerConfig.ts at 5.2.5&lt;/a&gt; — terser-webpack-plugin chosen for every Rspack version but 1.4.11&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/scripts/bundle.js" rel="noopener noreferrer"&gt;React Native source: bundle.js at 0.85.3&lt;/a&gt; — the script Xcode's phase calls: project config through &lt;code&gt;cli.js config&lt;/code&gt;, then Metro's bundle function directly&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://module-federation.io/configure/manifest.html" rel="noopener noreferrer"&gt;Module Federation: manifest&lt;/a&gt; — &lt;code&gt;mf-manifest.json&lt;/code&gt; and &lt;code&gt;mf-stats.json&lt;/code&gt;, the default file names&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/react-native/scripts/react-native-xcode.sh" rel="noopener noreferrer"&gt;React Native source: react-native-xcode.sh at 0.85.3&lt;/a&gt; — &lt;code&gt;CLI_PATH&lt;/code&gt; defaulting to Metro's &lt;code&gt;scripts/bundle.js&lt;/code&gt;, and the &lt;code&gt;BUNDLE_COMMAND&lt;/code&gt; and &lt;code&gt;EXTRA_PACKAGER_ARGS&lt;/code&gt; variables the fix sets&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt" rel="noopener noreferrer"&gt;React Native source: ReactExtension.kt at 0.85.3&lt;/a&gt; — &lt;code&gt;cliFile&lt;/code&gt; defaulting to &lt;code&gt;cli.js&lt;/code&gt; and &lt;code&gt;bundleCommand&lt;/code&gt; to &lt;code&gt;bundle&lt;/code&gt;, which is why Android reaches Re.Pack unchanged&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebook/react-native/blob/v0.85.3/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt" rel="noopener noreferrer"&gt;React Native source: AgpConfiguratorUtils.kt at 0.85.3&lt;/a&gt; — &lt;code&gt;usesCleartextTraffic&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt; for release builds&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.gradle.org/9.3.1/userguide/incremental_build.html" rel="noopener noreferrer"&gt;Gradle: incremental build&lt;/a&gt; — up-to-date checks compare fingerprints of declared inputs, and &lt;code&gt;inputs.property&lt;/code&gt; declares one&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/privacy-and-security/security-config" rel="noopener noreferrer"&gt;Android: network security configuration&lt;/a&gt; — cleartext off by default since Android 9, and the &lt;code&gt;domain-config&lt;/code&gt; that permits it per host&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/studio/run/emulator-networking-address" rel="noopener noreferrer"&gt;Android: emulator network address space&lt;/a&gt; — &lt;code&gt;10.0.2.2&lt;/code&gt; as the alias for the development machine's loopback interface&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/http-party/http-server" rel="noopener noreferrer"&gt;http-server&lt;/a&gt; — the &lt;code&gt;-p&lt;/code&gt;, &lt;code&gt;-c&lt;/code&gt; and &lt;code&gt;--cors&lt;/code&gt; options&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-14-production-build&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>shell.navigateTo: native screens from a federated remote in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 21 Sep 2026 00:05:30 +0000</pubDate>
      <link>https://dev.to/warrendeleon/shellnavigateto-native-screens-from-a-federated-remote-in-react-native-2knl</link>
      <guid>https://dev.to/warrendeleon/shellnavigateto-native-screens-from-a-federated-remote-in-react-native-2knl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/accessibility-testing-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;Post 12&lt;/a&gt; ended with the host offering the remotes a loan: its native side, through a bridge that launches a native screen from the Party tab and waits for the result. This post builds the bridge. A button in a federated tab opens a fully native screen, SwiftUI on one platform and Jetpack Compose on the other, and the battle's winner returns as one promise's resolution.&lt;/p&gt;

&lt;p&gt;The constraint underneath has been in the series since &lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;post 1&lt;/a&gt;: runtime delivery moves JavaScript and nothing else. &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;Post 11&lt;/a&gt; met it at the design system, and Re.Pack's limitations list states the host's half of the deal: "Host application must have native modules used in other containers". A remote that wants a native screen cannot bring one. The host lends its own.&lt;/p&gt;

&lt;p&gt;The loan's shape: &lt;strong&gt;a remote sees exactly one function&lt;/strong&gt;, &lt;code&gt;shellNavigate(destination, params?)&lt;/code&gt; from the contract, returning a promise. The title's &lt;code&gt;shell.navigateTo&lt;/code&gt; is this function, under the contract's exported name. Behind it sit the host's routing table and the blog's first TurboModule (React Native's typed native-module system), with its first Swift and Kotlin beyond the template. The round trip:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/native-handoff-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One practical difference before the first command. Until now the clone was a convenience: everything was typed, and a tree built by hand since &lt;a href="https://warrendeleon.com/blog/your-first-federated-remote-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;post 2&lt;/a&gt; served just as well. This post is the first where &lt;strong&gt;the companion repo is required input&lt;/strong&gt;: the native screens arrive from its tag, and by the time a TurboModule registers, your native project has to match the tag's. Start from post 12's finished state, the start tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-12-a11y-testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The finished state is the end tag, &lt;code&gt;post-13-native-handoff&lt;/code&gt;; a fetch later in the build reaches into it, and everything else you type.&lt;/p&gt;

&lt;h2&gt;
  
  
  One function in the contract
&lt;/h2&gt;

&lt;p&gt;The contract gains one file, the remote's entire view of the native world. Create &lt;code&gt;packages/contracts/src/shellNavigation.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- The shell's routing surface, and the whole of what a remote knows about native. A remote&lt;/span&gt;
&lt;span class="c1"&gt;// imports `shellNavigate` and nothing else: no TurboModule, no native types, no idea whether the&lt;/span&gt;
&lt;span class="c1"&gt;// destination it names is another micro-app or a fully native screen. The host owns the table that&lt;/span&gt;
&lt;span class="c1"&gt;// decides, so migrating a native flow to React Native later is one row edit here and no change in&lt;/span&gt;
&lt;span class="c1"&gt;// any remote. ---&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RouteEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="cm"&gt;/** Hands off to native: the host calls openNative, a native screen presents, and the promise
   *  resolves with whatever that screen passed back. */&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;nativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// One entry, because one thing dispatches. The union above is written as a union of one so a&lt;/span&gt;
&lt;span class="c1"&gt;// micro-app variant can join it without the readers of `entry.type` changing shape; a registry row&lt;/span&gt;
&lt;span class="c1"&gt;// with nothing behind it is a dead constant, and this series has already paid for one of those.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROUTE_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RouteEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;QuickBattle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quickBattle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// --- What crosses the boundary, in both directions. These types live in the contract because the&lt;/span&gt;
&lt;span class="c1"&gt;// HOST has to serialise them, not because another app dispatches anything: the battle's outcome is&lt;/span&gt;
&lt;span class="c1"&gt;// the party's own business and the party's own reducer handles it. ---&lt;/span&gt;

&lt;span class="cm"&gt;/** RN -&amp;gt; native. The party hands over its members; the native screen shows and battles them.
 *
 *  The colour scheme travels with them because it has to. Every federated surface reads the one
 *  styling runtime the host mounts, and a native screen is the one consumer that cannot subscribe
 *  to it: there is no bundle to share and no provider to sit under. So the theme stops being
 *  ambient at this boundary and becomes an argument, sent at the moment of the call. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;QuickBattleParams&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;colourScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Native -&amp;gt; RN. The uid, not the id: two copies of the same Pokémon are two contestants, and the
 *  party has stamped a uid on each member since 3.1.0 for exactly that reason. Absent when the
 *  screen closed without a battle. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;QuickBattleResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;winnerUid&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Native resolves with an object, or with nothing when the flow ended without a result. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ShellNavigateResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- The slot the host fills and every remote reads. It is a globalThis key rather than a React&lt;/span&gt;
&lt;span class="c1"&gt;// context because this package is bundled into the host AND into each remote: a context created&lt;/span&gt;
&lt;span class="c1"&gt;// here would be a different object in every bundle, so a remote's useContext would never find the&lt;/span&gt;
&lt;span class="c1"&gt;// host's provider. The module identity that posts 3 and 6 made a singleton for is exactly what is&lt;/span&gt;
&lt;span class="c1"&gt;// missing at this seam, and globalThis is the one slot all three bundles genuinely share. ---&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GLOBAL_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__POKEDEX_SHELL_NAVIGATE__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerShellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nx"&gt;GLOBAL_KEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shellNavigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ShellNavigateResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nx"&gt;GLOBAL_KEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No handler: warn and resolve. A remote that awaits this gets undefined and renders something&lt;/span&gt;
    &lt;span class="c1"&gt;// honest, which is the same tolerance the party slice's read shape is built on.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[shellNavigate] no handler registered (called with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comments carry the three decisions: one registry entry, since only one thing dispatches; the theme as an argument, since a native screen cannot subscribe to the styling runtime; and a &lt;code&gt;globalThis&lt;/code&gt; slot rather than a React context, which bundling into every app would make a different object in each. &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;Post 8&lt;/a&gt;'s prepare-stamped uid gets its first cross-boundary consumer here: the value that leaves the app and comes back.&lt;/p&gt;

&lt;p&gt;React Navigation's &lt;a href="https://reactnavigation.org/docs/native-stack-navigator/" rel="noopener noreferrer"&gt;native stack&lt;/a&gt; deserves naming first, and for native-backed React Native screens inside a navigator it is the right tool. What it does not model is a screen the host owns natively, outside React's tree, taking input from one remote and returning a value to it. That round trip is the bridge's reason to exist.&lt;/p&gt;

&lt;p&gt;Export it, bump, and publish through the same &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;Verdaccio flow post 5 established&lt;/a&gt;. In &lt;code&gt;packages/contracts/src/index.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./shellNavigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;packages/contracts/package.json&lt;/code&gt;, &lt;code&gt;"version": "3.3.0"&lt;/code&gt;: an additive minor on post 12's 3.2.2. Two tooling edits come with it: the new file calls &lt;code&gt;console.warn&lt;/code&gt;, so the contracts &lt;code&gt;tsconfig.json&lt;/code&gt; adds &lt;code&gt;"DOM"&lt;/code&gt; to its &lt;code&gt;lib&lt;/code&gt; array as &lt;code&gt;ui&lt;/code&gt; and &lt;code&gt;a11y-testing&lt;/code&gt; already do, and the &lt;code&gt;build&lt;/code&gt; script gains a dist clean so stale declaration files cannot survive into a publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node -e &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;require('fs').rmSync('dist',{recursive:true,force:true})&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; tsc"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publish, then take it in all three apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;app &lt;span class="k"&gt;in &lt;/span&gt;host list party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/&lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/contracts@3.3.0 &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The spec and the codegen
&lt;/h2&gt;

&lt;p&gt;A TurboModule starts as a TypeScript spec, read at build time by React Native's code generation, which writes the base classes the native implementations extend. Create &lt;code&gt;apps/host/specs/NativeShellNavigationModule.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TurboModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TurboModuleRegistry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- The TurboModule spec: the one place the host's JavaScript and its native code agree on a&lt;/span&gt;
&lt;span class="c1"&gt;// signature. Codegen reads this file at build time and writes the C++/ObjC++ and Kotlin base&lt;/span&gt;
&lt;span class="c1"&gt;// classes the native implementations extend, which is why the file lives outside src/ and why&lt;/span&gt;
&lt;span class="c1"&gt;// its name has to start with `Native`.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// One method, and it is asynchronous by shape: openNative hands a native flow its input and&lt;/span&gt;
&lt;span class="c1"&gt;// resolves with whatever that flow passed back. The promise is the whole design — the party's&lt;/span&gt;
&lt;span class="c1"&gt;// await does not return until the native screen has finished, and every exit path of that screen&lt;/span&gt;
&lt;span class="c1"&gt;// has to settle it exactly once.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The boundary is JSON strings in both directions rather than codegen object types. Codegen can&lt;/span&gt;
&lt;span class="c1"&gt;// carry structured objects, and a bigger bridge should let it; a string keeps the serialisation&lt;/span&gt;
&lt;span class="c1"&gt;// visible in one place on each side, which is easier to follow while the bridge is one method&lt;/span&gt;
&lt;span class="c1"&gt;// wide. ---&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Spec&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TurboModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;openNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paramsJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// getEnforcing throws when no native module answers to the name: a binary built without the&lt;/span&gt;
&lt;span class="c1"&gt;// native half, or a codegen mismatch. React Native's own examples import a spec like this at&lt;/span&gt;
&lt;span class="c1"&gt;// module scope, and with the module in the binary that is fine. The host's handler requires this&lt;/span&gt;
&lt;span class="c1"&gt;// file lazily instead, as a choice about where that throw is allowed to land. Imported at boot,&lt;/span&gt;
&lt;span class="c1"&gt;// a missing native half kills the shell during bundle evaluation, before any error boundary&lt;/span&gt;
&lt;span class="c1"&gt;// exists; required at the call, the same fault surfaces at the button press, in a running app&lt;/span&gt;
&lt;span class="c1"&gt;// that can log it and carry on. Runtime delivery is what makes the skew real: JavaScript can&lt;/span&gt;
&lt;span class="c1"&gt;// arrive on a binary that never built the native side.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TurboModuleRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getEnforcing&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ShellNavigationModule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The boundary is JSON strings in both directions&lt;/strong&gt;; the spec's opening comment ends on the why. Tell codegen where the spec lives, in &lt;code&gt;apps/host/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"codegenConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HostSpecs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"modules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsSrcsDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"specs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"android"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"javaPackageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.host.specs"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codegen runs during pod install, so run it now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host/ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;pod &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host's handler is the routing table's reader. Create &lt;code&gt;apps/host/src/shell/shellNavigation.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ROUTE_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Spec&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigationSpec&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../specs/NativeShellNavigationModule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- The host's half of shell.navigateTo. A remote calls the contract's shellNavigate; this is&lt;/span&gt;
&lt;span class="c1"&gt;// what runs. It looks the destination up in the routing table and hands native destinations to the&lt;/span&gt;
&lt;span class="c1"&gt;// TurboModule, JSON in and JSON out. The remote never learns which branch it took. ---&lt;/span&gt;

&lt;span class="c1"&gt;// The spec module calls TurboModuleRegistry.getEnforcing at import, and that throws when no&lt;/span&gt;
&lt;span class="c1"&gt;// native module answers to the name. Requiring it here, inside the call, decides where that&lt;/span&gt;
&lt;span class="c1"&gt;// throw can land: at the tap, in a running shell that logs it and carries on, rather than during&lt;/span&gt;
&lt;span class="c1"&gt;// boot evaluation, where a missing native half would kill the app with no error boundary.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nativeModule&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigationSpec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../specs/NativeShellNavigationModule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigationSpec&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTE_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// An unknown destination is a caller's bug, not a crash: warn and resolve, so a remote built&lt;/span&gt;
    &lt;span class="c1"&gt;// against a newer contract than the host degrades to nothing happening.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[shellNavigate] unknown destination: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;nativeModule&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;openNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{}));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ShellNavigateResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// A native result that does not parse is a native-side bug, and not a reason to detonate the&lt;/span&gt;
    &lt;span class="c1"&gt;// remote's await: warn and resolve, the same tolerance every other edge of this seam shows.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[shellNavigate] unparseable native result for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The lazy &lt;code&gt;require()&lt;/code&gt; chooses where a missing native half fails: at the tap, not at boot.&lt;/strong&gt; &lt;code&gt;getEnforcing&lt;/code&gt; throws when no module answers to the name. React Native's own examples import a spec at module scope, which works while the module is in the binary; runtime delivery makes the skew real. At boot the fault is a white screen with no error boundary; at the call, a loggable failure on one button.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Registration is two lines in &lt;code&gt;apps/host/App.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;partyStateReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registerShellNavigateHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shellNavigateHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/shell/shellNavigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The host fills the contract's navigation slot once, at module scope, before any remote can&lt;/span&gt;
&lt;span class="c1"&gt;// render and call shellNavigate. Registering the handler touches no native code — the TurboModule&lt;/span&gt;
&lt;span class="c1"&gt;// is not reached until a destination is actually navigated to — so this is safe at import in a way&lt;/span&gt;
&lt;span class="c1"&gt;// that requiring the spec here would not be.&lt;/span&gt;

&lt;span class="nf"&gt;registerShellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shellNavigateHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type the bridge, fetch the screens
&lt;/h2&gt;

&lt;p&gt;From here the work splits by what each half teaches. The bridge is federation, and you type all of it: the module holding a promise open across the boundary, its registration, the threading rules. The screens are SwiftUI and Compose UI code, so they arrive from this post's own end tag. Around a thousand lines stay off your keyboard, none unexplained.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-13-native-handoff /tmp/pokedex-ref-13
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/ios/Host/QuickBattle.swift apps/host/ios/Host/QuickBattle.swift
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/android/app/src/main/java/com/host/QuickBattleActivity.kt apps/host/android/app/src/main/java/com/host/QuickBattleActivity.kt
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/__tests__/shellNavigation.test.ts apps/host/__tests__/shellNavigation.test.ts
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/party/__tests__/quickBattle.test.tsx apps/party/__tests__/quickBattle.test.tsx
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/host/android/app/src/test/java/com/host apps/host/ios/HostTests
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/android/app/src/test/java/com/host/ShellNavigationModuleTest.kt apps/host/android/app/src/test/java/com/host/ShellNavigationModuleTest.kt
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref-13/apps/host/ios/HostTests/QuickBattlePresenterTests.swift apps/host/ios/HostTests/QuickBattlePresenterTests.swift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both screens wear the design system. No bundle crosses this boundary, so each file mirrors &lt;code&gt;colours.ts&lt;/code&gt; and the eighteen type colours by hand, rebuilt into the card language the Pokédex grid renders: tinted sprite disc, hashed number pill, type badges, accent foot. Each resolves the &lt;code&gt;colourScheme&lt;/code&gt; the params carry against the same pairs the &lt;code&gt;dark:&lt;/code&gt; rules use; the Compose buttons declare &lt;code&gt;Role.Button&lt;/code&gt;, since a styled &lt;code&gt;Text&lt;/code&gt; otherwise announces to TalkBack as plain text; and each screen wears a purple NATIVE badge. No federated screen carries one, so a screenshot declares its side of the boundary on its own.&lt;/p&gt;

&lt;p&gt;Four suites arrive with the screens. The JavaScript pair pins the bridge's seam: the host's, the contract slot and the handler, unparseable results included; the party's, the button's gating and the winner landing through the private action. The native pair holds the promise guarantees the next two sections build, wired in after the exit table.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS: every exit resolves
&lt;/h2&gt;

&lt;p&gt;The iOS module is Objective-C++, and the extension is the point: &lt;code&gt;getTurboModule&lt;/code&gt; returns the generated JSI module (JavaScript Interface, the C++ layer under the new architecture) as a &lt;code&gt;std::shared_ptr&lt;/code&gt;, which Swift cannot express. The screen stays pure SwiftUI; the &lt;code&gt;.mm&lt;/code&gt; is a thin adapter. In Xcode, create &lt;code&gt;ShellNavigationModule.h&lt;/code&gt; and &lt;code&gt;ShellNavigationModule.mm&lt;/code&gt; in the &lt;code&gt;Host&lt;/code&gt; group, which puts them in the target, and add the fetched &lt;code&gt;QuickBattle.swift&lt;/code&gt; with File → Add Files to "Host".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &amp;lt;React/RCTBridgeModule.h&amp;gt;
#import &amp;lt;HostSpecs/HostSpecs.h&amp;gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// --- The host's native half of shell.navigateTo on iOS. Codegen read specs/&lt;/span&gt;
&lt;span class="c1"&gt;// NativeShellNavigationModule.ts and wrote NativeShellNavigationModuleSpecBase and the&lt;/span&gt;
&lt;span class="c1"&gt;// NativeShellNavigationModuleSpec protocol into HostSpecs; this class subclasses the one and&lt;/span&gt;
&lt;span class="c1"&gt;// conforms to the other, which is what makes the JavaScript `openNative` land here.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The header exists because the implementation is ObjC++ (.mm) rather than Swift: returning the&lt;/span&gt;
&lt;span class="c1"&gt;// generated C++ JSI module from getTurboModule needs C++, and Swift cannot. The screen it&lt;/span&gt;
&lt;span class="c1"&gt;// presents is pure SwiftUI — see QuickBattle.swift. ---&lt;/span&gt;
&lt;span class="k"&gt;@interface&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NativeShellNavigationModuleSpecBase&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#import "ShellNavigationModule.h"
#import &amp;lt;HostSpecs/HostSpecs.h&amp;gt;
&lt;/span&gt;&lt;span class="c1"&gt;// Host-Swift.h declares every @objc Swift class in this app, which is how this ObjC++ file reaches&lt;/span&gt;
&lt;span class="c1"&gt;// QuickBattlePresenter. Imported after the app-delegate header so the generated header's forward&lt;/span&gt;
&lt;span class="c1"&gt;// references resolve in this translation unit.&lt;/span&gt;
&lt;span class="cp"&gt;#import &amp;lt;React-RCTAppDelegate/RCTDefaultReactNativeFactoryDelegate.h&amp;gt;
#import "Host-Swift.h"
&lt;/span&gt;
&lt;span class="c1"&gt;// --- The TurboModule implementation. Two things live here and nothing else: the registration&lt;/span&gt;
&lt;span class="c1"&gt;// that puts the module in the registry under the name the spec asked for, and openNative.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// openNative is where the promise starts. It hands the resolve block to the presenter and returns&lt;/span&gt;
&lt;span class="c1"&gt;// immediately; the block is called later, from the native screen, whenever that screen finishes.&lt;/span&gt;
&lt;span class="c1"&gt;// Until then the party's `await` is genuinely suspended. That is the whole handoff, and it is also&lt;/span&gt;
&lt;span class="c1"&gt;// the whole risk: a path through the native screen that never calls the block leaves the promise&lt;/span&gt;
&lt;span class="c1"&gt;// pending for the life of the process. ---&lt;/span&gt;
&lt;span class="k"&gt;@implementation&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;

&lt;span class="n"&gt;RCT_EXPORT_MODULE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;openNative&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;nativeId&lt;/span&gt;
        &lt;span class="nf"&gt;paramsJson&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;paramsJson&lt;/span&gt;
           &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;RCTPromiseResolveBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;resolve&lt;/span&gt;
            &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;RCTPromiseRejectBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;reject&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;QuickBattlePresenter&lt;/span&gt; &lt;span class="nf"&gt;presentWithNativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;nativeId&lt;/span&gt;
                                 &lt;span class="nl"&gt;paramsJson:&lt;/span&gt;&lt;span class="n"&gt;paramsJson&lt;/span&gt;
                                 &lt;span class="nl"&gt;completion:&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_Nonnull&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                   &lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                                 &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The codegen C++ module. This method is the reason the file is ObjC++ rather than Swift: it&lt;/span&gt;
&lt;span class="c1"&gt;// returns a std::shared_ptr, which Swift has no way to express.&lt;/span&gt;
&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;shared_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;facebook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TurboModule&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;getTurboModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;facebook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ObjCTurboModule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InitParams&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;facebook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;react&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NativeShellNavigationModuleSpecJSI&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens to that &lt;code&gt;resolve&lt;/code&gt; block is the post's core. &lt;strong&gt;Every exit of the native screen settles the promise, exactly once&lt;/strong&gt;, and every path lands in a settle-once wrapper. The call arrives off the main thread, so presentation hops before touching UIKit; the host controller comes from &lt;code&gt;hostProvider&lt;/code&gt;, a static variable defaulting to a key-window scan, reassigned only by the unit bundle; a re-entrancy guard settles a refused second presentation; the sheet blocks interactive dismissal; and two safety nets close the exits no button took. One catches a sheet that left the screen outside &lt;code&gt;finish()&lt;/code&gt;. The other catches a presentation UIKit silently refused, and its mechanism assumes nothing: whether the completion of a refused &lt;code&gt;present()&lt;/code&gt; runs is not documented. One main-queue turn later a refused controller still has no &lt;code&gt;presentingViewController&lt;/code&gt;, and none means nobody will ever dismiss the sheet, so the wrapper settles it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every path below funnels into this, and it settles at most once. The battle's own exits&lt;/span&gt;
&lt;span class="c1"&gt;// race the two safety nets further down, and whichever lands first wins; the rest become&lt;/span&gt;
&lt;span class="c1"&gt;// no-ops instead of double settles.&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;isPresenting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// openNative arrives on the TurboModule's own queue, not the main thread. Every UIKit call&lt;/span&gt;
&lt;span class="c1"&gt;// below has to be on main, so hop before touching anything.&lt;/span&gt;
&lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostProvider&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isPresenting&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Nothing to present from, or one flow already up: settle rather than wedge.&lt;/span&gt;
    &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;isPresenting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;// The result settles BEFORE the dismissal starts, not in its completion. viewDidDisappear&lt;/span&gt;
  &lt;span class="c1"&gt;// fires while the sheet is still animating out, so a settle scheduled after the transition&lt;/span&gt;
  &lt;span class="c1"&gt;// would lose the race to safety net one and the winner would arrive as "{}".&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;QuickBattleView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contestants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contestants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;QuickBattleHostingController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;rootView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modalPresentationStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pageSheet&lt;/span&gt;
  &lt;span class="c1"&gt;// Exit only through the screen's own controls. An interactive swipe-to-dismiss would tear&lt;/span&gt;
  &lt;span class="c1"&gt;// the sheet away without reaching the settle above, leaving the promise pending.&lt;/span&gt;
  &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isModalInPresentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="c1"&gt;// Safety net one: the sheet leaving the screen by any route the buttons did not take —&lt;/span&gt;
  &lt;span class="c1"&gt;// an ancestor being torn down is enough. A resultless settle beats a pending promise.&lt;/span&gt;
  &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onDisappear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Safety net two: present() silently does nothing when the host is mid-transition for&lt;/span&gt;
  &lt;span class="c1"&gt;// reasons this file cannot see. Whether UIKit runs the completion of a refused present is&lt;/span&gt;
  &lt;span class="c1"&gt;// not documented, so nothing here depends on it: one main-queue turn later, a presented&lt;/span&gt;
  &lt;span class="c1"&gt;// controller has a presentingViewController and a refused one still has none, and nil&lt;/span&gt;
  &lt;span class="c1"&gt;// means nobody will ever dismiss this sheet, so settle now.&lt;/span&gt;
  &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;presentingViewController&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One order matters: the result settles before the dismissal starts, because &lt;code&gt;viewDidDisappear&lt;/code&gt; fires mid-animation. In the view, both exits, Close and Done, land in one &lt;code&gt;finish()&lt;/code&gt;, and a battle-free exit is a result rather than an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;winnerId&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kt"&gt;JSONSerialization&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withJSONObject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"winnerUid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;winnerId&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flatMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"{}"&lt;/span&gt;
  &lt;span class="nf"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A never-settled promise has no safety net.&lt;/strong&gt; No timeout on the bridge, no garbage-collector rescue, no error to observe: the &lt;code&gt;await&lt;/code&gt; simply never returns, and the resolve block and everything it captures stay retained for the life of the process. The opposite fault costs a log line at worst, since the bridge ignores a second settle. Resolving on every exit is mandatory; resolving twice is survivable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Android: every exit is a result
&lt;/h2&gt;

&lt;p&gt;Android gets most of the guarantee from structure: the screen is an Activity started for a result, the platform reports back every exit of a screen that opened, and the module's listener is the single place a delivered result settles it. What structure does not cover is the launch itself (a start that throws, or a teardown racing the UI-thread hop), so the module closes both by hand. Create &lt;code&gt;apps/host/android/app/src/main/java/com/host/ShellNavigationModule.kt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.host&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.app.Activity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.content.Intent&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.BaseActivityEventListener&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.Promise&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.ReactApplicationContext&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.host.specs.NativeShellNavigationModuleSpec&lt;/span&gt;

&lt;span class="c1"&gt;// --- The host's native half of shell.navigateTo on Android, and the mirror of the iOS&lt;/span&gt;
&lt;span class="c1"&gt;// ShellNavigationModule. Codegen read the same spec file and wrote NativeShellNavigationModuleSpec&lt;/span&gt;
&lt;span class="c1"&gt;// into com.host.specs; this class extends it, so the JavaScript `openNative` lands here.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Android's shape does most of the promise discipline for it. The screen is an Activity started&lt;/span&gt;
&lt;span class="c1"&gt;// for result, so the platform delivers a result for every exit of a screen that opened — the Done&lt;/span&gt;
&lt;span class="c1"&gt;// button, the system back gesture — and onActivityResult is the single place a delivered result&lt;/span&gt;
&lt;span class="c1"&gt;// settles the promise. What structure does not cover is the launch itself: a start that throws,&lt;/span&gt;
&lt;span class="c1"&gt;// or a teardown racing the UI-thread hop. Those two windows are closed by hand below. ---&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ReactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// Held from the moment the Activity is launched until it returns. One at a time, which is the&lt;/span&gt;
  &lt;span class="c1"&gt;// counterpart of the iOS presenter's isPresenting flag. Both fields are touched from the&lt;/span&gt;
  &lt;span class="c1"&gt;// native-modules thread (openNative, invalidate) and the UI thread (the launch block, the&lt;/span&gt;
  &lt;span class="c1"&gt;// result listener), so every mutation sits inside synchronized(this).&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;pendingPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

  &lt;span class="c1"&gt;// Flipped once in invalidate and never back: a launch queued before teardown must not open a&lt;/span&gt;
  &lt;span class="c1"&gt;// screen after it, because nobody is listening and its result could land on a later battle.&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;invalidated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activityEventListener&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="err"&gt;: &lt;/span&gt;&lt;span class="nc"&gt;BaseActivityEventListener&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;requestCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCode&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;QUICK_BATTLE_REQUEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="c1"&gt;// Every exit lands here. A battle that finished carries its JSON; a back gesture carries&lt;/span&gt;
        &lt;span class="c1"&gt;// no data at all, and an empty object is the honest answer for it — the party reads a&lt;/span&gt;
        &lt;span class="c1"&gt;// missing winnerUid as "nothing happened".&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getStringExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QuickBattleActivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_RESULT_JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;"{}"&lt;/span&gt;
        &lt;span class="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="nd"&gt;@ShellNavigationModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addActivityEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activityEventListener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;openNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nativeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paramsJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentActivity&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Nothing to launch from: settle now rather than leave the caller waiting on a result&lt;/span&gt;
      &lt;span class="c1"&gt;// that will never be delivered.&lt;/span&gt;
      &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;invalidated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// A battle already running, or the module already torn down: same answer.&lt;/span&gt;
        &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// nativeId goes unread: one native flow exists today, and a second registry row would need a&lt;/span&gt;
    &lt;span class="c1"&gt;// switch here first. openNative arrives on a background queue; hop to the UI thread to start&lt;/span&gt;
    &lt;span class="c1"&gt;// the Activity rather than assume the queue is safe to start it from.&lt;/span&gt;
    &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runOnUiThread&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// invalidate can run between the queueing above and this block: the promise is settled&lt;/span&gt;
        &lt;span class="c1"&gt;// by then, and launching anyway would open a screen nobody is listening to.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invalidated&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@runOnUiThread&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;QuickBattleActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QuickBattleActivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_PARAMS_JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paramsJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startActivityForResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;QUICK_BATTLE_REQUEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// A launch that throws leaves no Activity to deliver a result. Settle now, or the&lt;/span&gt;
          &lt;span class="c1"&gt;// await outlives a screen that never opened.&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// A dev reload or host teardown while a battle is open would strand the caller's await:&lt;/span&gt;
    &lt;span class="c1"&gt;// the listener is about to go away, so settle the pending promise the way a resultless&lt;/span&gt;
    &lt;span class="c1"&gt;// exit does before it can no longer be settled at all, and refuse any launch still queued.&lt;/span&gt;
    &lt;span class="nf"&gt;synchronized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;invalidated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;pendingPromise&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;pendingPromise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;reactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeActivityEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activityEventListener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;companion&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;QUICK_BATTLE_REQUEST&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xB47&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS registered itself with one macro; Android writes the registration down, in a package class the template leaves a slot for. The &lt;code&gt;com.host.specs&lt;/code&gt; base class appears at the first Gradle build, when Android's codegen runs, so an editor complaining before then is early rather than wrong. Create &lt;code&gt;apps/host/android/app/src/main/java/com/host/HostNativePackage.kt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.host&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.BaseReactPackage&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.NativeModule&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.ReactApplicationContext&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.module.model.ReactModuleInfo&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.module.model.ReactModuleInfoProvider&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.host.specs.NativeShellNavigationModuleSpec&lt;/span&gt;

&lt;span class="c1"&gt;// --- Registration. iOS gets this for free from RCT_EXPORT_MODULE and autolinking; Android wants&lt;/span&gt;
&lt;span class="c1"&gt;// it written down, in two halves. getModule builds the instance when the registry asks for it by&lt;/span&gt;
&lt;span class="c1"&gt;// name, and getReactModuleInfoProvider declares what that name is and how it behaves — the last&lt;/span&gt;
&lt;span class="c1"&gt;// flag is the one that matters here, because it is what marks the module as a TurboModule rather&lt;/span&gt;
&lt;span class="c1"&gt;// than a legacy bridge module.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The package is added to MainApplication's list, in the slot the React Native template leaves&lt;/span&gt;
&lt;span class="c1"&gt;// commented for exactly this. ---&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HostNativePackage&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseReactPackage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ReactApplicationContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;NativeModule&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NAME&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getReactModuleInfoProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReactModuleInfoProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NAME&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;
        &lt;span class="nc"&gt;ReactModuleInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nc"&gt;NativeShellNavigationModuleSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nc"&gt;ShellNavigationModule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// canOverrideExistingModule&lt;/span&gt;
          &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// needsEagerInit&lt;/span&gt;
          &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// isCxxModule&lt;/span&gt;
          &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// isTurboModule&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;MainApplication.kt&lt;/code&gt;, the template's commented slot finally gets used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;PackageList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The host's own native modules. Autolinking finds packages in node_modules; a module&lt;/span&gt;
  &lt;span class="c1"&gt;// that lives in the app itself is added here, in the slot the template leaves for it.&lt;/span&gt;
  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HostNativePackage&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fetched screen is the repo's first Jetpack Compose, so the build declares the compiler and the libraries. In &lt;code&gt;apps/host/android/build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;classpath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.jetbrains.kotlin:kotlin-gradle-plugin"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Kotlin 2.x ships the Compose compiler as a separate plugin; the native Quick Battle&lt;/span&gt;
&lt;span class="c1"&gt;// screen is the only thing in the app that needs it.&lt;/span&gt;
&lt;span class="n"&gt;classpath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;apps/host/android/app/build.gradle&lt;/code&gt;, the plugin, the build feature, and the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s2"&gt;"org.jetbrains.kotlin.android"&lt;/span&gt;
&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="nl"&gt;plugin:&lt;/span&gt; &lt;span class="s2"&gt;"org.jetbrains.kotlin.plugin.compose"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Jetpack Compose, for the native Quick Battle screen (the SwiftUI counterpart on iOS).&lt;/span&gt;
&lt;span class="n"&gt;buildFeatures&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- Jetpack Compose, for QuickBattleActivity. The BOM pins the Compose family from one&lt;/span&gt;
&lt;span class="c1"&gt;// version, so those artefacts carry none of their own; activity-compose sits outside the&lt;/span&gt;
&lt;span class="c1"&gt;// BOM and pins itself. ---&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;composeBom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose:compose-bom:2024.09.03"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;composeBom&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose.ui:ui"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose.foundation:foundation"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.compose.material3:material3"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"androidx.activity:activity-compose:1.9.3"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Activity joins &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, unexported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- The native Quick Battle screen. exported=false: nothing outside this app launches it;
     the shell's TurboModule does, with startActivityForResult. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;activity&lt;/span&gt;
  &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".QuickBattleActivity"&lt;/span&gt;
  &lt;span class="na"&gt;android:exported=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;
  &lt;span class="na"&gt;android:theme=&lt;/span&gt;&lt;span class="s"&gt;"@style/AppTheme"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the fetched Activity's exit against the Swift version: Done calls &lt;code&gt;deliverResult&lt;/code&gt;; the system back button calls nothing, and needs nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RESULT_OK with the JSON extra is what ShellNavigationModule's ActivityEventListener reads.&lt;/span&gt;
&lt;span class="c1"&gt;// A system back never reaches this method, and does not need to: the Activity finishes with no&lt;/span&gt;
&lt;span class="c1"&gt;// result, and the listener reads a null Intent as an empty object.&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;deliverResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RESULT_OK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_RESULT_JSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS every exit resolves because the code closed each one; on Android, because the platform reports back every exit of a screen that opened, and the module closes the launch paths by hand. The full set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit&lt;/th&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Done after a battle&lt;/td&gt;
&lt;td&gt;resolves &lt;code&gt;{"winnerUid": …}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RESULT_OK&lt;/code&gt; extra, resolves &lt;code&gt;{"winnerUid": …}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close before battling&lt;/td&gt;
&lt;td&gt;resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RESULT_OK&lt;/code&gt; extra, resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swipe to dismiss&lt;/td&gt;
&lt;td&gt;blocked by &lt;code&gt;isModalInPresentation&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;not applicable, full-screen Activity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System back&lt;/td&gt;
&lt;td&gt;no such gesture on a sheet&lt;/td&gt;
&lt;td&gt;null Intent, resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second &lt;code&gt;openNative&lt;/code&gt; while one is open&lt;/td&gt;
&lt;td&gt;the re-entrancy guard resolves it with &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the pending-promise guard resolves it with &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The launch itself fails&lt;/td&gt;
&lt;td&gt;net two's nil check settles &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;catch&lt;/code&gt; around the start resolves &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell torn down mid-battle&lt;/td&gt;
&lt;td&gt;the disappearance net settles &lt;code&gt;{}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;invalidate()&lt;/code&gt; settles &lt;code&gt;{}&lt;/code&gt; and refuses a queued launch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second-launch row is defence in depth: the party's in-flight flag, coming next, already blocks it from the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the guarantees
&lt;/h2&gt;

&lt;p&gt;Half the table covers moments a simulator run cannot stage: a launch that throws, a teardown racing the hop, a presentation UIKit refuses. The two native suites fetched earlier hold them.&lt;/p&gt;

&lt;p&gt;The Android suite is plain JVM: &lt;code&gt;ShellNavigationModuleTest.kt&lt;/code&gt; drives the real module against mocked edges and walks the promise through eight paths. The mock Promise records its settlements; the mock Activity's &lt;code&gt;runOnUiThread&lt;/code&gt; runs inline, or is captured and fired after &lt;code&gt;invalidate()&lt;/code&gt;, which stages the teardown race. Two &lt;code&gt;apps/host/android/app/build.gradle&lt;/code&gt; edits wire it up. In the &lt;code&gt;android&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// JVM unit tests for ShellNavigationModule run against the SDK's stub android.jar; default&lt;/span&gt;
&lt;span class="c1"&gt;// values instead of "not mocked" throws let a real Intent be constructed as a no-op while&lt;/span&gt;
&lt;span class="c1"&gt;// the mocks around it carry the assertions.&lt;/span&gt;
&lt;span class="n"&gt;testOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;unitTests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;returnDefaultValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;dependencies&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// --- JVM unit tests for the bridge module: the promise-settling contract under launch&lt;/span&gt;
&lt;span class="c1"&gt;// failure, teardown, and the result listener. Mockito 5 mocks finals by default, which is&lt;/span&gt;
&lt;span class="c1"&gt;// what lets Activity and the React context stand in without wrappers. ---&lt;/span&gt;
&lt;span class="n"&gt;testImplementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"junit:junit:4.13.2"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;testImplementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.mockito:mockito-core:5.14.2"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;testImplementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.mockito.kotlin:mockito-kotlin:5.4.0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iOS suite needs one piece of project surgery, because the app is not its host. In Xcode: File → New → Target → Unit Testing Bundle, named &lt;code&gt;HostTests&lt;/code&gt;, with "Target to be Tested" set to None; then select the fetched &lt;code&gt;QuickBattlePresenterTests.swift&lt;/code&gt; and &lt;code&gt;QuickBattle.swift&lt;/code&gt; and tick &lt;code&gt;HostTests&lt;/code&gt; under Target Membership. If the new bundle is not in the Host scheme's Test action (Product → Scheme → Edit Scheme → Test), add it. The bundle compiles the presenter directly instead of loading the app, so its three tests need no React Native boot, dev server, or window hierarchy: no host to present from, a refused second presentation, and a presentation UIKit turns down. That last is the nil-check net observed as a test rather than trusted as a comment. The tests stage each case by reassigning the &lt;code&gt;hostProvider&lt;/code&gt; seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire the button
&lt;/h2&gt;

&lt;p&gt;The party's half starts in its slice; both new pieces stay private, in no contract, since nothing outside the party dispatches them. &lt;code&gt;apps/party/src/partySlice.ts&lt;/code&gt; in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSlice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PayloadAction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- The party's state, owned outright by the party app. The contract carries the action creator&lt;/span&gt;
&lt;span class="c1"&gt;// and the read shape; this file carries what they mean. Nothing outside this app imports it — the&lt;/span&gt;
&lt;span class="c1"&gt;// host loads it as a federated module at boot, for the side effect at the bottom. ---&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partySlice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSlice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;reducers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Private: nobody else dispatches remove, so it ships in no contract.&lt;/span&gt;
    &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PayloadAction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// A removed member cannot go on being the last winner; the banner would name a Pokémon&lt;/span&gt;
      &lt;span class="c1"&gt;// that is no longer in the party.&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Also private, and deliberately so. The winner comes back from a native screen the HOST&lt;/span&gt;
    &lt;span class="c1"&gt;// presented, but it lands in the party's own state and nothing outside this app dispatches&lt;/span&gt;
    &lt;span class="c1"&gt;// it — so it appears in no contract. The handoff's params and result types are at the seam&lt;/span&gt;
    &lt;span class="c1"&gt;// because the host has to serialise them, which is a different reason from crossing.&lt;/span&gt;
    &lt;span class="nf"&gt;setLastBattleWinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PayloadAction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;extraReducers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The crossing interaction. The list app dispatches the contract's addToParty, and this case&lt;/span&gt;
    &lt;span class="c1"&gt;// matches it on the type string: addCase reads actionCreator.type and keys the reducer by&lt;/span&gt;
    &lt;span class="c1"&gt;// `party/add`, so agreement on that string is what makes the match work, not the identity of&lt;/span&gt;
    &lt;span class="c1"&gt;// the creator object. Sharing @pokedex/contracts as a singleton is what stops the two sides&lt;/span&gt;
    &lt;span class="c1"&gt;// from retyping that string, the cap and the read shape separately.&lt;/span&gt;
    &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the cap lives with the owner&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastBattleWinner&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;partySlice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Importing this module is what adds the reducer to the shared store. rootReducer is the same&lt;/span&gt;
&lt;span class="c1"&gt;// object the host's configureStore wired in — that is why injection from a separately-built app&lt;/span&gt;
&lt;span class="c1"&gt;// works at all.&lt;/span&gt;
&lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partySlice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of the party's own tests assert the slice's exact shape, so they grow the same field. In &lt;code&gt;__tests__/partyStateReady.test.ts&lt;/code&gt; and &lt;code&gt;__tests__/partySelfRecovery.test.tsx&lt;/code&gt;, the &lt;code&gt;toEqual({ members: [] })&lt;/code&gt; assertion becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PartyScreen.tsx&lt;/code&gt; grows in four places. First the imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useColorScheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nativewind&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;partyStateReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;shellNavigate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;QuickBattleResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ButtonText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EmptySlot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PokemonCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ScreenContainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toast&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastBattleWinner&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At module level, the owner's local read shape and the battle's floor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The owner's own view of its slice. The contract carries `members`, because foreign modules read&lt;/span&gt;
&lt;span class="c1"&gt;// it; the last winner is read here and nowhere else, so it stays out of the contract and is added&lt;/span&gt;
&lt;span class="c1"&gt;// to the shape locally instead.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartyOwnShape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Two contestants is the floor for a battle. Stated as a constant because the button's disabled&lt;/span&gt;
&lt;span class="c1"&gt;// state and the hint under it are two readings of the same rule.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the component, the selectors and the handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;colorScheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useColorScheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartyOwnShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;EMPTY_MEMBERS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartyOwnShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastWinner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lastBattleWinnerUid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The one call a remote makes to reach native. `shellNavigate` is the whole of what this app&lt;/span&gt;
&lt;span class="c1"&gt;// knows about the other side: no TurboModule import, no native types, no idea that the screen&lt;/span&gt;
&lt;span class="c1"&gt;// it opens is SwiftUI on one platform and Compose on the other. The host resolves the&lt;/span&gt;
&lt;span class="c1"&gt;// destination and owns everything past it.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// The await is the point. It does not return until the native flow has finished, and what comes&lt;/span&gt;
&lt;span class="c1"&gt;// back lands in this app's own state through this app's own action — the round trip starts and&lt;/span&gt;
&lt;span class="c1"&gt;// ends inside the party, so nothing about the battle needs to cross the contract.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;battleInFlight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setBattleInFlight&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onQuickBattle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;battleInFlight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setBattleInFlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The theme is read at the moment of the call, from the same NativeWind observable every&lt;/span&gt;
    &lt;span class="c1"&gt;// dark: class in the federation subscribes to. It is sent rather than observed because the&lt;/span&gt;
    &lt;span class="c1"&gt;// native screen has no way to subscribe: a toggle while the battle is open will not reach it.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;shellNavigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QuickBattle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;colourScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colorScheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;QuickBattleResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// No winner is a real outcome, not a failure: the screen can be closed without battling,&lt;/span&gt;
    &lt;span class="c1"&gt;// and the native side resolves with an empty object when it is.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;winnerUid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;setLastBattleWinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;winnerUid&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setBattleInFlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;battleInFlight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The render gains the button, its hint, and the banner, after the slot grid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The handoff's one control. Disabled below two members, because a battle needs two
    contestants; the hint under it says which rule it is rather than leaving a dead
    button to explain itself. */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;
  &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onQuickBattle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;battleInFlight&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lg"&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`mt-6 rounded-xl &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
    &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-lightGrey dark:bg-white/10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-purple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// The 44pt minimum is declared, not inherited from the size variant, the same way the&lt;/span&gt;
  &lt;span class="c1"&gt;// detail's Add button declares it: the accessibility suite can only check what the&lt;/span&gt;
  &lt;span class="c1"&gt;// control states about itself.&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;alignSelf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stretch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ButtonText&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-midGrey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;battleInFlight&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Battling…&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Quick Battle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ButtonText&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTESTANTS&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"xs"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-2 text-center text-darkGrey dark:text-lightGrey"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Add at least 2 Pokémon to battle.
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* What came back from native, rendered by the app that owns the state. */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastWinner&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sm"&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-3 text-center font-semi text-darkGrey dark:text-lightGrey"&lt;/span&gt;
    &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Last battle winner: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastWinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One change is easy to miss, and it cost this build a debugging session. The filled slots sit in a module-level &lt;code&gt;React.memo&lt;/code&gt;, and a theme change alters none of their props, so the memo skips the subtree and the card keeps the old scheme's style objects: an empty white rectangle. The slot's key carries the scheme, so a toggle becomes a remount of six small cards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The key carries the colour scheme, and it has to. A theme change alters none of&lt;/span&gt;
&lt;span class="c1"&gt;// this slot's props, so the memo above finds them identical and skips the subtree;&lt;/span&gt;
&lt;span class="c1"&gt;// the card then keeps the style objects the styling runtime resolved for the old&lt;/span&gt;
&lt;span class="c1"&gt;// scheme and paints as an empty white rectangle. Keying by scheme makes a toggle a&lt;/span&gt;
&lt;span class="c1"&gt;// remount of six small cards, while the memo goes on doing its job within a theme.&lt;/span&gt;
&lt;span class="c1"&gt;// The Pokédex grid does not memoise its cards, which is why it never showed this.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PartySlot&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;member&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onOpen&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;openDetail&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onRemoveMember&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;removeMember&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Native code changed on both platforms, so both need real builds, as &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;post 4's screens&lt;/a&gt; and post 11's animation pods did. This time the compiled code is the series' own. Three dev servers, each in its own terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Restart the host's dev server with &lt;code&gt;--reset-cache&lt;/code&gt; if it was already running.&lt;/strong&gt; A server started before contracts 3.3.0 was installed keeps serving the old resolution, and boot dies with &lt;code&gt;TypeError: undefined is not a function&lt;/code&gt; at &lt;code&gt;App.tsx&lt;/code&gt;: a cached contracts build with no &lt;code&gt;registerShellNavigateHandler&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the builds, and the suites with them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run android &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;apps/host apps/party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx jest &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the native suites, swapping the simulator name for one you have installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host/android &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./gradlew :app:testDebugUnitTest &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host/ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; xcodebuild &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-workspace&lt;/span&gt; Host.xcworkspace &lt;span class="nt"&gt;-scheme&lt;/span&gt; Host &lt;span class="nt"&gt;-only-testing&lt;/span&gt;:HostTests &lt;span class="nt"&gt;-destination&lt;/span&gt; &lt;span class="s1"&gt;'platform=iOS Simulator,name=iPhone 17 Pro'&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add two Pokémon, open the Party tab, press Quick Battle. The native sheet rises wearing the grid's own cards, badge aside; Battle rings the winner in its type colour; Done dismisses; the banner names the winner the promise carried back. Flip the theme first and the native screen follows, from the scheme the call sent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/native-handoff-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The badge is the only giveaway. That is the design system holding across a boundary no bundle crosses:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqfk0la1phexhw1wz72q.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqfk0la1phexhw1wz72q.webp" alt="The native Quick Battle sheet in dark mode: two party members drawn in the design system's card language, the winner ringed and haloed in its type colour, under a purple NATIVE iOS badge" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;With the round trip working, the bridge's dangerous property deserves one deliberate viewing. The presenter funnels every exit into its settle-once wrapper, so the honest sabotage is the one line every path trusts. In the fetched &lt;code&gt;QuickBattle.swift&lt;/code&gt;, find the wrapper and comment out its last call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resultJson&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;isPresenting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="c1"&gt;// completion(resultJson)   &amp;lt;- break-it: the resolve never happens&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild, open Quick Battle, battle, tap Done. The sheet dismisses normally, and back on the Party tab the button reads "Battling…" for the life of the process: the &lt;code&gt;await&lt;/code&gt; never returns, so the &lt;code&gt;finally&lt;/code&gt; that clears &lt;code&gt;battleInFlight&lt;/code&gt; never runs. A second tap does nothing, blocked by the in-flight guard. No red box, no log line, no rejected promise. Neither safety net helped: net one fired its &lt;code&gt;settle("{}")&lt;/code&gt; straight into the flipped &lt;code&gt;settled&lt;/code&gt; guard, and net two had nothing to catch, since the presentation succeeded. The nets guard the paths into the wrapper, and nothing guards the wrapper's own last line. That is worth a moment: defence in depth ends somewhere, and wherever it ends is the line a review has to read hardest.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nothing crashed, which is the problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The series has a small taxonomy of quiet failures, and this is the quietest. &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;Post 8's lost add&lt;/a&gt; at least dispatched into a store and showed in devtools; this one leaves no trace, because nothing happened. Android cannot reproduce the same fault: empty out &lt;code&gt;deliverResult&lt;/code&gt;, and the system back gesture still lands in &lt;code&gt;onActivityResult&lt;/code&gt; with a null Intent and resolves &lt;code&gt;{}&lt;/code&gt;. That is the exit table's asymmetry, observed. Restore the line and rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;A federated remote asked for a native screen and got one, without learning anything about native. The contract exposes one promise-returning function over a one-entry routing table; the host implements it with a TurboModule carrying JSON both ways, presenting SwiftUI on one platform and a Compose Activity on the other, both in the design system's tokens and theme. The winner's uid returns on the promise and lands through a private action, crossing no contract action, as &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-native-handoff" rel="noopener noreferrer"&gt;post 7's ownership rules&lt;/a&gt; predict.&lt;/p&gt;

&lt;p&gt;The honest limits: the bridge is one-way, so a native screen cannot yet ask the shell to navigate anywhere, and deep links do not route through the table; both are deliberate cuts and the bridge's next additions. The theme travels in the params, so a mid-battle toggle reaches the next battle, not the current one.&lt;/p&gt;

&lt;p&gt;Next, the build gets real: a production bundle, and the first federated release artefact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://v4.re-pack.dev/docs/module-federation" rel="noopener noreferrer"&gt;Re.Pack: Module Federation&lt;/a&gt; — the limitations list, including "Host application must have native modules used in other containers"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactnative.dev/docs/turbo-native-modules-introduction" rel="noopener noreferrer"&gt;React Native: Turbo Native Modules introduction&lt;/a&gt; — the spec-first workflow this post follows on both platforms&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/reactwg/react-native-new-architecture/blob/main/docs/turbo-modules.md" rel="noopener noreferrer"&gt;React Native New Architecture working group: Turbo Modules guide&lt;/a&gt; — the worked promise-returning module example&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/reactwg/react-native-new-architecture/blob/main/docs/appendix.md" rel="noopener noreferrer"&gt;React Native New Architecture working group: appendix&lt;/a&gt; — the type mappings, &lt;code&gt;Promise&amp;lt;T&amp;gt;&lt;/code&gt; included&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactnative.dev/docs/legacy/native-modules-android" rel="noopener noreferrer"&gt;React Native: legacy Android native modules&lt;/a&gt; — the deferred-promise pattern around &lt;code&gt;onActivityResult&lt;/code&gt; that the Kotlin module carries into the new architecture&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactnavigation.org/docs/native-stack-navigator/" rel="noopener noreferrer"&gt;React Navigation: native stack&lt;/a&gt; — the right tool for native-backed React Native screens, and the alternative this bridge is not competing with&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.android.com/develop/ui/compose/bom" rel="noopener noreferrer"&gt;Jetpack Compose BOM&lt;/a&gt; — the bill of materials pinning the Compose libraries to one version set&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-13-native-handoff&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Accessibility testing across federated remotes in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 14 Sep 2026 00:05:03 +0000</pubDate>
      <link>https://dev.to/warrendeleon/accessibility-testing-across-federated-remotes-in-react-native-4i6f</link>
      <guid>https://dev.to/warrendeleon/accessibility-testing-across-federated-remotes-in-react-native-4i6f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;Post 11&lt;/a&gt; closed on a promise: accessibility across the seam, a shared testing package that checks touch targets, contrast and focus order in remotes the host only meets at runtime. Two of those three fit in a Jest suite. Focus order does not, and &lt;em&gt;What these checks cannot see&lt;/em&gt; says who it belongs to instead.&lt;/p&gt;

&lt;p&gt;The problem is post 11's problem one level up. Three teams ship three bundles on their own schedules, each with its own idea of what counts as accessible, and those ideas drift exactly the way the greys drifted before a design system existed. Writing the bar down in a wiki page does not stop the drift, because a convention has no version and no install step. A package has both.&lt;/p&gt;

&lt;p&gt;So the bar becomes &lt;code&gt;@pokedex/a11y-testing&lt;/code&gt;: a Jest preset, a NativeWind-aware render, a small set of WCAG (Web Content Accessibility Guidelines) assertion helpers, the A and AA criteria catalogue, and a reporter. It publishes to the same local Verdaccio registry as every other package at the seam, and both source packages and both remotes install it. &lt;strong&gt;It is also the first &lt;code&gt;@pokedex&lt;/code&gt; package that never reaches a bundle.&lt;/strong&gt; Every consumer takes it as a devDependency, so no shared map changes and no runtime moves.&lt;/p&gt;

&lt;p&gt;The criteria being checked are not this project's invention. The European Accessibility Act (Directive (EU) 2019/882) has applied since 28 June 2025 to the products and services it names. The Act itself sets functional requirements and names no standard; EN 301 549, which applies WCAG 2.1's A and AA criteria to mobile apps, is the standard that European practice measures against, and its revision to WCAG 2.2 is expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not jest-axe
&lt;/h2&gt;

&lt;p&gt;On the web this is a solved problem with a package attached. &lt;code&gt;jest-axe&lt;/code&gt; wraps axe-core, &lt;code&gt;expect(await axe(container)).toHaveNoViolations()&lt;/code&gt; covers a wide slice of the rules in one line, and for a React web app it is the right first move.&lt;/p&gt;

&lt;p&gt;axe-core tests HTML. &lt;code&gt;jest-axe&lt;/code&gt; needs a jsdom environment, and its own README says colour contrast checks do not work in jsdom, so they are switched off even there. &lt;code&gt;@axe-core/react-native&lt;/code&gt; is not a package at all: the npm registry answers 404. Deque, who maintain axe-core, do sell React Native tooling, including device SDKs that a native test run can call. That is the device-audit layer &lt;em&gt;What these checks cannot see&lt;/em&gt; names, not the Jest one this post builds.&lt;/p&gt;

&lt;p&gt;React Native Testing Library (RNTL) supplies the queries and the matchers and stops short of an audit. Nothing in its API returns a list of violations. Searching for prior art on accessibility testing across Module Federation remotes turned up nothing, in React Native or on the web. Two community packages come up when you search for React Native accessibility assertions, and neither is the shape this needs. &lt;code&gt;react-native-accessibility-engine&lt;/code&gt; last published in 2022. &lt;code&gt;react-native-ama&lt;/code&gt; is alive, though you have to know it moved: the unscoped package stopped at 0.7.5 in 2023 and the work moved to a scope. Seven of the eight &lt;code&gt;@react-native-ama/*&lt;/code&gt; packages shipped 1.2.1 in August 2025, and five of the eight shipped a 2.0 beta in July 2026. It is a component library with dev-time runtime checks rather than a Jest assertion layer, so it answers a different question from the one in this post.&lt;/p&gt;

&lt;p&gt;The bar gets assembled instead: a render that resolves classes, a handful of assertions, and a report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/accessibility-testing-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing#why-not-jest-axe" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four arrows, two jobs. Two go to the packages that own the shared pixels, where a check runs once for everybody. Two go to the teams, where each suite covers the screens only that team composes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The package
&lt;/h2&gt;

&lt;p&gt;Start from post 11's tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-11-design-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things have to be true before anything here runs, and all three are easy to assume you&lt;br&gt;
already have. The local Verdaccio registry needs to be up with &lt;code&gt;@pokedex/contracts&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;@pokedex/ui&lt;/code&gt; and &lt;code&gt;@pokedex/detail&lt;/code&gt; published to it, which is post 11's work. npm needs to know&lt;br&gt;
that the &lt;code&gt;@pokedex&lt;/code&gt; scope lives there. And you need to be logged in to that registry, which is&lt;br&gt;
&lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;post 5&lt;/a&gt;'s step rather than post 11's,&lt;br&gt;
because the publishes later in this post are authenticated though the reads are not.&lt;/p&gt;

&lt;p&gt;Start the registry first, because &lt;code&gt;npm adduser&lt;/code&gt; below needs something to log in to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx verdaccio@6.2.0                              &lt;span class="c"&gt;# :4873, leave it running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the other two once, for your user, because &lt;strong&gt;npm reads its project config from the directory&lt;br&gt;
holding &lt;code&gt;package.json&lt;/code&gt;, not from the repository root&lt;/strong&gt;, so the repo's own &lt;code&gt;.npmrc&lt;/code&gt; is invisible to&lt;br&gt;
a command run inside &lt;code&gt;packages/ui&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm config &lt;span class="nb"&gt;set&lt;/span&gt; @pokedex:registry http://localhost:4873/
npm adduser &lt;span class="nt"&gt;--registry&lt;/span&gt; http://localhost:4873/     &lt;span class="c"&gt;# only if you have not already&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you did not build post 11, its &lt;em&gt;A package for the pixels&lt;/em&gt; and &lt;em&gt;The detail finishes dressing&lt;/em&gt;&lt;br&gt;
sections publish the three packages this one installs. &lt;code&gt;apps/host&lt;/code&gt; needs its dependencies; the two remotes get theirs with their packages further down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source is more than a post can usefully retype. Fetch the reference copy once and publish it the way &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;post 5&lt;/a&gt; and &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;post 6&lt;/a&gt; published theirs. The tag is released the day this post goes live, so &lt;code&gt;degit&lt;/code&gt; can reach it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-12-a11y-testing /tmp/pokedex-a11y-ref
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/a11y-testing packages/
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/a11y-testing &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The anatomy follows the packages before it: &lt;code&gt;publishConfig&lt;/code&gt; points at Verdaccio, as it has since &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;post 5&lt;/a&gt;, and &lt;code&gt;react-native-builder-bob&lt;/code&gt; builds an ES-module &lt;code&gt;lib/&lt;/code&gt;, the arrangement &lt;code&gt;@pokedex/ui&lt;/code&gt; introduced in post 11. One deliberate omission. &lt;strong&gt;The package ships no &lt;code&gt;exports&lt;/code&gt; map.&lt;/strong&gt; Consumers reach for the root, &lt;code&gt;/jest-preset&lt;/code&gt; in every Jest config, and &lt;code&gt;/reporter.js&lt;/code&gt; in every &lt;code&gt;test:a11y&lt;/code&gt; script, and a map has to name all three. Omit one and Node answers &lt;code&gt;ERR_PACKAGE_PATH_NOT_EXPORTED&lt;/code&gt;, which for the preset means no test run at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jest-preset.js&lt;/code&gt; extends &lt;code&gt;@react-native/jest-preset&lt;/code&gt; rather than replacing it, which is what lets every suite that already existed keep passing untouched. The two additions that matter here are both about NativeWind. &lt;code&gt;nativewind/babel&lt;/code&gt; joins the transform presets, so &lt;code&gt;className&lt;/code&gt; reaches the styling runtime as style instead of sitting there as an inert prop that makes every colour assertion read &lt;code&gt;undefined&lt;/code&gt;. And &lt;code&gt;react-native-css-interop/dist/test/setupAfterEnv.js&lt;/code&gt; joins &lt;code&gt;setupFilesAfterEnv&lt;/code&gt;, which is where the &lt;code&gt;toHaveStyle&lt;/code&gt; matcher lives. Neither line is what makes the checks in this post work (the contrast matrix reads the preset's colour object directly, and the component checks read class strings), but a shared preset has to serve the suite that does assert on a resolved style, and both are asserted present so a future edit cannot quietly drop them.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Two of the three entry points this package &lt;em&gt;imports&lt;/em&gt; are undocumented.&lt;/strong&gt; &lt;code&gt;nativewind/babel&lt;/code&gt; is fine: it is step 3 of NativeWind's own installation guide. The other two are not written down anywhere. &lt;code&gt;react-native-css-interop/dist/test/setupAfterEnv.js&lt;/code&gt;, which the preset adds to &lt;code&gt;setupFilesAfterEnv&lt;/code&gt;, and &lt;code&gt;nativewind/test&lt;/code&gt;, which the render imports, are real and shipped and power NativeWind's own suite, and nativewind.dev has no testing section at all. So the versions this is proven against are recorded rather than assumed: nativewind 4.2.6 and react-native-css-interop 0.2.6, carried on caret ranges and re-checked whenever NativeWind moves.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;RNTL is held below 14 for a related reason: version 14 rewrote &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;fireEvent&lt;/code&gt; and &lt;code&gt;act&lt;/code&gt; as async, and this render path has not been exercised against it. Never reach for &lt;code&gt;@testing-library/jest-native&lt;/code&gt; either; it is deprecated, and RNTL has shipped its own matchers since 12.4.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createThemedRender&lt;/code&gt; binds a Tailwind preset once and hands back the render every suite in that package uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderWithTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createThemedRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui/tailwind.preset.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getByRole&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderWithTheme&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SomeButton&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Taking the preset as an argument keeps the accessibility package free of a dependency on the design system. It is a tool, not a member of the federation. Binding it to &lt;code&gt;@pokedex/ui&lt;/code&gt;'s own preset is what lets a suite resolve a class the way the app will, which is what the touch-target checks read and what the contrast helpers are computed against.&lt;/p&gt;

&lt;p&gt;The helpers are plain functions that call the global &lt;code&gt;expect&lt;/code&gt;, not &lt;code&gt;expect.extend&lt;/code&gt; matchers. A helper imported by name needs no setup file, behaves identically in every package, and appears in a stack trace as itself. None of them import React Native: a test element is anything with props. Each one is tied to a success criterion (SC), and to the bar that criterion sets.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;The bar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text on a surface&lt;/td&gt;
&lt;td&gt;SC 1.4.3 Contrast (Minimum)&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;4.5:1 normal text, 3:1 large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A control's own boundary&lt;/td&gt;
&lt;td&gt;SC 1.4.11 Non-text Contrast&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;3:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What a screen reader reads out&lt;/td&gt;
&lt;td&gt;SC 4.1.2 Name, Role, Value&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;name, role &lt;strong&gt;and&lt;/strong&gt; state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meaning not carried by colour&lt;/td&gt;
&lt;td&gt;SC 1.4.1 Use of Color&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;the information is in words too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A change announced without moving focus&lt;/td&gt;
&lt;td&gt;SC 4.1.3 Status Messages&lt;/td&gt;
&lt;td&gt;AA&lt;/td&gt;
&lt;td&gt;a role or live region carries it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Touch target&lt;/td&gt;
&lt;td&gt;SC 2.5.5 Target Size (Enhanced)&lt;/td&gt;
&lt;td&gt;AAA&lt;/td&gt;
&lt;td&gt;44 by 44 CSS pixels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The touch-target row is the one to read twice. &lt;strong&gt;44pt is this project's bar, and it is Apple's number rather than WCAG's.&lt;/strong&gt; The current Human Interface Guidelines give iOS and iPadOS a default control size of 44 by 44 points and a minimum of 28 by 28, so 44 is the size Apple asks you to design to, not the smallest it will accept. An older Apple page, still widely quoted, says only "at least 44 points x 44 points", which is where the "Apple's minimum" shorthand comes from. On the WCAG side, SC 2.5.5 sits at Level AAA, and WCAG 2.2's AA criterion, SC 2.5.8 Target Size (Minimum), asks for 24 by 24 CSS pixels with five exceptions. Forty-four clears that comfortably. It does not clear everything: Android's guidance asks for at least 48dp, so a project that ships to both platforms and stops at 44 has taken Apple's recommended size and accepted that Android would prefer more. That is a decision worth making on purpose. Calling any of it "what WCAG AA requires" is the mistake to avoid.&lt;/p&gt;

&lt;p&gt;The contrast helper uses &lt;code&gt;0.04045&lt;/code&gt; in the channel linearisation. The relative-luminance definition carried &lt;code&gt;0.03928&lt;/code&gt; before May 2021 and the older constant is still copied around widely. The spec's own note says the change has no practical effect on the results, and the current constant costs nothing.&lt;/p&gt;

&lt;p&gt;The touch-target helper has one behaviour that decides how much the whole layer is worth: it throws where it cannot see, rather than passing. That applies per axis. A control declaring a height and no width has not been measured on its width, and substituting the bar for the axis nobody stated would report it as accessible at exactly the moment the test could not see it. A &lt;code&gt;hitSlop&lt;/code&gt; is measured too, not merely noted as present, because &lt;code&gt;hitSlop: 0&lt;/code&gt; extends nothing.&lt;/p&gt;

&lt;p&gt;That rule is the difference between a suite and a decoration, and it is the one this package got wrong first: an early version borrowed the bar for an undeclared axis, so a button that stated only its height passed on a width nobody had measured. It has its own regression test now, in the package's own suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the source
&lt;/h2&gt;

&lt;p&gt;Both source packages install the bar and point Jest at it. &lt;code&gt;@tailwindcss/container-queries&lt;/code&gt; is not optional here: the test render fails with &lt;em&gt;Cannot find module&lt;/em&gt; without it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 @types/jest@29.5.14 @types/react-test-renderer@19.1.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/detail &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 @types/jest@29.5.14 @types/react-test-renderer@19.1.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suites, the two Jest configs and the per-suite not-applicable declarations come from the reference copy, and each package gains a &lt;code&gt;test:a11y&lt;/code&gt; script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;pkg &lt;span class="k"&gt;in &lt;/span&gt;ui detail&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/jest.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-ref/packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/a11y-report.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-ref/packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/.gitignore packages/&lt;span class="nv"&gt;$pkg&lt;/span&gt;/
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; packages/ui/src/tokens/__tests__ packages/ui/src/components/__tests__
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/tokens/__tests__/contrast.accessibility.ts packages/ui/src/tokens/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/components/__tests__/components.accessibility.tsx packages/ui/src/components/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/detail/__tests__/detail-view.accessibility.tsx packages/detail/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/detail/__tests__/detail-view.test.tsx packages/detail/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/tsconfig.json packages/ui/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/detail/tsconfig.json &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/detail/tsconfig.build.json packages/detail/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design-system changes come across the same way. These are the token repairs the rest of this&lt;br&gt;
section is about, and they have to be in the tree before the matrix can pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/tailwind.preset.js packages/ui/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/tokens/colours.ts &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/tokens/typeColours.ts packages/ui/src/tokens/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/packages/ui/src/components/type-badge.tsx &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/components/back-pill.tsx &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/components/pokemon-card.tsx &lt;span class="se"&gt;\&lt;/span&gt;
   /tmp/pokedex-a11y-ref/packages/ui/src/components/empty-slot.tsx packages/ui/src/components/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;type-badge.tsx&lt;/code&gt; takes the two-surface decision the matrix is about to prove. The other three are&lt;br&gt;
the same fault on smaller controls: the back pill's dark scrim, which the next section measures&lt;br&gt;
alongside the badge's; the card's remove badge, whose touch target &lt;em&gt;Same bar for every remote&lt;/em&gt;&lt;br&gt;
comes back to; and the two secondary-text lines the card and the empty slot paint, which the&lt;br&gt;
section after that repairs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;pkg &lt;span class="k"&gt;in &lt;/span&gt;packages/ui packages/detail&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$pkg&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s1"&gt;'scripts.test:a11y=jest --testPathPattern accessibility --reporters=default --reporters=@pokedex/a11y-testing/reporter.js'&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/detail&lt;/code&gt; needs one more script change, and skipping it publishes a package that cannot be imported. Its suite lives in &lt;code&gt;__tests__&lt;/code&gt;, so its &lt;code&gt;tsconfig.json&lt;/code&gt; now includes that directory, which gives &lt;code&gt;tsc&lt;/code&gt; two roots and moves the output down to &lt;code&gt;dist/src/&lt;/code&gt;. &lt;code&gt;package.json&lt;/code&gt; still declares &lt;code&gt;dist/index.js&lt;/code&gt;. So set &lt;code&gt;packages/detail&lt;/code&gt;'s build script by hand, in its &lt;code&gt;package.json&lt;/code&gt;, to the config that keeps &lt;code&gt;src&lt;/code&gt; as the only root, clearing &lt;code&gt;dist&lt;/code&gt; first so nothing from the old layout survives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node -e &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;require('fs').rmSync('dist',{recursive:true,force:true})&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; tsc -p tsconfig.build.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;tsc&lt;/code&gt; that exits 0 and writes the files one directory too deep is the same fault as a green check measuring the wrong thing: nothing complains until whichever app installs the package first stops with &lt;em&gt;Cannot find module&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/ui&lt;/code&gt; already had unit tests for its components. It gains an accessibility layer beside them, and the token matrix is the interesting half. Every foreground and background pair the design system promises is checked once, in the package that owns the tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WCAG 1.4.3 Contrast (Minimum) — type colours on the solid fill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TYPE_NAMES&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%s badge text clears AA on its type fill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expectColorContrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;textOnTypeClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bgClassForType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normalText&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hexForClass&lt;/code&gt; is the interesting part, and it is there because the first version of this file did not have it. That version mapped the class to a literal: &lt;code&gt;'text-black': '#000000'&lt;/code&gt;. All eighteen types passed, and the number it printed was true of a colour the app never painted.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The design system defines its own &lt;code&gt;black&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;tailwind.preset.js&lt;/code&gt; sets &lt;code&gt;black: '#2E3138'&lt;/code&gt; for near-black surfaces, which shadows Tailwind's default. So &lt;code&gt;text-black&lt;/code&gt; painted &lt;code&gt;#2E3138&lt;/code&gt;, and on the water fill that is 3.74:1 where the test itself demanded 4.5:1. Two of the eighteen were failing on screen while the matrix reported all eighteen green, because the matrix was measuring a constant instead of the token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The repair is in two parts, and only the second one is durable. The badge foreground moved to its own token, &lt;code&gt;typeInk&lt;/code&gt;, which is the real black the contrast map was computed against all along. And the matrix stopped writing hex at all: it resolves &lt;strong&gt;both sides of every pair&lt;/strong&gt; out of the preset, so a token that is renamed or shadowed fails the suite rather than slipping past it. Both sides matter. An interim version resolved only the foreground and kept reading backgrounds from the token module, which left the same hole open on the other half: darkening a type fill to 1.64:1 still left all eighteen pairs green. Point &lt;code&gt;typeInk&lt;/code&gt; back at &lt;code&gt;#2E3138&lt;/code&gt; in the preset now and six checks go red immediately: the water and psychic badges on their own fills, the rock, ghost and dragon badges on the hero surface, and the check that pins the preset to the token modules.&lt;/p&gt;

&lt;p&gt;All eighteen fills pass now, and the sentence means something it did not mean before. &lt;strong&gt;A pair that passes here passes in every remote that composes it&lt;/strong&gt;, and nobody re-checks a type badge in the Party app.&lt;/p&gt;

&lt;p&gt;"Every remote that composes it" is carrying weight in that sentence, and it is worth being precise about what composing a pair means. A badge on a card sits on the type colour. A badge on the detail hero does not: the hero &lt;em&gt;is&lt;/em&gt; the type colour, so a solid pill would vanish into it, and the hero variant lays a 30% white scrim down instead. That scrim is translucent but it is not transparent, and the foreground chosen for the solid fill is the wrong one for four of the six types dark enough to take white text on it. Rock, ghost, dragon and steel were landing at 3.17:1, 3.14:1, 3.39:1 and 2.71:1 on the surface they were actually drawn on, while the matrix checked a surface they were not.&lt;/p&gt;

&lt;p&gt;So there are two decisions, not one, and the design system now computes both. The matrix composites the scrim exactly as the runtime composites it and checks the badge against that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;composite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-white&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;HERO_SCRIM_ALPHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bgClassForType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nf"&gt;expectColorContrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hexForClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;textOnHeroScrimClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;scrim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normalText&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both faults have the same shape: a check measuring something adjacent to what ships. No green suite reveals it.&lt;/p&gt;

&lt;p&gt;The floating back pill is the third of that shape, one control further out. It lays its own&lt;br&gt;
translucent scrim over whichever hero it lands on, and it was painting &lt;code&gt;bg-black/35&lt;/code&gt;, the&lt;br&gt;
near-black neutral again, at 35%. On the three palest fills the white chevron came out at 2.81:1&lt;br&gt;
on flying, 2.83 on ice and 2.84 on electric, under the 3:1 SC 1.4.11 asks of a control's own&lt;br&gt;
boundary. Real black at the same alpha clears every fill, worst case 3.48.&lt;/p&gt;

&lt;p&gt;It does not borrow &lt;code&gt;typeInk&lt;/code&gt; for that. &lt;code&gt;typeInk&lt;/code&gt; is a foreground: every use of it is a &lt;code&gt;text-&lt;/code&gt;&lt;br&gt;
class, and the experiment above reverts it to prove badge text moves. A scrim painted from the&lt;br&gt;
same token would be dragged along by that experiment, which is how a decision about badge ink&lt;br&gt;
would come to own an unrelated control. The background half of the same fact gets its own name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BACK_PILL_SCRIM_ALPHA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BACK_PILL_SCRIM_CLASS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-scrim/35&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The matrix reads the token and the alpha out of that one class, so the value the pill paints and&lt;br&gt;
the value the map composites cannot drift apart.&lt;/p&gt;

&lt;p&gt;One token did not pass. &lt;code&gt;midGrey&lt;/code&gt; (&lt;code&gt;#9A9AB0&lt;/code&gt;) painted secondary text in four places on three light surfaces: two section headings on the detail sheet (2.60:1), the card's number line inside its grey pill (2.46:1), and the disabled Add button's label on its fill (2.02:1). The matrix recorded each surface separately, through a helper the package exports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;knownFinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled button label on its fill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.02:1, exempt under 1.4.3 Incidental&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expectColorContrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;midGrey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lightGrey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;knownFinding&lt;/code&gt; wraps &lt;code&gt;it.failing&lt;/code&gt;, which Jest reports as passing, so the suite stays green while the finding stays visible and the reporter lists it apart from the violations. The helper is there because the first version wrote that call by hand, with a title starting &lt;code&gt;(known&lt;/code&gt; that the reporter matched on. A marker that is only a string is a marker one typo away from silence: misspell it and the reporter stops seeing a tracked finding, &lt;code&gt;it.failing&lt;/code&gt; goes on reporting green, and a live violation reads as an ordinary pass. Writing the title in one place is what makes the marker impossible to misspell.&lt;/p&gt;

&lt;p&gt;Two more surfaces joined the list once the matrix started resolving what it measures. The empty slot's caption is &lt;code&gt;text-midGrey/70&lt;/code&gt;, not the token: composited over the app background it is 1.88:1. And everything so far was the light theme. Both remotes mount a theme toggle in their header, so every one of those surfaces has a dark counterpart the design system also composes, and &lt;code&gt;text-midGrey&lt;/code&gt; carried no &lt;code&gt;dark:&lt;/code&gt; override at either site. The card's number line sits on &lt;code&gt;bg-white/10&lt;/code&gt; over near-black at 3.44:1, and the empty slot's caption on navy at 3.82:1. Both are &lt;code&gt;xs&lt;/code&gt; text, so both need 4.5:1.&lt;/p&gt;

&lt;p&gt;Five pairs, then, every one of them below the bar and on a screen both apps ship. They stayed parked for five rounds behind a reason that reads well and does not survive arithmetic: there is no single darker value that fixes them. Past roughly &lt;code&gt;#5F5F6D&lt;/code&gt; the token clears AA on the three light surfaces, and the detail sheet is &lt;code&gt;dark:bg-navy&lt;/code&gt;, so a pair sitting at a comfortable 6.48:1 would drop to 2.84:1. All true, and none of it load-bearing, because nothing here needs a single value. The design system is theme-aware. It already shipped &lt;code&gt;dark:text-lightGrey&lt;/code&gt; in eight other places, and secondary text had simply never been given the same treatment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"xs"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-darkGrey dark:text-lightGrey"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/ui&lt;/code&gt; carries two of those four sites, and they arrive with the components copied earlier: the card's number line and the empty slot's caption, which now paints the token at full strength rather than at 70%. The other two are the detail sheet's section headings, so they are a hand edit in &lt;code&gt;packages/detail/src/PokemonDetailView.tsx&lt;/code&gt;, on both of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Both section headings. Secondary text is themed here like every other secondary&lt;/span&gt;
&lt;span class="c1"&gt;// line in the design system; the parked version had no dark override at all.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"xs"&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"uppercase tracking-widest text-darkGrey dark:text-lightGrey"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worst of the six pairs is now 6.94:1. Parking a finding is a real option and this post still keeps one, but "it is a palette decision" turned out to be cover for a change that took one class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the matrix went green, and green was not enough.&lt;/strong&gt; Putting &lt;code&gt;text-midGrey&lt;/code&gt; back on the card afterwards left all one hundred and forty-nine checks passing, because a matrix measures pairs and never sees which class a component paints. That is the same hole the host's tab tints fall through, and it takes the same answer: a check that reads the component's own source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;secondaryClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pokemon-card.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;darkGrey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lightGrey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A render assertion would not have closed it. &lt;code&gt;nativewind/test&lt;/code&gt; compiles only the tree handed to &lt;code&gt;render&lt;/code&gt;, so a class a component picks inside its own render never compiles, and the assertion passes on nothing at all.&lt;/p&gt;

&lt;p&gt;One finding stays tracked, and it is not a criterion failure. SC 1.4.3's Incidental exception exempts "text or images of text that are part of an inactive user interface component", so the disabled Add label has no contrast requirement to fail. It is tracked because this project would rather a disabled control stayed readable, and it sits under a &lt;em&gt;Project bars&lt;/em&gt; heading that says so, rather than under 1.4.3 where it would be the same mis-citation the touch-target framing is careful to avoid.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/detail&lt;/code&gt; gets its own suite next, and the shape matters more there than the assertions: a component library that ships its own accessibility tests. The tests hand &lt;code&gt;PokemonDetailView&lt;/code&gt; its props directly, which is the props seam from &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-a11y-testing" rel="noopener noreferrer"&gt;post 8&lt;/a&gt; again. No store, no query client, no navigator. Both consumers inherit whatever this file proves, and when it finds something, one patch release repairs both of them. One ordering note if you run this suite early: it reads the hero-scrim decision out of &lt;code&gt;@pokedex/ui&lt;/code&gt;, and post 11's lockfile pins a version that predates it. The suite runs, and twenty-four of its thirty checks fail, from four causes. Four throw &lt;code&gt;textOnHeroScrimClass is not a function&lt;/code&gt;, which the design-system release further down repairs. One is the Add button's touch target, and one is the sheet's section headings still on the parked token. The other eighteen are one check per type, all saying the same thing about the hero's dex number, which the next section repairs in a single line. Three of those four causes are findings this post is about, so a red run here is the expected shape rather than a broken clone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A failing test, then the fix
&lt;/h2&gt;

&lt;p&gt;The failure this suite was built to catch did not happen. The expectation was the disabled Add button reporting no &lt;code&gt;accessibilityState&lt;/code&gt;, so a screen reader would meet a button that greys out on screen and says nothing about being unavailable. It reports the state correctly. React Native's &lt;code&gt;Pressable&lt;/code&gt; copies the &lt;code&gt;disabled&lt;/code&gt; prop into &lt;code&gt;accessibilityState&lt;/code&gt; itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;_accessibilityState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;_accessibilityState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;_accessibilityState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test stays anyway, as a regression net rather than a fix. The day someone disables that button by swapping the handler for a no-op and greying the fill by hand, the state goes quiet and this test says so.&lt;/p&gt;

&lt;p&gt;What did fail was the check beside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;● WCAG 2.5.5 Target Size — the Add action › the Add button declares at least 44pt on both axes

  Element has no measurable size and no hitSlop; cannot verify the 44pt touch target.
  Give the control an explicit width/height or a hitSlop, or assert on a parent that
  has one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things sit behind that line, and both reach past this button.&lt;/p&gt;

&lt;p&gt;The button declared no size the suite could read. Its height came from gluestack's &lt;code&gt;size="lg"&lt;/code&gt; variant, which composes &lt;code&gt;px-6 h-11&lt;/code&gt; while the component renders. In the app that class is fine: Tailwind scans the design system's source, finds the literal &lt;code&gt;h-11&lt;/code&gt;, and the built bundle carries the rule. In the test tree it is not, and the reason is broader than this button. &lt;code&gt;nativewind/test&lt;/code&gt; compiles the class strings it finds on the element tree you hand to &lt;code&gt;render&lt;/code&gt;, and feeds those to Tailwind as its content. A class that appears inside a child component is not on that tree, so it never compiles: not one a variant builds, and not a literal one either. The Add button arrived carrying &lt;code&gt;{"alignSelf": "stretch"}&lt;/code&gt; and nothing else, and the helper throws on nothing, which is the only reason this surfaced.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;And &lt;code&gt;h-11&lt;/code&gt; is not 44 anyway.&lt;/strong&gt; Tailwind generates &lt;code&gt;.h-11 { height: 2.75rem }&lt;/code&gt;, and NativeWind's rem on React Native is 14, not the browser's 16. A literal &lt;code&gt;&amp;lt;View className="h-11" /&amp;gt;&lt;/code&gt; renders &lt;code&gt;height: 38.5&lt;/code&gt;, in the test tree and in the app alike. So the size that variant asks for was under Apple's 44 all along, whether or not any suite could see it. Two separate faults, one control: a size the test could not read, and a size that would not have cleared the bar if it had. Touch-target checks read declared style and props for this reason, rather than deriving a number from a class.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix puts the bar where a variant cannot quietly move it. In &lt;code&gt;packages/detail/src/PokemonDetailView.tsx&lt;/code&gt;, on the Add button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The 44pt bar is declared here rather than inherited from the size variant.&lt;/span&gt;
&lt;span class="c1"&gt;// A variant is a visual decision that can change; the minimum tappable size of&lt;/span&gt;
&lt;span class="c1"&gt;// the screen's one primary action is a commitment, and the accessibility suite&lt;/span&gt;
&lt;span class="c1"&gt;// can only verify what the control actually declares. `alignSelf: 'stretch'`&lt;/span&gt;
&lt;span class="c1"&gt;// makes the button far wider than 44, but stretch is a layout instruction, not&lt;/span&gt;
&lt;span class="c1"&gt;// a measurement, so the minimum is stated on both axes.&lt;/span&gt;
&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;alignSelf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stretch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same file carried the opposite fault, a hundred lines up, in the hero the button sits under. The&lt;br&gt;
hero paints the type's colour full strength, and a comment above it explains that its text cannot be a&lt;br&gt;
fixed colour, because the design system already decided per type and the hero should ask the token&lt;br&gt;
rather than assume. The dex number under the name did assume. It muted itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: a second answer, invented one line under the comment saying not to.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heroMuted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heroInk&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-white/70&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-black/60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;text-black&lt;/code&gt; resolves to &lt;code&gt;#2E3138&lt;/code&gt; here, the neutral the preset warns is not a foreground, and at&lt;br&gt;
60% over the fill it measures 2.21:1 on water. Sixteen of the eighteen types fail. No alpha rescues&lt;br&gt;
it: sweep the value from 1% to 100% and the best case is full strength, where water still sits at&lt;br&gt;
3.74:1, because the ink is the wrong ink. So the line asks what the name asks, in&lt;br&gt;
&lt;code&gt;packages/detail/src/PokemonDetailView.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Delete heroMuted. The dex number takes the same decision the name takes.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sm"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`font-head &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;onHero&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dexNumber&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worst case becomes steel at 4.71:1, and the pair is one the token matrix already measures, so&lt;br&gt;
nothing new needs adding to it. A muted variant would have needed its own row.&lt;/p&gt;

&lt;p&gt;The Add button's fault sits one level lower too. The design system's &lt;code&gt;ErrorState&lt;/code&gt; builds its retry from &lt;code&gt;size="md"&lt;/code&gt;, which is &lt;code&gt;h-10&lt;/code&gt;, or 35 points, and that retry is the way back from a failed load in all three apps. Its declaration goes into the design system rather than into whichever app noticed first, in &lt;code&gt;packages/ui/src/components/error-state.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The retry is the way back from a failed load, so its tappable size is declared here&lt;/span&gt;
&lt;span class="c1"&gt;// rather than left to the size variant. A variant is a visual decision; the minimum&lt;/span&gt;
&lt;span class="c1"&gt;// target is a commitment, and a declared one is the only kind a suite can verify.&lt;/span&gt;
&lt;span class="c1"&gt;// Both axes are declared: the button is far wider than 44 in every layout it appears in,&lt;/span&gt;
&lt;span class="c1"&gt;// but a width nobody states is a width nobody has measured.&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;
  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"md"&lt;/span&gt;
  &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onRetry&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more finding sits beside them, in two components at once, and it is the one a sighted review never catches at all. Neither &lt;code&gt;ErrorState&lt;/code&gt; nor &lt;code&gt;LoadingState&lt;/code&gt; had any status semantics: no role, no live region, nothing. They looked correct, and to a screen reader a failed load simply happened in silence.&lt;/p&gt;

&lt;p&gt;They do not get the same repair, because they are not the same kind of message. A failed load should interrupt; a spinner should not. So &lt;code&gt;error-state.tsx&lt;/code&gt; becomes an alert, which announces on both platforms without a live region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 px-6"&lt;/span&gt; &lt;span class="na"&gt;accessible&lt;/span&gt; &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;loading-state.tsx&lt;/code&gt; gets the polite variant, which waits for whatever the screen reader is already saying to finish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1"&lt;/span&gt; &lt;span class="na"&gt;accessible&lt;/span&gt; &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design system carries five of these repairs and &lt;code&gt;packages/detail&lt;/code&gt; three, so each is one release&lt;br&gt;
rather than several:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm version 1.0.13 &lt;span class="nt"&gt;--no-git-tag-version&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/detail &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/ui@1.0.13 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm version 4.0.12 &lt;span class="nt"&gt;--no-git-tag-version&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host takes the release too, and it has a repair of its own, the only fault in this post&lt;br&gt;
that lives in the shell rather than in a package or a remote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/host/App.tsx apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/host/__tests__/TabBar.test.tsx apps/host/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/host/tsconfig.json apps/host/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tab labels are 10pt, and both were failing. The focused one took &lt;code&gt;colours.blue&lt;/code&gt;, the brand&lt;br&gt;
fill: 3.48:1 on the light bar, 3.74:1 on the dark. The unfocused one was never set at all, so&lt;br&gt;
React Navigation derived it by mixing the theme's text halfway into the bar's own colour, which&lt;br&gt;
gives 3.27:1 on white. One tab is always unfocused, so that pair is on screen whenever the&lt;br&gt;
app is. &lt;code&gt;colours.ts&lt;/code&gt; brings two readable blues with it; the greys are the ones every other&lt;br&gt;
secondary line already uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;tabBarActiveTintColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blueTextDark&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blueText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;tabBarInactiveTintColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lightGrey&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;darkGrey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TabBar.test.tsx&lt;/code&gt; is why the guard is worth its own file. The matrix in &lt;code&gt;@pokedex/ui&lt;/code&gt; proves those&lt;br&gt;
four pairs clear AA, and it cannot prove the host uses them: reverting the active tint to&lt;br&gt;
&lt;code&gt;colours.blue&lt;/code&gt; leaves every one of the design system's checks green. A matrix measures pairs; only&lt;br&gt;
the app can be asked whether it composes them. It reads source rather than a render, because a&lt;br&gt;
navigator option is resolved inside React Navigation and reaches no element a suite can query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/ui@1.0.13 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two patch releases, three apps, and no coordination between the teams that ship them.&lt;/strong&gt; The caret range every consumer already carried is what makes this a patch rather than a negotiation. The Pokédex suite records what that means in the place it is easiest to misread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This passes because the design system declares the size on ErrorState's button, not&lt;/span&gt;
&lt;span class="c1"&gt;// because this app did anything. One package release, every remote's retry covered.&lt;/span&gt;
&lt;span class="nf"&gt;expectMinTouchTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buttonContaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Try again&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It renders the same &lt;code&gt;ErrorState&lt;/code&gt; in its remote-load boundary, so it moves to the same version as the two remotes rather than lagging behind them, which for a package the host provides as an eager singleton is not optional.&lt;/p&gt;

&lt;p&gt;How far that retry gets you depends on what failed. A failed data request retries cleanly. A remote that never answered is a harder case: the boundary discards React's cached rejection and starts a fresh import, which is why the button visibly does something, but the same failure comes back every time. The runtime behaves as though it is still holding the failed manifest fetch underneath it. That is an observation rather than a documented guarantee, so treat the behaviour as the finding and not the cause. Clearing it needs cache-busting the runtime, and cache-busting is only worth having once there is something to fall back to, so both arrive together in the resilience post later in this series. What the checks in this post can say is narrower and still worth saying: whatever the retry does, it is reachable, it is named, and it is big enough to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same bar for every remote
&lt;/h2&gt;

&lt;p&gt;Both remotes install the same three devDependencies, and take the two fixed packages in the same command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/detail@4.0.12 @pokedex/ui@1.0.13 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @pokedex/detail@4.0.12 @pokedex/ui@1.0.13 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each app takes its own suite, its Jest config and its not-applicable declarations, and the README catches up with the tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;app &lt;span class="k"&gt;in &lt;/span&gt;list party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/jest.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/a11y-report.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-a11y-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/tsconfig.json apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/
  &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/.gitignore &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/.gitignore
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/list/__tests__/ListStack.accessibility.tsx apps/list/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/party/__tests__/PartyStack.accessibility.tsx apps/party/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/list/src/PokedexScreen.tsx apps/list/src/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/party/src/PartyScreen.tsx apps/party/src/
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/party/__mocks__
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/apps/party/__mocks__/styleMock.js apps/party/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-a11y-ref/README.md &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two screens carry the counter repairs. &lt;code&gt;PokedexScreen.tsx&lt;/code&gt; has the live-region fix below, and&lt;br&gt;
both screens paint the same count pill, whose numeral was &lt;code&gt;text-darkGreen&lt;/code&gt; on &lt;code&gt;bg-lightGreen&lt;/code&gt;:&lt;br&gt;
&lt;strong&gt;1.53:1&lt;/strong&gt;, 10.5pt bold, in each remote's header. &lt;code&gt;darkGreen&lt;/code&gt; is &lt;code&gt;#A6D3A0&lt;/code&gt;, the same value as&lt;br&gt;
the grass fill, so the name was the only dark thing about it. Both now use &lt;code&gt;text-darkGrey&lt;/code&gt;, the&lt;br&gt;
colour the label beside them already used. At 7.18:1, it is a pair the token matrix has held&lt;br&gt;
all along. The counter was composing a different one nobody had measured.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;styleMock.js&lt;/code&gt; is duller and just as necessary, and it is there because &lt;code&gt;PartyStack&lt;/code&gt; pulls in &lt;code&gt;global.css&lt;/code&gt;, one hop away through &lt;code&gt;./styles&lt;/code&gt;, so the remote's&lt;br&gt;
classes reach the shared styling runtime when the host loads the exposed module. The app's own&lt;br&gt;
entry already covers the standalone build; without the import in &lt;code&gt;PartyStack&lt;/code&gt; the styles work&lt;br&gt;
standalone and quietly do nothing federated. Jest has no CSS loader, so the copied config maps&lt;br&gt;
that import to the stub. Without it the Party suite does not fail; it refuses to run.&lt;/p&gt;

&lt;p&gt;Then the script that runs the accessibility files through the reporter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;app &lt;span class="k"&gt;in &lt;/span&gt;apps/list apps/party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s1"&gt;'scripts.test:a11y=jest --testPathPattern accessibility --reporters=default --reporters=@pokedex/a11y-testing/reporter.js'&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both Jest configs point at the shared preset. Because it extends the React Native preset, the suites each app already had keep running, and the rule is to widen its allowlist rather than replace it. In outline, since the copied files carry more entries than this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/a11y-testing/jest-preset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/a11y-testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transformIgnorePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;`node_modules/(?!(&lt;/span&gt;&lt;span class="p"&gt;${[...&lt;/span&gt;&lt;span class="nx"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uncompiledPackages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)/)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replacing that array is how a shared preset quietly stops being shared. It does not fail quietly, though: the styling stack ships ES modules, so Jest hits &lt;code&gt;SyntaxError: Cannot use import statement outside a module&lt;/code&gt; and runs nothing at all. That is the good case. The trap is that the error names a file deep in &lt;code&gt;node_modules&lt;/code&gt; and reads like a broken dependency rather than like a config line the app owns. Exporting the list makes widening the easy move.&lt;/p&gt;

&lt;p&gt;Each suite covers only what its team composes, and the way to keep that honest is to render the team's own screens rather than the design system's components. Both files mount the real stack through the real store and the real navigator, the same harness the apps' existing tests use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderWithTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt; &lt;span class="na"&gt;initialMetrics&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PartyStack&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is more setup than mounting a &lt;code&gt;PokemonCard&lt;/code&gt; directly, and it is the difference between testing this app and testing somebody else's package. A suite that renders design-system components re-checks what the source already settled, which is exactly the duplication the source contract exists to remove.&lt;/p&gt;

&lt;p&gt;So the Pokédex checks the rows its own query data produces, the states its screen can land in, and its party counter. The Party checks its grid: a filled slot is a button naming its Pokémon, the empty ones are named gaps rather than four identical silences, and removal is an accessibility action on the card rather than a second focus stop beside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Pikachu, number 025/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessibilityActions&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remove&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Remove from party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ✕ badge is small (21 points, because &lt;code&gt;h-6&lt;/code&gt; is 1.5rem and NativeWind's rem is 14, so its &lt;code&gt;hitSlop&lt;/code&gt; carries it to 45 rather than the 41 it had), and making it its own focus stop would double the number of things to swipe past on a full party. The design system hides it and exposes removal as an action instead, so a screen-reader user reaches it through VoiceOver's rotor or TalkBack's actions menu. The check is that the action exists at all, because a hidden control with nothing behind it is simply unreachable.&lt;/p&gt;

&lt;p&gt;Rendering the real screen is also what turned the counter's second fault from a note into a fix. The Pokédex header shows "My Party" beside a count out of six, and that count changes when something is added from a screen away, without focus moving. A sighted user watches the number tick; a screen-reader user was told nothing, because the header was static text. That is the whole of SC 4.1.3, and it is now a live region with a label that spells the ratio out, because a bare "3/6" is left to how a given screen reader says it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;
  &lt;span class="na"&gt;accessible&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLiveRegion&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`My Party, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Party's own header needs the same three props, and that is the cost of a fault living in app code twice over: two screens, two suites, and nothing that makes the second one look.&lt;/p&gt;

&lt;p&gt;A test asserting that against a hand-written object would have proved nothing about the app. Against the rendered screen it fails if the props are removed, which is the only version worth keeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The report
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;accessibility-report.md&lt;/code&gt; comes out of each suite, written by the reporter as the run finishes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reporter reads the criterion out of each describe title, so &lt;code&gt;WCAG 1.4.3 …&lt;/code&gt; is all the wiring there is. The design system's run opens like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Accessibility report — ui&lt;/span&gt;

Automated coverage: &lt;span class="gs"&gt;**5 of 13**&lt;/span&gt; WCAG 2.1 A + AA criteria that a Jest suite can decide,
after 2 declared not applicable here.

&lt;span class="gu"&gt;## Violations (0)&lt;/span&gt;

None.

&lt;span class="gu"&gt;## Known findings (0)&lt;/span&gt;

None.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exempt pair does not appear there. It has its own heading, because a threshold a project&lt;br&gt;
chooses is not a result against a criterion, and filing one as the other is how a coverage&lt;br&gt;
number stops meaning anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Project bars&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**not held**&lt;/span&gt; — pairs held above what the criteria require · disabled button label
  on its fill (known: 2.02:1, exempt under 1.4.3 Incidental)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The denominator is where a coverage number is easiest to inflate, so it is built in two steps. The catalogue carries all 50 of WCAG 2.1's A and AA criteria and tags each one with the layer that can decide it: 15 a Jest process can, and the rest belonging to the device audit, the manual pass, or to nothing here at all, like the audio and video criteria in an app with neither. A suite then removes the ones it declares inapplicable, which is why the design system is measured out of 13. Those declarations live in an &lt;code&gt;a11y-report.config.js&lt;/code&gt; at the suite root, and each carries a reason, whether the criterion was in the counted set or not. Nothing enforces that the reason is any good; it is there so a reviewer can disagree with it.&lt;/p&gt;

&lt;p&gt;The touch-target work appears in the reports of the suites that do it, and never in that fraction. The design system's run says so in its own section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Checked beyond the counted scope&lt;/span&gt;

These ran and are reported, but they sit outside the WCAG 2.1 A + AA set the coverage
number is measured against, so they do not raise it.
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**WCAG 2.5.5**&lt;/span&gt; not in the A + AA catalogue — 4 checks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SC 2.5.5 is Level AAA, so counting it would inflate an A and AA figure. Dropping it silently would hide a check that ran. The report does neither.&lt;/p&gt;

&lt;p&gt;That is roughly the artefact a paid accessibility scanner sells, generated from tests the team already owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these checks cannot see
&lt;/h2&gt;

&lt;p&gt;A green suite here proves less than it looks like it proves.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;th&gt;What it cannot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jest suite&lt;/td&gt;
&lt;td&gt;token pairs, declared sizes, the name, role and state a control exposes&lt;/td&gt;
&lt;td&gt;what a device paints, real traversal order, hit regions after clipping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device audit&lt;/td&gt;
&lt;td&gt;contrast as drawn, clipped targets, sizing on a real screen&lt;/td&gt;
&lt;td&gt;whether a label reads well out loud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual pass&lt;/td&gt;
&lt;td&gt;the actual experience, traversal order included&lt;/td&gt;
&lt;td&gt;catching every regression, on every commit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The device audit is &lt;code&gt;performAccessibilityAudit&lt;/code&gt; on iOS and the Accessibility Test Framework on Android; the manual pass is a person with VoiceOver or TalkBack.&lt;/p&gt;

&lt;p&gt;Focus order sits with the manual pass, which is the honest answer to the third thing post 11 promised. A Jest suite can say that focusable elements are present. It cannot say what order a screen reader traverses them in, because that order comes out of layout on a real screen. Nor is the device layer a clean home for it: Android's Accessibility Test Framework has a traversal-order check and iOS's &lt;code&gt;performAccessibilityAudit&lt;/code&gt; has no equivalent, and a criterion only one platform can automate is not one a report should claim. So it stays with the person driving the screen reader, which is where the catalogue tags it.&lt;/p&gt;

&lt;p&gt;The layers are not a ladder where the top one contains the ones below it. The token matrix checks the pairs the design system composes, on the surfaces it composes them on, whether or not a screen shows a given pair today; a device audit checks what is on the screen it was pointed at, which is a different set and never the whole palette. &lt;strong&gt;A clean automated run is necessary and not sufficient&lt;/strong&gt;, and the same is true of a clean device pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Every suite, including the ones that predate this post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/a11y-testing &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/detail &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line is the bar testing itself. A package that tells four workspaces what accessible&lt;br&gt;
means should be able to prove its own helpers, and the cases in there are the ones earlier&lt;br&gt;
versions got wrong.&lt;/p&gt;

&lt;p&gt;Then the accessibility layer on its own, with the report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/detail &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run &lt;span class="nb"&gt;test&lt;/span&gt;:a11y &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No simulator and no dev servers. Every package was published as it was built, so nothing here republishes anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;One package now holds the accessibility bar for the whole federation, and five suites run against it: the bar's own helpers, the tokens and components in &lt;code&gt;@pokedex/ui&lt;/code&gt;, the shared screen in &lt;code&gt;@pokedex/detail&lt;/code&gt;, and each remote's own screens.&lt;/p&gt;

&lt;p&gt;What came out of it divides by where the fault lived, and that division is the point. &lt;code&gt;@pokedex/ui&lt;/code&gt; held eleven: two badge foregrounds below AA on their own fill, four more below it on the hero scrim, the back pill's glyph below the non-text bar on three of the eighteen heroes, two secondary-text lines failing in both themes, and two components that announced a failed load in silence. Add the retry button's touch target and the card badge's, and it is thirteen. All three apps took every one of them by a version bump, which is the whole argument for a design system stated as a number.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/detail&lt;/code&gt; held three, and the host does not install it, so all three stopped at the two remotes: the Add button's target, the hero's dex number, and the sheet's two section headings.&lt;/p&gt;

&lt;p&gt;Four could not travel at all, and they are the interesting ones. Two are the host's own tab labels, in the shell that owns no screens. The other two are the party counter: app code, the same header written twice, once in each remote, owned by no package. It carried one fault of each kind, a count that announced nothing and a numeral at 1.53:1 on its own pill. Each of those four had to be fixed where it sat, and two of them had to be fixed twice.&lt;/p&gt;

&lt;p&gt;The faults that mattered more were in the checks. This post showed four: a helper that borrowed the bar for an axis nobody had declared, a matrix reading a constant instead of the token, the same matrix reading the wrong surface, and a marker that was only a string. More turned up afterwards, in the design system's suites and in the bar's own. A scrim guard in the design system compared two constants exported from the same module. In a clamp test the value never reached the ceiling it was clamping to. The bar's own use-of-colour helper read screen-reader metadata rather than the text on screen. And a target guard fell back to a number written in the test when its own pattern stopped matching. Every one of them now has a case that fails when the repair is taken out again, which is the only evidence a guard is real. &lt;strong&gt;A check aimed slightly beside the thing it names reports green, and goes on reporting green.&lt;/strong&gt; Nothing in the output distinguishes it from a check that works.&lt;/p&gt;

&lt;p&gt;Nothing stays open against a criterion. Five pairs did for five rounds, all of them the same secondary-text token, parked on the reasoning that no single darker value clears every surface. That was true and it was answering the wrong question: the design system is theme-aware, and the pairs took a themed class rather than a new token. One finding stays tracked and it is not a criterion result, because SC 1.4.3 exempts an inactive control's label. The report prints it under &lt;em&gt;Project bars&lt;/em&gt;, counted nowhere.&lt;/p&gt;

&lt;p&gt;The honest limit is the layer. This post builds the Jest layer and neither of the other two. The device audit harness is real work that has not been done here, and the manual VoiceOver and TalkBack pass is not automatable by design. This layer exists for the regression: the things you have already fixed staying fixed, on every commit, in every remote, against one definition.&lt;/p&gt;

&lt;p&gt;Next: the host lends the remotes its native side. A navigation bridge launches a native screen from the Party tab and waits for its result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html" rel="noopener noreferrer"&gt;WCAG 2.2, SC 2.5.5 Target Size (Enhanced)&lt;/a&gt; — Level AAA, 44 by 44 CSS pixels&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html" rel="noopener noreferrer"&gt;WCAG 2.2, SC 2.5.8 Target Size (Minimum)&lt;/a&gt; — the AA criterion, 24 by 24 with five exceptions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;WCAG 2.2, contrast (minimum)&lt;/a&gt; — 4.5:1 for normal text, 3:1 for large&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/TR/WCAG22/#dfn-relative-luminance" rel="noopener noreferrer"&gt;WCAG 2.2, relative luminance&lt;/a&gt; — the formula, and the note recording the May 2021 change from 0.03928 to 0.04045&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/design/human-interface-guidelines/accessibility" rel="noopener noreferrer"&gt;Apple Human Interface Guidelines: Accessibility&lt;/a&gt; — the current control-size table: 44 by 44 points as the iOS and iPadOS default, 28 by 28 as the minimum&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/design/tips/" rel="noopener noreferrer"&gt;Apple: UI Design Dos and Don'ts&lt;/a&gt; — the older page, "at least 44 points x 44 points"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://support.google.com/accessibility/android/answer/7101858" rel="noopener noreferrer"&gt;Android accessibility help: touch target size&lt;/a&gt; — the 48dp guidance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/nickcolley/jest-axe" rel="noopener noreferrer"&gt;jest-axe&lt;/a&gt; — the jsdom requirement and the disabled contrast checks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882" rel="noopener noreferrer"&gt;Directive (EU) 2019/882&lt;/a&gt; — the European Accessibility Act, applied from 28 June 2025&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf" rel="noopener noreferrer"&gt;EN 301 549&lt;/a&gt; — the European standard that applies WCAG 2.1 to mobile apps&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://oss.callstack.com/react-native-testing-library/" rel="noopener noreferrer"&gt;React Native Testing Library&lt;/a&gt; — the queries and matchers this builds on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/nativewind" rel="noopener noreferrer"&gt;nativewind&lt;/a&gt; — &lt;code&gt;nativewind/test&lt;/code&gt;, shipped and undocumented&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/react-native-css-interop" rel="noopener noreferrer"&gt;react-native-css-interop&lt;/a&gt; — the &lt;code&gt;toHaveStyle&lt;/code&gt; matcher, and the rem this stack resolves against&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-12-a11y-testing&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>The design system as a federated singleton in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Tue, 08 Sep 2026 00:04:58 +0000</pubDate>
      <link>https://dev.to/warrendeleon/the-design-system-as-a-federated-singleton-in-react-native-163o</link>
      <guid>https://dev.to/warrendeleon/the-design-system-as-a-federated-singleton-in-react-native-163o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/rtk-query-vs-apollo-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;Post 10&lt;/a&gt; closed on a promise: the design system becomes a federated singleton, one UI package shared at runtime, so every remote renders the same components without shipping its own copy. This post keeps it.&lt;/p&gt;

&lt;p&gt;The debt is real. Since &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;post 4&lt;/a&gt; every screen has styled itself with &lt;code&gt;StyleSheet.create&lt;/code&gt; and hard-coded hexes, and the duplication now spans three apps and the view package: the same template greys sit in the list, the party and the detail view, and the same blue accent appears in the host's Refresh button and the detail's Add button. Four codebases, four copies of the same design decisions, and no way to change any of them in one place.&lt;/p&gt;

&lt;p&gt;The fix applies two earlier patterns to pixels. &lt;a href="https://warrendeleon.com/blog/shared-singleton-contract-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;Post 3&lt;/a&gt; shared libraries as singletons so one copy serves every app. Posts &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;5&lt;/a&gt; and &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;6&lt;/a&gt; put packages at the seam so every side installs one definition.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/ui&lt;/code&gt; does both. It packages gluestack-ui's copy-in components, styles them with NativeWind 4.2.6 and publishes them through the local Verdaccio registry. Module Federation then gives every remote the host's copy at runtime. This build uses &lt;code&gt;@gluestack-ui/core&lt;/code&gt; on &lt;code&gt;^3.0.25&lt;/code&gt;, &lt;code&gt;@gluestack-ui/utils&lt;/code&gt; on &lt;code&gt;^3.0.21&lt;/code&gt; and Tailwind 3.4.19.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The seam now holds three packages, and they cross it differently. The contracts and the design system resolve to one copy at runtime, drawn as the host providing them. The detail package deliberately does not: each consumer keeps its own version pin, which has been its point since post 5. That contrast is the heart of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  A package for the pixels
&lt;/h2&gt;

&lt;p&gt;Start from post 10's tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-10-two-backends
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One honest boundary before the anatomy. The design system's source is more than a post can carry: thirteen copied gluestack components, token maps for eighteen types, eleven composed components. So this post's packages are pulled from the finished tag rather than typed in, and that includes the new versions of contracts and detail, because this post grows both (&lt;em&gt;The detail finishes dressing&lt;/em&gt; says how). Fetch the reference copy once (&lt;em&gt;Run it&lt;/em&gt; reuses it for the native chrome) and take all three packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-11-design-system /tmp/pokedex-ref
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; packages/ui packages/contracts packages/detail
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/packages/ui /tmp/pokedex-ref/packages/contracts /tmp/pokedex-ref/packages/detail packages/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/ui&lt;/code&gt; follows the anatomy posts 5 and 6 established: &lt;code&gt;react-native-builder-bob&lt;/code&gt; builds an ESM &lt;code&gt;lib/&lt;/code&gt;, &lt;code&gt;"react-native": "./src/index.ts"&lt;/code&gt; ships raw source so Re.Pack compiles the same files the repo holds, &lt;code&gt;publishConfig&lt;/code&gt; points at the local Verdaccio, and the &lt;code&gt;files&lt;/code&gt; list adds two entries the earlier packages never needed: &lt;code&gt;tailwind.preset.js&lt;/code&gt; and &lt;code&gt;global.css&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The gluestack CLI copies component source into &lt;code&gt;src/components/ui/&lt;/code&gt;. &lt;strong&gt;Copy-in means you own the source&lt;/strong&gt;: your package publishes that code, and upstream updates do not arrive automatically. The companion keeps the thirteen primitives these screens use, from &lt;code&gt;box&lt;/code&gt; to &lt;code&gt;vstack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One copy has a hidden dependency. &lt;code&gt;heading&lt;/code&gt; imports &lt;code&gt;@expo/html-elements&lt;/code&gt;, which the generated package does not declare. Add it to &lt;code&gt;dependencies&lt;/code&gt;, or consumers fail on a package they did not install.&lt;/p&gt;

&lt;p&gt;Above the primitives sit the tokens and composed components. &lt;code&gt;tailwind.preset.js&lt;/code&gt; carries the brand and neutral colours plus &lt;code&gt;type.&amp;lt;name&amp;gt;&lt;/code&gt; for all eighteen Pokémon types. The same values live in &lt;code&gt;src/tokens/&lt;/code&gt; for runtime code. Eleven components use them: ScreenContainer, PokemonCard, TypeBadge, LoadingState, ErrorState, InfoRow, StatBar, EmptySlot, BackPill, ThemeToggle and Toaster. A &lt;code&gt;primitives.ts&lt;/code&gt; barrel keeps consumer imports on one package surface.&lt;/p&gt;

&lt;p&gt;Two decisions belong with the tokens, and both fail quietly when they are left to the components.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Only literal class strings generate.&lt;/strong&gt; A constructed &lt;code&gt;`bg-type-${t}`&lt;/code&gt; is invisible to Tailwind's scanner, and the colour silently never ships. The token file spells out every class in literal maps (&lt;code&gt;TYPE_BG_CLASS&lt;/code&gt; and friends) and exports lookup helpers, which keeps the whole palette in one auditable place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second is text contrast. &lt;strong&gt;Type colours need measured foregrounds, not a visual guess.&lt;/strong&gt; White on the water blue measures 3.48:1 and on the psychic pink 3.41:1, below &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;WCAG's 4.5:1 minimum for small text&lt;/a&gt;. Black clears 6:1 on both. &lt;code&gt;textOnTypeClass()&lt;/code&gt; computes the higher-contrast choice once and every type-coloured surface uses it. A hard-coded black foreground would put the fighting-type hero at 1.7:1.&lt;/p&gt;

&lt;p&gt;One kind of input could never have been typed in anyway: images. The theme toggle's moon and sun and the back pill's chevron ship as PNG files in &lt;code&gt;src/assets&lt;/code&gt;, which came across with the copy, and the &lt;code&gt;files&lt;/code&gt; list bundles them into the published package.&lt;/p&gt;

&lt;p&gt;Publish it the way posts 5 and 6 did, install step and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/ui &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Version 1.0.2 is the intended 1.0.0 API plus the patches later hardening passes added.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail finishes dressing
&lt;/h2&gt;

&lt;p&gt;The finished detail needs data the earlier contract did not carry. Contracts 3.2.2 adds &lt;code&gt;heightM&lt;/code&gt;, &lt;code&gt;weightKg&lt;/code&gt;, &lt;code&gt;abilities&lt;/code&gt;, &lt;code&gt;stats&lt;/code&gt; and optional &lt;code&gt;flavourText&lt;/code&gt; to &lt;code&gt;PokemonDetail&lt;/code&gt;. It also adds &lt;code&gt;types&lt;/code&gt; to &lt;code&gt;PartyMember&lt;/code&gt;, so the party can tint cards without another request. &lt;code&gt;parsePokemonDetail&lt;/code&gt; converts PokéAPI's units and normalises the flavour text once at the boundary.&lt;/p&gt;

&lt;p&gt;Both remotes now fetch the species payload beside the Pokémon, and the list passes type data when it adds a member. The app-file sweep in &lt;em&gt;Wiring a styling runtime into a federated build&lt;/em&gt; carries those edits. The new package source already came from the reference copy, so publish contracts and detail before the apps install them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/detail &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@pokedex/detail&lt;/code&gt; 4.0.2 rebuilds the view around the design system: a type-coloured collapsing hero, a ringed sprite, flavour text, Info rows, Base Stats and the Add button. &lt;strong&gt;It is a major because it gains peers&lt;/strong&gt;: &lt;code&gt;@pokedex/ui&lt;/code&gt;, NativeWind and Reanimated become requirements on every consumer.&lt;/p&gt;

&lt;p&gt;Its props do not change. The view still accepts &lt;code&gt;{ pokemon, loading, error, onRetry, onAddToParty, addDisabled, addLabel }&lt;/code&gt;, so both containers move to the new view with a version-pin edit. The largest visual change in the series reaches them through &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tvdhob187h42oycvyvb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tvdhob187h42oycvyvb.webp" alt="The finished Pokémon detail screen: a grass-green hero with the sprite in a translucent ring over a ghost #001, Bulbasaur's name and type pills, then a flavour-text quote card, Info rows and Base Stats bars in floating cards" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The collapse is the screen's signature move, and it is worth seeing before the wiring section, because every colour in it is a token class resolved through the seam:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#the-detail-finishes-dressing" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wiring a styling runtime into a federated build
&lt;/h2&gt;

&lt;p&gt;NativeWind's own docs support Metro; Re.Pack appears under community-driven solutions. From the other side, Callstack ships a first-party plugin, &lt;code&gt;@callstack/repack-plugin-nativewind&lt;/code&gt;, version-locked to the installed Re.Pack (5.2.5 here). Reanimated needs the same treatment: Re.Pack detects the package and warns when &lt;code&gt;ReanimatedPlugin&lt;/code&gt; is missing, so &lt;code&gt;@callstack/repack-plugin-reanimated&lt;/code&gt; goes in beside it at the same version. Both halves are true, and this wiring is the plugin's documented path.&lt;/p&gt;

&lt;p&gt;All three apps receive the styling dependencies. The two remotes also install detail 4.0.2 and contracts 3.2.2. Every app declares gluestack core and utils directly because the copied source imports them. Each app also pins &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt; to its React version, 19.2.3, so peer resolution cannot install a second React.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set &lt;/span&gt;overrides.react&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 overrides.react-dom&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set &lt;/span&gt;overrides.react&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 overrides.react-dom&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 @pokedex/detail@4.0.2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set &lt;/span&gt;overrides.react&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 overrides.react-dom&lt;span class="o"&gt;=&lt;/span&gt;19.2.3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;nativewind@4.2.6 @gluestack-ui/core@3.0.25 @gluestack-ui/utils@3.0.21 react-native-reanimated@4.6.0 react-native-worklets@0.12.1 @pokedex/ui@1.0.2 @pokedex/contracts@3.2.2 @pokedex/detail@4.0.2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss@3.4.19 @callstack/repack-plugin-nativewind@5.2.5 @callstack/repack-plugin-reanimated@5.2.5 postcss@8.4.47 postcss-loader@8.1.1 autoprefixer@10.4.20 @module-federation/enhanced@2.9.0 @react-native-community/cli@20.2.0 @react-native-community/cli-platform-android@20.2.0 @react-native-community/cli-platform-ios@20.2.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The &lt;code&gt;overrides&lt;/code&gt; block is why those installs start with &lt;code&gt;npm pkg set&lt;/code&gt;.&lt;/strong&gt; gluestack pulls &lt;code&gt;react-native-web&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt; in as transitive peers, and their react ranges collide with the app's pinned &lt;code&gt;react@19.2.3&lt;/code&gt;. Pinning both in &lt;code&gt;overrides&lt;/code&gt; settles it. Reaching for &lt;code&gt;--legacy-peer-deps&lt;/code&gt; instead hides the version mismatches a shared singleton needs you to see.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last four pins update &lt;code&gt;@module-federation/enhanced&lt;/code&gt; and the React Native CLI trio (the core package and both platform packages) to their current releases, which clear this stack's fixable &lt;code&gt;npm audit&lt;/code&gt; advisories; the repo README records what remains and why. &lt;code&gt;react-native-reanimated&lt;/code&gt; and &lt;code&gt;react-native-worklets&lt;/code&gt; carry native code, so the host needs its pods refreshed before the &lt;em&gt;Run it&lt;/em&gt; build: &lt;code&gt;( cd apps/host/ios &amp;amp;&amp;amp; bundle install &amp;amp;&amp;amp; bundle exec pod install )&lt;/code&gt;, the same convention the repo README uses. &lt;em&gt;Who shares what&lt;/em&gt; explains why &lt;code&gt;react-native-reanimated&lt;/code&gt; and &lt;code&gt;react-native-worklets&lt;/code&gt; are here at all. Pin &lt;code&gt;tailwindcss&lt;/code&gt; below 4. NativeWind 4's styling runtime, &lt;code&gt;react-native-css-interop&lt;/code&gt;, declares a Tailwind 3 dependency, and npm's &lt;code&gt;latest&lt;/code&gt; Tailwind is a 4.x that will not work here.&lt;/p&gt;

&lt;p&gt;The per-app configuration, entry files and rebuilt screens come from the reference copy. This sweep makes the reader's tree match the tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;app &lt;span class="k"&gt;in &lt;/span&gt;host list party&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/babel.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/global.css &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/index.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/jest.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/rspack.config.mjs &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/tailwind.config.js &lt;span class="se"&gt;\&lt;/span&gt;
     /tmp/pokedex-ref/apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/tsconfig.json apps/&lt;span class="nv"&gt;$app&lt;/span&gt;/
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/App.tsx /tmp/pokedex-ref/apps/host/mf-modules.d.ts apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/__mocks__/react-native-reanimated.js /tmp/pokedex-ref/apps/host/__mocks__/partyApp-styles.js apps/host/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/__tests__/RemoteBoundary.test.tsx apps/host/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/src/ListStack.tsx /tmp/pokedex-ref/apps/list/src/PokedexScreen.tsx /tmp/pokedex-ref/apps/list/src/detailApi.ts /tmp/pokedex-ref/apps/list/src/typesApi.ts apps/list/src/
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/list/__mocks__
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/__mocks__/react-native-reanimated.js /tmp/pokedex-ref/apps/list/__mocks__/styleMock.js apps/list/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/__tests__/addGate.test.tsx /tmp/pokedex-ref/apps/list/__tests__/doubleTapAdd.test.tsx /tmp/pokedex-ref/apps/list/__tests__/parsePokemonDetail.test.ts /tmp/pokedex-ref/apps/list/__tests__/parsePokemonTypes.test.ts apps/list/__tests__/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/src/PartyScreen.tsx /tmp/pokedex-ref/apps/party/src/PartyStack.tsx /tmp/pokedex-ref/apps/party/src/detailApi.ts /tmp/pokedex-ref/apps/party/src/styles.ts apps/party/src/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/__tests__/partyStateReady.test.ts /tmp/pokedex-ref/apps/party/__tests__/partySelfRecovery.test.tsx /tmp/pokedex-ref/apps/party/__tests__/removeAnimatesBeforeCommit.test.tsx apps/party/__tests__/
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; apps/party/__mocks__
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/__mocks__/react-native-reanimated.js apps/party/__mocks__/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/scripts &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/README.md &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Babel gains NativeWind's JSX source, the Rspack config gains the plugin, and a three-line &lt;code&gt;global.css&lt;/code&gt; holds the Tailwind directives. The interesting file is &lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nativewind/preset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui/tailwind.preset.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
  &lt;span class="na"&gt;darkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/detail/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three wiring warnings live in that file and its neighbours, and each one fails silently if ignored.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Scan the packages' source.&lt;/strong&gt; Tailwind generates a class only when it sees the literal string, and the design system's classes live in &lt;code&gt;node_modules&lt;/code&gt;. The &lt;code&gt;content&lt;/code&gt; globs must include the shipped source of &lt;code&gt;@pokedex/ui&lt;/code&gt; and &lt;code&gt;@pokedex/detail&lt;/code&gt;, resolved via &lt;code&gt;require.resolve&lt;/code&gt;. Miss this and the breakage looks intermittent: a class the app also uses still works, and only the classes unique to the library vanish.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Import &lt;code&gt;global.css&lt;/code&gt; in every exposed module, not just the entry.&lt;/strong&gt; Federation loads the exposed module, never the remote's own &lt;code&gt;index.js&lt;/code&gt;, so a &lt;code&gt;global.css&lt;/code&gt; imported only at the entry works standalone and silently no-ops federated. &lt;code&gt;ListStack.tsx&lt;/code&gt; imports it at the top for exactly this reason, and the party reaches it through the module described in &lt;em&gt;Styles are a boot-time registration&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code&gt;darkMode: 'class'&lt;/code&gt; is load-bearing.&lt;/strong&gt; NativeWind's &lt;code&gt;setColorScheme&lt;/code&gt; throws without it, and the &lt;em&gt;One toggle, three bundles&lt;/em&gt; flip is built on that call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Styles are a boot-time registration
&lt;/h3&gt;

&lt;p&gt;The party's copy of that import moved one step earlier, and the reason is worth the detour.&lt;/p&gt;

&lt;p&gt;Registering a stylesheet writes into the styling runtime every mounted component reads. When the party tab's module arrives during a lazy render, that write lands while the Pokédex's screens are on screen, and React says so in development: &lt;em&gt;Can't perform a React state update on a component that hasn't mounted yet&lt;/em&gt;, with a component stack pointing into the list's bundle. Three of them, on the tab's first open. Nothing breaks. The screen paints correctly, the warning is a development-only log, and a reader who never opens the second tab never sees it.&lt;/p&gt;

&lt;p&gt;It is still worth fixing, because the cause is now specific rather than mysterious. Post 8 recorded a warning of the same shape at boot and said plainly that it had not been traced. This one traces: &lt;code&gt;import '../global.css'&lt;/code&gt; at the top of an exposed module runs during the render that loads that module. Bisecting confirms it. Comment the import out and the tab opens silently.&lt;/p&gt;

&lt;p&gt;So the party's styles became a module of their own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/party/src/styles.ts&lt;/span&gt;
&lt;span class="c1"&gt;// The remote's styles as a module the host can preload, for the same reason partySlice is one:&lt;/span&gt;
&lt;span class="c1"&gt;// registering them is a side effect the styling runtime shares with every app already mounted.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../global.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It joins the exposes map beside the state module, and &lt;code&gt;PartyStack.tsx&lt;/code&gt; imports &lt;code&gt;./styles&lt;/code&gt; rather than the stylesheet directly, so the standalone build still registers them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PartyStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/PartyStack.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/partySlice.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/styles.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host then loads it in the effect that already loads the state module, one line above it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp/styles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party styles failed to load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That extends post 8's rule by a second half. What a remote must register before it renders (its reducer, its styles) loads at boot; screens still load on demand. The Pokédex tab needs no such move for a reason worth stating: it is the first thing the host renders, so its styles register while there is nothing else mounted to disturb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who shares what
&lt;/h2&gt;

&lt;p&gt;The shared map decision is the identity test from posts 3 and 6, asked three times with three different answers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nativewind&lt;/code&gt; belongs in the shared map because its runtime holds a module-level style registry and colour-scheme state. Two copies let a remote register styles in a registry the host never reads. Re.Pack &lt;a href="https://github.com/callstack/repack/discussions/1032" rel="noopener noreferrer"&gt;discussion #1032&lt;/a&gt; records the same registry failure. Its accepted answer suggests deep-sharing &lt;code&gt;react-native-css-interop/&lt;/code&gt;, later replies still report the problem, and &lt;a href="https://github.com/callstack/repack/pull/1242" rel="noopener noreferrer"&gt;PR #1242&lt;/a&gt; closed without merging its accumulation approach.&lt;/p&gt;

&lt;p&gt;The host provides the one stateful styling runtime. With NativeWind shared as an eager singleton and the Re.Pack plugin wired into each app, both remotes register styles in the registry the host reads. This build verifies that behaviour across both remotes and both colour schemes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/ui&lt;/code&gt; joins it as an eager host provider: one provider, one theme and one set of component instances. &lt;strong&gt;Build-time tools stay out of the runtime map.&lt;/strong&gt; Tailwind and PostCSS never ship, while Zod carries values rather than process-wide identity. The four host entries are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="nx"&gt;nativewind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativewind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-worklets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-worklets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reanimated and Worklets register native views and a worklet runtime once per process. When the list bundled another copy for the detail collapse, the app died at load with the same native view name registered twice. The host now provides one copy to every consumer.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;@pokedex/detail&lt;/code&gt; stays out on purpose. Per-consumer versioning is its feature: the skew ladder from post 5 still works after this post, and a list shipping detail 4.0.2 while the party lags on 3.x is a supported state, not a bug. Same test, opposite answer, both correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The greys collapse
&lt;/h2&gt;

&lt;p&gt;The screens keep their structure. The list renders PokemonCard in a three-column FlatList, and the party uses the same card beside the design system's EmptySlot. App-level colour literals move behind shared tokens; only token definitions and translucent detail overlays retain raw values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before, per app&lt;/th&gt;
&lt;th&gt;After, from the seam&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#2a75bb&lt;/code&gt; in two apps' buttons&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bg-pokemonGreen&lt;/code&gt;, one token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#6b7280&lt;/code&gt;, &lt;code&gt;#9ca3af&lt;/code&gt;, &lt;code&gt;#f3f4f6&lt;/code&gt; template greys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;text-darkGrey&lt;/code&gt;, &lt;code&gt;text-midGrey&lt;/code&gt;, &lt;code&gt;bg-offGrey&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#eef2ff&lt;/code&gt; / &lt;code&gt;#4338ca&lt;/code&gt; badge colours in the list&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TYPE_BG_CLASS&lt;/code&gt; per real type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;#fff&lt;/code&gt; screens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ScreenContainer&lt;/code&gt; variants: &lt;code&gt;bg-offWhite&lt;/code&gt; light, &lt;code&gt;bg-navy&lt;/code&gt; dark&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two smaller changes come with the conversion. The detail now arrives as a push inside each tab's stack, with the design system's BackPill floating over the hero, so the tab bar stays put while a detail is open:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#the-greys-collapse" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And post 6's header Refresh button becomes a pull gesture the list owns. The dispatch is unchanged (one invalidated tag, which both of post 10's endpoints provide), so the REST rows and the GraphQL badges refetch together from a gesture instead of a button:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#the-greys-collapse" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One toggle, three bundles
&lt;/h2&gt;

&lt;p&gt;ThemeToggle is a design-system component with no props: a press writes the colour scheme into the shared &lt;code&gt;nativewind&lt;/code&gt; singleton, and each remote mounts it in its own header's right slot. The interesting part is what it does not do: it never talks to a remote. The scheme is module-level state inside the shared styling runtime, every &lt;code&gt;dark:&lt;/code&gt; class in the runtime re-resolves on the write, and three independently shipped bundles repaint together. No doc promises that; it is verified in this build.&lt;/p&gt;

&lt;p&gt;The chrome changes on the same write. Everything derives from the same observable: the host themes its &lt;code&gt;NavigationContainer&lt;/code&gt; from &lt;code&gt;useColorScheme()&lt;/code&gt;, the same subscription the classes use. An earlier cut of this build kept a separate piece of host state beside it, and the native navigation bars flipped almost half a second before the content's styling pass. One source of truth put every surface on the same update wave.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#one-toggle-three-bundles" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Feedback rides the same singleton
&lt;/h2&gt;

&lt;p&gt;Party feedback uses the same shared package. &lt;code&gt;toast()&lt;/code&gt; is a plain function in &lt;code&gt;@pokedex/ui&lt;/code&gt;, while the host mounts the runtime's one &lt;code&gt;Toaster&lt;/code&gt; above the tab bar. After dispatching an add, the list reads the store and reports what the owner did. Two fast taps at five members produce one “joined your party” message and one “Your party is full” message because the reducer decides the outcome.&lt;/p&gt;

&lt;p&gt;The list never mounts the component that draws the capsule. It calls the shared presentation API, and the host's Toaster renders the result with the Pokémon's sprite and type colour.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#feedback-rides-the-same-singleton" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The party's remove confirms through the same path, which is the ownership rule from post 6 carried into presentation: each write's owner owns its feedback. &lt;code&gt;toast()&lt;/code&gt; also announces the message to the screen reader itself, so a caller cannot show the visual confirmation and skip the accessible one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#feedback-rides-the-same-singleton" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;The map earned its entries; prove it by removing one. In &lt;code&gt;apps/list/rspack.config.mjs&lt;/code&gt;, delete the &lt;code&gt;nativewind&lt;/code&gt; entry from &lt;code&gt;shared&lt;/code&gt;, restart the list's dev server, and reload.&lt;/p&gt;

&lt;p&gt;Nothing crashes. The app boots, the list renders, the sprites load, and the layout is quietly wrong: the grid's column wrappers lose their widths, so each card sizes to its own content and the three columns stop lining up between rows. The colours hold, and so do the badges and the type, which is what makes this failure easy to miss. The remote now bundles its own styling runtime. Its build compiles the remote's classes into a registry the shared components never read, and the widths are the part that goes missing on screen. The components from &lt;code&gt;@pokedex/ui&lt;/code&gt; are unaffected, because their classes were compiled by the host's scan into the host's runtime.&lt;/p&gt;

&lt;p&gt;Post 6's failure was loud, post 8's was silent, and this one is silent and partial: the screen still works, and its grid stops being the design system's grid. Put the entry back, restart, and the grid snaps into shape. &lt;strong&gt;A styling runtime is shared state, and shared state under federation is either a singleton or a bug.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Native assets still ship with the host binary.&lt;/strong&gt; Copy the tab icons, app icon, launch ball, Nunito fonts and native configuration from the companion. The &lt;code&gt;ios/Host&lt;/code&gt; copy carries the icon catalogue, launch storyboard and app delegate. Copy &lt;code&gt;project.pbxproj&lt;/code&gt; too, because Xcode renders the loose splash images only when the project lists them as resources. The final command links the typefaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/apps/host/src/assets apps/host/src/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/assets &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/react-native.config.js apps/host/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/list/react-native.config.js apps/list/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/party/react-native.config.js apps/party/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/apps/host/ios/Host apps/host/ios/
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/pokedex-ref/apps/host/ios/Host.xcodeproj/project.pbxproj apps/host/ios/Host.xcodeproj/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref/apps/host/android/app/src/main/res apps/host/android/app/src/main/
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx react-native-asset@2.3.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verdaccio is already running from the publish steps, and the three packages are published. Start the three dev servers, each from its own terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build and launch the host app once, so the new app icon and asset catalogue compile in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the Pokédex tab for the three-column card grid, tap a Pokémon for the finished detail, add it to the party and watch the confirmation rise, then flip the header toggle to watch every surface, both remotes and the chrome included, change scheme on one press. The whole loop, splash to empty party, looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/federated-design-system-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-design-system#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;One design system and one styling runtime now serve every remote. The remotes import &lt;code&gt;@pokedex/ui&lt;/code&gt; as a package, while federation resolves it to the host's copy. A shell release can re-skin them without rebuilding each remote, provided the component API remains compatible.&lt;/p&gt;

&lt;p&gt;The native boundary remains. &lt;code&gt;react-native-svg&lt;/code&gt; ships a native module, and native code cannot arrive through federation. Fonts reach the same boundary for a different reason: the binary must contain and register each typeface at build time.&lt;/p&gt;

&lt;p&gt;The companion makes both cases visible. The tab icons are plain tinted images, the Nunito faces are linked into the host binary, and the launch ball is a storyboard asset.&lt;/p&gt;

&lt;p&gt;Next: accessibility across the seam, a shared testing package that checks touch targets, contrast and focus order in remotes the host only meets at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gluestack.io/" rel="noopener noreferrer"&gt;gluestack-ui&lt;/a&gt; — the copy-in component architecture; "you copy, paste, and own your components"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nativewind.dev/" rel="noopener noreferrer"&gt;NativeWind&lt;/a&gt; — the className runtime, and the other-bundlers page that lists community solutions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@callstack/repack-plugin-nativewind" rel="noopener noreferrer"&gt;@callstack/repack-plugin-nativewind&lt;/a&gt; — the first-party plugin this post wires&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@callstack/repack-plugin-reanimated" rel="noopener noreferrer"&gt;@callstack/repack-plugin-reanimated&lt;/a&gt; — the Reanimated transform Re.Pack asks for by name&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://v3.tailwindcss.com/docs/content-configuration" rel="noopener noreferrer"&gt;Tailwind v3: content configuration&lt;/a&gt; — scanning your own component library's source&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/discussions/1032" rel="noopener noreferrer"&gt;callstack/repack discussion #1032&lt;/a&gt; — a remote's styles clobbering the host's registry; still open upstream&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/callstack/repack/pull/1242" rel="noopener noreferrer"&gt;callstack/repack PR #1242&lt;/a&gt; — the closed accumulation attempt; this build's singleton result is its own evidence&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;WCAG 2.2, contrast (minimum)&lt;/a&gt; — the 4.5:1 bar the type-colour tokens are computed against&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-11-design-system&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Two backends, one client? RTK Query vs Apollo in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 31 Aug 2026 00:04:31 +0000</pubDate>
      <link>https://dev.to/warrendeleon/two-backends-one-client-rtk-query-vs-apollo-in-react-native-31ln</link>
      <guid>https://dev.to/warrendeleon/two-backends-one-client-rtk-query-vs-apollo-in-react-native-31ln</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;Post 9&lt;/a&gt; closed on a promise: the stack stays the same and the backend splits in two, REST for the list, GraphQL for the type badges, one client and one cache. This post builds it.&lt;/p&gt;

&lt;p&gt;The question mark in the title is deliberate. During the transition, with a GraphQL backend arriving while REST is still live, one client wins almost regardless of which one it is, and if your app already runs RTK Query that settles it. At the destination, with GraphQL everywhere, the answer turns on two things you can check against your own app: whether you have a federation constraint, and whether your schema is relational enough that a normalised cache earns its keep. A normalised cache stores each entity once, one Pokémon in one slot, so every view that references it reads the same copy. This series has federation, so RTK Query stays the pick here. Change the context and the answer changes, and this post says where.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/rtk-query-vs-apollo-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The left shape is the build: REST and GraphQL feeding one &lt;code&gt;baseApi&lt;/code&gt; cache under one tag graph, with the host's Refresh button untouched. The right shape is two clients with two caches and no edge between them. That missing edge is what this post is really about.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second backend arrives
&lt;/h2&gt;

&lt;p&gt;The API team stands up GraphQL. For screens that need related data it can replace several round trips with one, which is where the speed-up comes from rather than from the protocol itself, and it is where the backend is heading. REST is not going away this quarter, or the next. For a while, often a long while, both are live, and every screen is served by one or the other.&lt;/p&gt;

&lt;p&gt;The hazard is running two data-fetching clients at once, whichever two you pick. Two clients keep two caches, and the two do not coordinate on their own. A mutation that goes out over GraphQL updates the GraphQL cache, and the update never reaches the REST cache holding the same record. The user edits a Pokémon on the screen served by GraphQL, switches to the list served by REST, and sees the old value. No exception, no failed request, no warning. Just a screen that is quietly wrong until something forces a refetch. Mid-cutover, when the two backends overlap most, is exactly when it does the most damage.&lt;/p&gt;

&lt;p&gt;So the transition question is how few caches to run, and the cheapest answer is one.&lt;/p&gt;

&lt;h2&gt;
  
  
  One slice, two protocols
&lt;/h2&gt;

&lt;p&gt;RTK Query makes one cache serve both protocols, in the slice this series has run since &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;post 6&lt;/a&gt;. Start from post 8's tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-08-client-state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list already fetches its rows over REST. It gains a second &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;server-state&lt;/a&gt; need, a type badge on each row, served over GraphQL. Two dependencies first, in the list remote only. &lt;code&gt;graphql&lt;/code&gt; stays on 16 because &lt;code&gt;graphql-request&lt;/code&gt; declares a peer range of 14 to 16 while npm's latest is 17:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;graphql@16.14.2 graphql-request@7.4.0 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the endpoint, in the same api slice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;typesApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GRAPHQL_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;POKEMON_TYPES&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CUSTOM_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid GraphQL response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;providesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonTypesQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;typesApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An api slice has exactly one &lt;code&gt;baseQuery&lt;/code&gt;, and the list's is &lt;code&gt;fetchBaseQuery&lt;/code&gt; on the REST base. A second protocol against a different URL comes from a &lt;code&gt;queryFn&lt;/code&gt;, which RTK's own docs list for "one-off queries that use a different base URL". &lt;code&gt;graphql-request&lt;/code&gt; posts the query, and the same file carries what the excerpt leaves out: the bounded, ordered GraphQL query itself, the Zod schema that polices ids and type names, &lt;code&gt;parsePokemonTypes&lt;/code&gt;, and the screen integration. Rather than print all of it, land your tree on the finished state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-10-two-backends /tmp/pokedex-ref-10
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-10/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is validated with its own &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; schema at the seam the REST list already guards, held constant so the comparison swaps only the protocol. The schema lives in the list app rather than the contract package, because a data definition travels with the domain that owns it (&lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;post 7's rule&lt;/a&gt;).&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;PokéAPI's GraphQL lives at &lt;code&gt;graphql.pokeapi.co/v1beta2&lt;/code&gt;. The older &lt;code&gt;v1beta&lt;/code&gt; schema is retired, and with it the &lt;code&gt;pokemon_v2_&lt;/code&gt; field prefix that fills every pre-2025 tutorial. Asking for &lt;code&gt;pokemon_v2_pokemon&lt;/code&gt; today returns &lt;code&gt;field 'pokemon_v2_pokemon' not found in type: 'query_root'&lt;/code&gt;. The working shape is the unprefixed one, checked live against the endpoint while writing this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things stay deliberately absent. There is no contracts bump: the endpoint injects into the existing &lt;code&gt;baseApi&lt;/code&gt;, and it declares the tag the list already owns. And &lt;code&gt;graphql-request&lt;/code&gt; stays out of the Module Federation shared map, exactly as Zod does. It is a value-only library with no instance identity to share, so it rides as the list remote's own dependency. Which protocol served a row stays the remote's private business, invisible to the host and to every other remote.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;PokéAPI's GraphQL is rate-limited to 100 calls an hour per IP, where REST is fair-use only. RTK Query's caching absorbs it in normal use. A run of cold reloads can still hit the cap, and a 429 mid-build is the limit, not a bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One tag across the cutover
&lt;/h2&gt;

&lt;p&gt;The host's Refresh button has dispatched &lt;code&gt;invalidateTags(['PokemonList'])&lt;/code&gt; since post 6, and it holds no reference to either endpoint. Both the REST list and the GraphQL types query provide &lt;code&gt;'PokemonList'&lt;/code&gt;, so one press refetches both. React Native DevTools' Network panel shows it happening: one tap lands both requests together, and the &lt;code&gt;v1beta2&lt;/code&gt; preview shows the type data arriving over GraphQL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fya0simec9ptxvbkragmk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fya0simec9ptxvbkragmk.webp" alt="React Native DevTools' Network panel: an empty log, then one Refresh tap lands two requests together, the GraphQL v1beta2 call and the REST pokemon list call, and the GraphQL row's preview shows the pokemontypes data arriving" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the transition thesis in one tag: &lt;strong&gt;whichever client you keep, keep one&lt;/strong&gt;. Nothing crosses automatically, though. A mutation clears the other protocol's cache when it names the tag they both provide, and only then; the shared graph is what makes that possible, not what performs it.&lt;/p&gt;

&lt;p&gt;The same race is visible on the device: on a cold start the REST rows land first, and the GraphQL badges arrive a moment later as each query fills the shared cache:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/rtk-query-vs-apollo-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo#one-tag-across-the-cutover" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The badges also degrade quietly. Point the GraphQL endpoint at an unreachable host and the rows still render, the badges are simply absent, and nothing moves on the screen.&lt;/p&gt;

&lt;p&gt;The badges make GraphQL's own pitch concrete too. Types over REST would mean 151 detail calls, one per row, because the list endpoint returns only names and URLs. Over GraphQL it is a single query. Call that what it is, client request fan-out rather than N+1: the classic problem describes a backend repeating a data access per row, while this is one client making a request per row because the endpoint gives it nothing else. Fewer round trips is the win, and it is a point for the protocol rather than for either client.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Apollo does that this can't
&lt;/h2&gt;

&lt;p&gt;RTK Query treats a GraphQL response as data to cache by endpoint, the same as any REST payload. Apollo does something RTK Query does not attempt: with its default &lt;code&gt;InMemoryCache&lt;/code&gt; it normalises. Each object with a &lt;code&gt;__typename&lt;/code&gt; and an id gets one slot in the cache, and every query that references it reads that slot. A mutation whose response carries the same type and id updates the entity everywhere it appears, with no refetch.&lt;/p&gt;

&lt;p&gt;That is real, and it is the honest reason to reach for Apollo. It is also narrower than the pitch. Auto-consistency covers modifying an entity already in the cache. It does not cover growing a list. In Apollo's own words, "a newly cached object isn't automatically added to any &lt;em&gt;list fields&lt;/em&gt; that should now include that object", so a new row still needs an update function or a refetch.&lt;/p&gt;

&lt;p&gt;The rest of Apollo's case holds up on its own terms. Fragment colocation lets a component declare the fields it needs and compose them upward by plain template-literal interpolation, with no build step; code generation is optional, and earns its place when you want typed results. Cache redirects can serve a detail view straight from data a list already fetched, but only when every field the detail query asks for is already in the cache. One extra field and the whole query hits the network. Subscriptions and &lt;code&gt;@defer&lt;/code&gt; are supported, and &lt;code&gt;@defer&lt;/code&gt; needs an incremental-delivery handler plus a streaming-fetch polyfill on React Native, so it is "supported, with setup" rather than free.&lt;/p&gt;

&lt;p&gt;And the schema matters more than any of this. PokéAPI's own shape, Pokémon referencing types, abilities, species and evolution chains, with the same entities reused across many queries, is exactly the relational shape where normalisation pays for itself. On this data, Apollo's cache is the better fit, and pretending otherwise would be the strawman this series avoids. (Relay sits in the same family. urql belongs there only once you add Graphcache: its default document cache stores whole responses by query, closer to RTK Query's model than to Apollo's. The trade-offs in &lt;em&gt;Choosing&lt;/em&gt; are about the normalised family, not any one library.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What federation does to the choice
&lt;/h2&gt;

&lt;p&gt;Start with the part that surprises people. Apollo has no &lt;code&gt;injectEndpoints&lt;/code&gt;, and it needs none. An operation is a document executed against a client, not an artifact registered in a store, so any remote can ship its own queries against a shared client with zero registration machinery. A remote can even extend the cache at mount through &lt;code&gt;cache.policies.addTypePolicies&lt;/code&gt;, which is documented public API. There is no official micro-frontend guide (an unanswered community thread and one customer story are the whole record), but the cache API happens to allow it. On the runtime-extensibility axis &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;post 9&lt;/a&gt; cared about, Apollo is not behind.&lt;/p&gt;

&lt;p&gt;It also does not share TanStack's context problem. Apollo caches a single React context on the React instance itself, keyed by a well-known symbol, precisely so two copies of the library cannot hand you divergent contexts. The "no client in context" error fires only when React itself is duplicated. The singleton case for &lt;code&gt;@apollo/client&lt;/code&gt; under federation is one cache instance and no version skew between the client and its hooks, not the context split.&lt;/p&gt;

&lt;p&gt;Federation still tips the choice in two places: the tag graph and the transition. The one tag that refetches across remotes, and across two protocols, is coordination RTK Query has &lt;a href="https://warrendeleon.com/blog/rtk-query-tags-vs-tanstack-query-keys/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-rtk-query-apollo" rel="noopener noreferrer"&gt;by construction&lt;/a&gt; and Apollo leaves to convention. And adopting Apollo mid-cutover means standing up a second cache next to the RTK Query one you already run, the exact two-cache shape from the start of this post, for the length of the migration. Apollo Client 4 also makes rxjs a required peer and moves its React exports, so the bundle gains a new dependency rather than swapping one.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p&amp;gt;RTK Query&amp;lt;/p&amp;gt;
&amp;lt;ul&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cache model:&amp;lt;/strong&amp;gt; by endpoint, refetch on invalidate&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cross-protocol invalidation:&amp;lt;/strong&amp;gt; one shared tag graph, built in&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Adding queries from a remote:&amp;lt;/strong&amp;gt; &amp;lt;code&amp;gt;injectEndpoints&amp;lt;/code&amp;gt; at runtime&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Relational, entity-reuse schema:&amp;lt;/strong&amp;gt; refetches what it already had&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;


&amp;lt;p&amp;gt;Apollo&amp;lt;/p&amp;gt;
&amp;lt;ul&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cache model:&amp;lt;/strong&amp;gt; normalised by &amp;lt;code&amp;gt;__typename&amp;lt;/code&amp;gt; + id, patch in place&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Cross-protocol invalidation:&amp;lt;/strong&amp;gt; per-client; a second cache to coordinate&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Adding queries from a remote:&amp;lt;/strong&amp;gt; documents against the client; nothing to register&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Relational, entity-reuse schema:&amp;lt;/strong&amp;gt; the cache pays for itself&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Choosing
&lt;/h2&gt;

&lt;p&gt;No absolute winner, so read down to the case that fits your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transition, both backends live:&lt;/strong&gt; run one client. If the app is already on RTK Query, and this series is, that decides it. One cache, one tag, no two-cache staleness to police, and it is the common case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destination, GraphQL everywhere, with a federation constraint and shared coordinated state:&lt;/strong&gt; RTK Query stays. The honest price is living without normalisation: tag invalidation refetches where Apollo would patch in place, and &lt;code&gt;updateQueryData&lt;/code&gt; covers the few spots where a hand-patched update is worth it. You trade some round trips for one cache and one tag graph across independently shipped remotes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destination, GraphQL everywhere, a relational entity-heavy schema, and no federation constraint:&lt;/strong&gt; Apollo, plainly. The normalised cache is the point of a GraphQL client, and without federation pulling the other way, that is the reason to adopt it. No hedge.&lt;/p&gt;

&lt;p&gt;The signals to watch for are two specific days. The day you notice most of your invalidations refetch data the cache already held: normalisation would have saved those round trips. And the day the second backend goes live and two caches disagree on screen with no error to explain it. The first is a nudge toward Apollo. The second is the reason to run one client, and only one, all along.&lt;/p&gt;

&lt;p&gt;Next: the design system becomes a federated singleton, one UI package shared at runtime, so every remote renders the same components without shipping its own copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/customizing-queries" rel="noopener noreferrer"&gt;RTK Query: customising queries&lt;/a&gt; — &lt;code&gt;queryFn&lt;/code&gt; as an inline baseQuery, and the GraphQL baseQuery example&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/automated-refetching" rel="noopener noreferrer"&gt;RTK Query: automated re-fetching&lt;/a&gt; — providing and invalidating by tag&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apollographql.com/docs/react/caching/overview" rel="noopener noreferrer"&gt;Apollo: caching overview&lt;/a&gt; — normalisation by &lt;code&gt;__typename&lt;/code&gt; and id&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apollographql.com/docs/react/caching/advanced-topics" rel="noopener noreferrer"&gt;Apollo: cache redirects&lt;/a&gt; — serving a query from the cache, and the all-fields-present rule&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.apollographql.com/docs/react/data/defer" rel="noopener noreferrer"&gt;Apollo: &lt;code&gt;@defer&lt;/code&gt;&lt;/a&gt; — incremental delivery and its setup&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pokeapi.co/docs/graphql" rel="noopener noreferrer"&gt;PokéAPI GraphQL&lt;/a&gt; — the v1beta2 endpoint and the 100-calls-per-hour limit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/graphql-request" rel="noopener noreferrer"&gt;graphql-request&lt;/a&gt; — the fetch-based client the queryFn uses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, the build at the tag &lt;code&gt;post-10-two-backends&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>State stacks under federation: the same React Native app built twice</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:09:59 +0000</pubDate>
      <link>https://dev.to/warrendeleon/state-stacks-under-federation-the-same-react-native-app-built-twice-3d30</link>
      <guid>https://dev.to/warrendeleon/state-stacks-under-federation-the-same-react-native-app-built-twice-3d30</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;Post 8&lt;/a&gt; ended by promising this one: the same app rebuilt on TanStack Query and Zustand, the stack most React Native teams reach for first. That build now exists, on a branch cut from post 8's tag.&lt;/p&gt;

&lt;p&gt;The rules I set myself: same three apps, same installed detail package, same feature set (the Pokédex list, the detail with its capped Add button, the party grid with remove, the live "My Party 3/6" counter), and no change the state stack did not force. The finished twin looks the same on the simulator, screen for screen, which is the control this comparison needs.&lt;/p&gt;

&lt;p&gt;The verdict, before the proof. Both stacks build this app well. The TanStack and Zustand version has fewer moving parts, and for adding server state from a remote it is the more federation-friendly of the two: nothing to register, nothing to inject. Redux Toolkit buys two narrow things: a store that grows at runtime, and a closed set of writes. They matter only when separately deployed teams share live client state with rules that have to hold. If your app never reaches that point, the lighter stack wins and most of this argument will not apply.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The arrows run into the contract on the Redux side and out of it on the twin: Redux lets the remotes add to the seam at runtime, and this branch has them consume what the seam already ships. &lt;code&gt;@pokedex/detail&lt;/code&gt; sits in both halves at the same version, because it is the same package, unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server state: nothing to inject
&lt;/h2&gt;

&lt;p&gt;TanStack Query's model is decentralised, and that is its best argument here. There is no central API object, no registration step, no build-time list of what exists. Any component in any bundle calls &lt;code&gt;useQuery&lt;/code&gt; with any key against the shared client. A remote shipped a year after the shell can add server state to the running app by calling a hook.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A claim you will meet says TanStack needs every query key registered at the shell's build time.&lt;/strong&gt; It is false. A key is a plain array named at the call site, so a remote shipped long after the shell registers its own keys at runtime, against the shared client, with nothing to inject. If anything, that is the more decentralised model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Post 8 needed &lt;code&gt;baseApi.injectEndpoints&lt;/code&gt;, one shared instance, and the middleware that runs its cache. The list app's data access on this branch is a fetch and a hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/list/src/listApi.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchPokemonList&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonSummary&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://pokeapi.co/api/v2/pokemon?limit=151&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PokéAPI responded &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePokemonList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchPokemonList&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;parsePokemonList&lt;/code&gt; is imported from the contract and is byte-identical to post 8's. The &lt;a href="https://warrendeleon.com/blog/runtime-api-validation-zod-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; parsing, the model, the shapes: all of it survives the swap, because the data model belongs to the domain rather than to whichever library is holding the cache.&lt;/p&gt;

&lt;p&gt;Sharing one client across separately built bundles is supported, with a caveat. The v5 migration guide's answer for micro-frontends is to pass a &lt;code&gt;queryClient&lt;/code&gt; to a hook or a provider, replacing v4's custom context. That is a paragraph in a migration guide rather than a chapter on federation. The failure modes in React Native are the singleton ones this series has met before: two copies of &lt;code&gt;@tanstack/react-query&lt;/code&gt; give you "No QueryClient set", two copies of React give you null hooks.&lt;/p&gt;

&lt;p&gt;The cost lands somewhere else. RTK's injected endpoints extend one typed surface, so a team adding an endpoint sees the whole API and the tag graph it shares. TanStack's coordination is a string array. My answer is the contract package again: the key factory ships beside the client, so a rename is a version bump every consumer sees.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/contracts/src/query.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pokemonKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pokemon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a recommendation, not documented practice. Key factories are established community practice for key sprawl at scale rather than an answer the official docs prescribe, and nothing in either addresses two teams owning keys under one prefix.&lt;/p&gt;

&lt;p&gt;Post 8's endpoint-name collision has a quieter mirror here. On RTK, two apps injecting an endpoint called &lt;code&gt;getPokemonDetail&lt;/code&gt; had to declare the collision with &lt;code&gt;overrideExisting&lt;/code&gt;, and load order picked the winner. On TanStack, two apps calling &lt;code&gt;useQuery&lt;/code&gt; with &lt;code&gt;pokemonKeys.detail(1)&lt;/code&gt; share one cache entry, because keys compare by value. Nothing is registered, so nothing collides. Open Bulbasaur in the Pokédex and open it again from the Party tab, and the second screen renders from cache.&lt;/p&gt;

&lt;p&gt;Whether a request follows is a default worth knowing: left on its defaults, TanStack marks cached data stale immediately (&lt;code&gt;staleTime: 0&lt;/code&gt;), so a second observer mounting on the same key renders the cache &lt;em&gt;and&lt;/em&gt; fires a background refetch. The &lt;code&gt;staleTime: 60_000&lt;/code&gt; on the client above (a minute, matching how long the Redux build keeps an unsubscribed cache entry) is what makes the remount cost no second request, so the two builds answer it the same way. The bad version of the shared entry is two teams whose &lt;code&gt;queryFn&lt;/code&gt;s have drifted, where the data depends on which screen the user opened first.&lt;/p&gt;

&lt;p&gt;One small thing the shell gave up. Post 8's &lt;code&gt;baseApi&lt;/code&gt; carried the base URL for every consumer. Here each &lt;code&gt;queryFn&lt;/code&gt; fetches whatever it likes, and the host has no say in where a remote's data comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidation: a prefix is a promise
&lt;/h2&gt;

&lt;p&gt;The host's Refresh button is host chrome that reaches into a remote's data without importing any of it. Post 8 dispatched &lt;code&gt;invalidateTags(['PokemonList'])&lt;/code&gt;. The twin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
  &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pokemonKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;hitSlop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Refresh Pokédex"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Refresh&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query filters match by prefix, and only an &lt;code&gt;exact&lt;/code&gt; filter narrows to one key, so everything the factory builds under &lt;code&gt;['pokemon']&lt;/code&gt; goes stale at once and any mounted query refetches. The prefix is the whole agreement.&lt;/p&gt;

&lt;p&gt;I compared these two models in &lt;a href="https://warrendeleon.com/blog/rtk-query-tags-vs-tanstack-query-keys/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;tags vs query keys&lt;/a&gt;, and that argument holds here. Federation adds one thing: the two sides are separately deployed bundles that never sat in the same build. A tag is a value declared in a shared list; a key prefix is string equality across code that ships on different days. Putting the factory in the versioned contract turns the convention back into something a compiler and a release process can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client state: the store moves back to the shell
&lt;/h2&gt;

&lt;p&gt;This is where the two designs differ most.&lt;/p&gt;

&lt;p&gt;In post 8 the party app created the party's state, owned it, and injected it into a store the host had already built. &lt;code&gt;combineSlices&lt;/code&gt; exists for exactly that, and the slice arrived at runtime from a bundle the shell knew nothing about.&lt;/p&gt;

&lt;p&gt;Zustand has no equivalent to that injection step. It creates stores dynamically without any trouble, so a remote can bring a store to a running app; what it has no built-in answer for is folding one into an existing store the way &lt;code&gt;inject&lt;/code&gt; does. I do not read that as an oversight: Zustand's natural federated answer is several independent stores, one per remote, with no machinery at all. That answer works until a piece of state has to cross a module boundary, which is the case this whole series is about.&lt;/p&gt;

&lt;p&gt;So the store ships in the contract package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/contracts/src/partyStore.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PartyState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the cap lives with the owner's action&lt;/span&gt;
    &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createStore&lt;/code&gt; from &lt;code&gt;zustand/vanilla&lt;/code&gt;, because the contract holds no React. Consumers subscribe with &lt;code&gt;useStore(partyStore, selector)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The ownership inversion is the honest cost. The party app no longer writes the party's rules; it reads state that arrives in a package it installs. Its &lt;code&gt;partySlice.ts&lt;/code&gt; is deleted, and so is the &lt;code&gt;./partySlice&lt;/code&gt; entry in its Module Federation config, so it exposes one module again instead of two. On the host side, &lt;code&gt;src/store.ts&lt;/code&gt; is gone, the boot import that loaded the party's state module is gone, and the ambient declaration that typed it went with them.&lt;/p&gt;

&lt;p&gt;Those deletions are the trade. Post 8 spent a section on a dispatch that vanishes because the slice's owner had not loaded yet, and it took a boot import, a readiness marker and a gated button to close that window. Here the failure cannot occur: the state exists as soon as anything imports the contract. The price was paid earlier, as a release. Every new piece of shared state is a new version of &lt;code&gt;@pokedex/contracts&lt;/code&gt; that every app installs: a deploy on the web, an App Store cycle on mobile. Neither stack removes the coordination; they move it to a different day.&lt;/p&gt;

&lt;p&gt;The component package is the control group. &lt;code&gt;@pokedex/detail&lt;/code&gt; 3.1.0 is untouched: same version, same file, installed by both apps, wired to a Zustand action on one branch and a Redux action creator on the other. Worth being exact about what Redux matches on, because the comparison rests on it: a reducer recognises a closed set of action-type strings, so the two sides agree on &lt;code&gt;party/add&lt;/code&gt;, not on a privileged creator object. A view that renders what it is handed does not care which library computed it. That is the payoff of the &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks" rel="noopener noreferrer"&gt;library rule from post 6&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership: setState from anywhere
&lt;/h2&gt;

&lt;p&gt;The cap is a rule the party owns, and on this branch it lives inside &lt;code&gt;add&lt;/code&gt;. Every write that goes through &lt;code&gt;add&lt;/code&gt; is capped. &lt;code&gt;setState&lt;/code&gt; is public on every Zustand store, and it does not go through &lt;code&gt;add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The whole demonstration sits in the list app's detail container, next to the legitimate wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/list/src/ListStack.tsx — a module with no business writing this state&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bypassTheCap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;partyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bypass-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spriteUri&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No type error. No warning. Nothing the owning team can review. The party fills to six, the Add button disables itself, and a long-press on the same screen puts a seventh Pokémon in a six-slot party:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-state-stacks#ownership-setstate-from-anywhere" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The long-press is only there so the bypass can be filmed. The call is the point.&lt;/p&gt;

&lt;p&gt;Watch the header. The party's grid renders six slots, so the seventh member has nowhere to appear, and the counter reading &lt;strong&gt;7/6&lt;/strong&gt; is the only surface that shows it. The owning team's own UI cannot display the state a foreign module just wrote.&lt;/p&gt;

&lt;p&gt;Redux permits foreign writes too: any module can dispatch anything. What differs is that a slice declares every write the owner will honour, so an action the owner never declared changes nothing, and the legitimate mutations are readable in one file. A Zustand store exposes the state itself, and a rule inside a function binds only the callers who use that function.&lt;/p&gt;

&lt;p&gt;Inside one team this is a non-issue. Everyone knows &lt;code&gt;setState&lt;/code&gt; is there, code review catches it, and the convention holds because the people holding it sit together. The moment the writer and the owner are different teams on different release schedules, a convention is what you have in place of a boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing
&lt;/h2&gt;

&lt;p&gt;There is no absolute winner. The deciding factor is who is allowed to write what.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One team owns all the state.&lt;/strong&gt; TanStack Query and Zustand. Fewer concepts, less ceremony, nothing to inject, and &lt;code&gt;setState&lt;/code&gt; is a convenience rather than a hole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent teams, shared server cache, no shared client state.&lt;/strong&gt; Still TanStack. Share the client as a singleton, put the key factory in a versioned contract, and you have the coordination you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent teams writing each other's client state, with rules that must hold.&lt;/strong&gt; This is the case Redux Toolkit's weight is for: a store that grows at runtime and a closed set of writes per slice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile, with a slow release cycle.&lt;/strong&gt; Weigh the shape of the change. On the Zustand branch every new piece of shared state is a contract release, and on mobile a release waits on App Store review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signal to watch for is the day a second team needs to write state the first team owns. After that day, the question is whether you would rather coordinate through a type or through a convention.&lt;/p&gt;

&lt;p&gt;The twin is on its own branch, cut from post 8's tag, at &lt;code&gt;post-09-tanstack-zustand&lt;/code&gt;. It is a fork, not a step forward: the series carries on from post 8 on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, the other half of the comparison. The stack stays the same and the backend splits in two: REST for the list, GraphQL for the type badges, one client, one cache, two teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5" rel="noopener noreferrer"&gt;TanStack Query: migrating to v5&lt;/a&gt; — the micro-frontend answer, passing a &lt;code&gt;queryClient&lt;/code&gt; in place of a custom context&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/query/latest/docs/framework/react/guides/filters" rel="noopener noreferrer"&gt;TanStack Query: query filters&lt;/a&gt; — prefix matching, and &lt;code&gt;exact&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-keys" rel="noopener noreferrer"&gt;TanStack Query: query keys&lt;/a&gt; and &lt;a href="https://tkdodo.eu/blog/effective-react-query-keys" rel="noopener noreferrer"&gt;effective query keys&lt;/a&gt; — key factories at scale&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://zustand.docs.pmnd.rs/reference/apis/create-store" rel="noopener noreferrer"&gt;Zustand: &lt;code&gt;createStore&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://zustand.docs.pmnd.rs/reference/hooks/use-store" rel="noopener noreferrer"&gt;&lt;code&gt;useStore&lt;/code&gt;&lt;/a&gt; — the vanilla store and its React subscription&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/api/combineSlices" rel="noopener noreferrer"&gt;Redux Toolkit: &lt;code&gt;combineSlices&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://redux-toolkit.js.org/rtk-query/usage/code-splitting" rel="noopener noreferrer"&gt;RTK Query code splitting&lt;/a&gt; — what the other branch relies on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-09-tanstack-zustand&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Client state across the seam: remotes that bring their own slices in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 17 Aug 2026 00:09:56 +0000</pubDate>
      <link>https://dev.to/warrendeleon/client-state-across-the-seam-remotes-that-bring-their-own-slices-in-react-native-l7b</link>
      <guid>https://dev.to/warrendeleon/client-state-across-the-seam-remotes-that-bring-their-own-slices-in-react-native-l7b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;Post 6&lt;/a&gt; ended on an honest limitation: close the app and reopen it, and nothing you did survives. No party, no favourites, no memory. Everything crossing the seam so far has been server state: data PokéAPI owns, held in a cache every module shares. The app itself owns nothing yet.&lt;/p&gt;

&lt;p&gt;This post gives it something to own: the party of six. &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;Client state&lt;/a&gt; has no server behind it and no cache to refetch. It lives in a Redux slice, and under federation a slice raises the question this whole series circles: who owns it, and how do other features touch it without touching its owner? The &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;ownership essay&lt;/a&gt; answered in principle. This post answers in code.&lt;/p&gt;

&lt;p&gt;The shape we're building:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hold one idea in your head: everyone shares the cache, but a client-state slice has exactly one owner. The party app owns the party. Everything another module needs (one action, one read shape, one cap) crosses through the contract, and near the end we dispatch into the slice before its owner has loaded, on purpose, to watch what Redux does with an action nobody is listening for.&lt;/p&gt;

&lt;p&gt;Carry on from your own post 6 code if you built along. Otherwise, start from its finished state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-06-shared-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Who owns client state?
&lt;/h2&gt;

&lt;p&gt;Two kinds of state now live in this app, each with a different owner, and between them sits the interaction that crosses.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;server cache&lt;/strong&gt; is everyone's. Post 6 built it: one &lt;code&gt;baseApi&lt;/code&gt; in the contract, one store in the host, endpoints injected by whoever owns the data. No app owns a cache entry. The data belongs to PokéAPI and the cache is only holding a copy, which is why any module may read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private state&lt;/strong&gt; has one owner and stays inside it. Which members the party holds, how removal works, what the cap means: all of that is the party app's business, in a slice nothing else imports.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;crossing interaction&lt;/strong&gt; is the narrow strip between them. The Pokédex needs to add a member to a party it does not own, and its header wants to show "My Party 3/6" for state it cannot see. Those crossings get typed, named, versioned and put in the contract, because the rule from the ownership essay holds here too: apps never depend on each other. Apps depend on contracts.&lt;/p&gt;

&lt;p&gt;That taxonomy decides everything else in this post. What follows is just each row of it, built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall, and the reducer moves
&lt;/h2&gt;

&lt;p&gt;The party's slice has to join the running store, and Redux Toolkit (RTK) has an API for exactly this: &lt;code&gt;combineSlices&lt;/code&gt; builds a reducer with an &lt;code&gt;inject&lt;/code&gt; method, and a slice injected at runtime starts reducing from that moment on. So the party app needs to call &lt;code&gt;inject&lt;/code&gt; on the reducer object the host's store actually wired in.&lt;/p&gt;

&lt;p&gt;Post 6's host built that reducer inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/src/store.ts, in post 6&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDefaultMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDefaultMiddleware&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing to reach. The reducer is an expression inside the host's source, never exported, and even an export would not help: apps don't import apps. A copy is worse than useless: &lt;code&gt;combineSlices(baseApi)&lt;/code&gt; in the party app builds a second reducer that no store runs, and injecting into it changes nothing on screen.&lt;/p&gt;

&lt;p&gt;This is the identity argument from post 6, third time around. &lt;code&gt;baseApi&lt;/code&gt; moved to the contract because a consumer can only inject endpoints into the &lt;em&gt;same&lt;/em&gt; instance the store wired. The root reducer moves for the same reason, one file over. &lt;code&gt;packages/contracts/src/store.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;combineSlices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the host's store slims to an import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/src/store.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;configureStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDefaultMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDefaultMiddleware&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host still owns the store: the middleware, the &lt;code&gt;Provider&lt;/code&gt;, the wiring. The contract package now owns the reducer the same way it owns the api instance. Because contracts is a federation singleton, &lt;code&gt;rootReducer&lt;/code&gt; is one object in the whole runtime, and a slice injected by an app built months after the shell lands in the reducer the shell is already running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract carries the crossing
&lt;/h2&gt;

&lt;p&gt;Now the taxonomy's middle row. The interactions that cross an app boundary go in a new &lt;code&gt;packages/contracts/src/party.ts&lt;/code&gt;, and it is short on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nanoid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addToParty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party/add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nanoid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyStateReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party/stateReady&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addToParty&lt;/code&gt; is the one action anything outside the party dispatches. The contract owns the action's shape; the party's reducer owns what it means. The &lt;code&gt;prepare&lt;/code&gt; callback stamps each member with a &lt;code&gt;nanoid()&lt;/code&gt; (shipped inside RTK, no new dependency), so the reducer stays pure and two copies of the same Pokémon stay distinguishable. Duplicates are allowed by design: a party of six Magikarp is a valid life choice, and the uid is what tells them apart when one gets removed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MAX_PARTY&lt;/code&gt; sits at the seam because every surface that reflects the rule has to read the same number: a disabled button in one app, a counter in another, the guard in the owner's reducer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;partyStateReady&lt;/code&gt; is the odd one out: no reducer case handles it, and nothing about the party changes when it is dispatched. It exists for the boot sequence, and the section on loading state modules shows the one useful thing it does.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PartySliceShape&lt;/code&gt; is the read side, and the optional marker is the design, not defensiveness. The slice is injected at runtime by a module the reader does not control, so at the moment a foreign module reads, &lt;code&gt;state.party&lt;/code&gt; may not exist yet. Readers write &lt;code&gt;s.party?.members ?? []&lt;/code&gt; and render something honest either way. Typed alternatives exist (RTK's &lt;code&gt;withLazyLoadedSlices&lt;/code&gt; can thread the possibly-absent slice through declaration merging), but the tolerant shape teaches the situation rather than hiding it, and the &lt;em&gt;Now break it&lt;/em&gt; sabotage depends on you understanding it.&lt;/p&gt;

&lt;p&gt;Notice what is absent. The party will also have a &lt;code&gt;remove&lt;/code&gt; action, and it appears in no contract, because nobody else dispatches it. The contract carries what crosses, nothing more; an entry nobody consumes is a liability with a version number.&lt;/p&gt;

&lt;p&gt;Both new files are additive, so the version is a minor; the tag carries &lt;code&gt;3.1.3&lt;/code&gt;, the minor plus the same hardening patches the 3.0.x line took. Publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;+ @pokedex/contracts@3.1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host and the list both sit on &lt;code&gt;^3.0.0&lt;/code&gt;, and here the lockfile matters more than the caret. A plain &lt;code&gt;npm install&lt;/code&gt; changes nothing: the lockfile pins &lt;code&gt;3.0.3&lt;/code&gt;, and install honours the lockfile even though the caret would accept &lt;code&gt;3.1.3&lt;/code&gt;. Walking a caret forward is its own command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm update @pokedex/contracts &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;package.json&lt;/code&gt; edit, one lockfile line moved. The pin only gets touched when a consumer crosses a major. One consumer is about to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The owner
&lt;/h2&gt;

&lt;p&gt;The party app has been the series' control group: contract on &lt;code&gt;^1.0.0&lt;/code&gt;, detail on &lt;code&gt;^1.0.0&lt;/code&gt;, no store access, a static grid of six empty slots. Growing a slice ends that on every front, and the adoption is a deliberate edit because two majors is a real distance. &lt;code&gt;apps/party/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"@pokedex/contracts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.1.0"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"@pokedex/detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.1.0"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"@reduxjs/toolkit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.12.0"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"react-redux"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^9.3.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt;, and the build breaks immediately, in the compiler rather than at runtime. Party's stack still mounts the detail screen the 1.0.0 way: &lt;code&gt;import PokemonDetailScreen from '@pokedex/detail'&lt;/code&gt;, straight into &lt;code&gt;component=&lt;/code&gt;. The 3.x package has no default export any more, so that import now resolves to the package's namespace object, and the mount stops type-checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nx"&gt;TS2322&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typeof import(".../@pokedex/detail/dist/index")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt;
&lt;span class="nx"&gt;assignable&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ScreenComponentType&amp;lt;PartyParamList, "PokemonDetail"&amp;gt; | undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail package has crossed two majors around this app while it wasn't looking, 1.x to 3.x. What 3.x exports is the named &lt;code&gt;PokemonDetailView&lt;/code&gt;, a view that demands data as props, so the compile error forces party to write the same four-line container the list app wrote in post 6. A container needs data, and that becomes a wall of its own in &lt;em&gt;The party needs data of its own&lt;/em&gt;. Chains like this are what version lag actually costs: not a crash in production, a stack of walls on the day you finally adopt.&lt;/p&gt;

&lt;p&gt;The slice itself is the whole point of the post, and it fits on a screen. &lt;code&gt;apps/party/src/partySlice.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSlice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PayloadAction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partySlice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSlice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PartyMember&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;reducers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Private: nobody else dispatches remove, so it ships in no contract.&lt;/span&gt;
    &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PayloadAction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;extraReducers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the cap lives with the owner&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;partySlice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Importing this module is what adds the reducer to the shared store.&lt;/span&gt;
&lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partySlice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things carry the design. The crossing action binds through &lt;code&gt;extraReducers&lt;/code&gt;: the party matches the &lt;em&gt;contract's&lt;/em&gt; &lt;code&gt;addToParty&lt;/code&gt;. Be precise about why that works across separately built apps, because it is easy to credit the wrong thing. &lt;code&gt;builder.addCase&lt;/code&gt; reads &lt;code&gt;actionCreator.type&lt;/code&gt; and keys its reducer by that string, so what has to agree is &lt;code&gt;party/add&lt;/code&gt;, not the identity of the creator object. Two copies of the contract would still match. Defining the string, the cap and the read shape once instead of retyping them per side is the &lt;em&gt;versioned package's&lt;/em&gt; work: publishing the contract is what prevents that drift, singleton or not. Where runtime singleton identity earns its place is the objects, &lt;code&gt;rootReducer&lt;/code&gt; and &lt;code&gt;baseApi&lt;/code&gt;: an injection only lands in the store the host wired if both sides hold the same instance.&lt;/p&gt;

&lt;p&gt;And the cap guard lives here, in the owner. The contract publishes the number; only the owner enforces it. A dispatcher that forgets to disable its button still cannot push a seventh member.&lt;/p&gt;

&lt;p&gt;The last line is the mechanism. &lt;code&gt;inject&lt;/code&gt; takes the slice itself (&lt;code&gt;createSlice&lt;/code&gt; defaults its &lt;code&gt;reducerPath&lt;/code&gt; to &lt;code&gt;name&lt;/code&gt;, so the state mounts at &lt;code&gt;state.party&lt;/code&gt;, matching &lt;code&gt;PartySliceShape&lt;/code&gt;), and importing the module is what performs the injection. That makes the slice a &lt;em&gt;state module&lt;/em&gt;: a federated module whose value is its side effect. The party's bundler config exposes it alongside the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PartyStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/PartyStack.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/partySlice.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The party also joins the state trio in its &lt;code&gt;shared&lt;/code&gt; map: &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; with the hand-stated &lt;code&gt;version&lt;/code&gt; every exports-map package needs in this config, &lt;code&gt;@pokedex/contracts&lt;/code&gt; as a singleton, none of them eager, exactly as the list app declares them. Same rule as post 6: the host provides the copies, remotes consume them.&lt;/p&gt;

&lt;p&gt;One dev-loop observation, since RTK guards against a hazard here: on paper, re-running this module hot-swaps a new function identity into an existing &lt;code&gt;reducerPath&lt;/code&gt;, and RTK's &lt;code&gt;inject&lt;/code&gt; refuses to replace it (a console error in development; the original reducer stays live). In this Re.Pack setup the hazard stays theoretical: an edit to the slice file reloads the app rather than hot-swapping the module, so you get a fresh store instead of a double inject. Worth knowing which of the two your stack does before you trust either.&lt;/p&gt;

&lt;p&gt;With state to render, the placeholder tab becomes a screen. The interesting lines of &lt;code&gt;PartyScreen.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDispatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSelector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filled slots render the sprite, the name, and a remove control dispatching &lt;code&gt;remove(member.uid)&lt;/code&gt;; empty slots keep the dashed placeholder up to six; the header counts &lt;code&gt;{members.length}/{MAX_PARTY}&lt;/code&gt;; tapping a member pushes &lt;code&gt;PokemonDetail&lt;/code&gt; with &lt;code&gt;{ id }&lt;/code&gt;, the same &lt;code&gt;DetailParams&lt;/code&gt; from post 5, no new fields. Note the owner reading its &lt;em&gt;own&lt;/em&gt; state through the tolerant shape. Even here the optional earns its keep: the first render can beat the first action into the store, and injection registers the reducer but leaves &lt;code&gt;state.party&lt;/code&gt; undefined until an action next reaches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  State modules load at boot
&lt;/h2&gt;

&lt;p&gt;The slice injects when its module is imported. So far only one thing imports it: the party screen, which also imports &lt;code&gt;remove&lt;/code&gt;. That means the slice exists only after the user opens the Party tab, and the Pokédex is about to dispatch into it long before that. Someone has to load the state module early, and only one participant is alive at boot on every path: the shell. &lt;code&gt;apps/host/App.tsx&lt;/code&gt;, in an effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Screens load on demand; state modules load at boot.&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partyApp/partySlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;partyStateReady&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;party state module failed to load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host already declares &lt;code&gt;partyApp&lt;/code&gt; in its remotes map, so this adds no coupling it didn't have. It fires the import and holds no reference to the module's contents: the ambient declaration types the module as empty, because the host imports it for the side effect and has no business naming anything inside. Screens stay lazy, because a screen the user has not opened costs nothing to defer.&lt;/p&gt;

&lt;p&gt;Loading at boot is a head start, not a guarantee. The chunk crosses the network, and nothing stops a fast finger reaching an Add button before it lands. A &lt;code&gt;party/add&lt;/code&gt; dispatched into a store with no reducer for it vanishes without a trace: no warning, no error, Redux ignoring an unmatched action by design. So the resolve is made visible, and this is where &lt;code&gt;partyStateReady&lt;/code&gt; earns its keep. &lt;code&gt;inject()&lt;/code&gt; swaps an entry in a reducer map and rebuilds the combined reducer, but it never dispatches, so &lt;code&gt;state.party&lt;/code&gt; stays undefined until the &lt;em&gt;next&lt;/em&gt; action runs through the new reducer. The marker is that action: the &lt;code&gt;.then&lt;/code&gt; dispatches it the moment the module resolves, &lt;code&gt;state.party&lt;/code&gt; appears, and every subscriber re-renders with the slice in place.&lt;/p&gt;

&lt;p&gt;The write side closes the loop by gating on exactly that: the container in &lt;em&gt;The write arrives as a prop&lt;/em&gt; disables Add while &lt;code&gt;s.party&lt;/code&gt; is undefined. A tap that cannot happen is a dispatch that cannot be lost.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The effect placement is an observation, not a diagnosis.&lt;/strong&gt; At module scope this import produced &lt;em&gt;Can't perform a React state update on a component that hasn't mounted yet&lt;/em&gt; on some cold starts and not others, and moving it into an effect removed the warning across repeated cold starts. What produces the update is not something this post has pinned down: &lt;code&gt;rootReducer.inject()&lt;/code&gt; is not the culprit, because it swaps an entry in a reducer map and rebuilds the combined reducer without ever dispatching or calling &lt;code&gt;replaceReducer&lt;/code&gt;, so it notifies nobody by itself. Until the update is traced to its source, treat the placement as a local observation: the effect is the arrangement that made the warning stop, and an effect is where React expects a side effect to live anyway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing awaits that import, so a party server that is down cannot block boot. That run is worth doing rather than trusting. Stop the party dev server and cold-start the app: the federation runtime reports the failed manifest fetch loudly in development (&lt;code&gt;[ Federation Runtime ]: Failed to get manifest. #RUNTIME-003&lt;/code&gt;), and the shell carries on. The Pokédex renders, the counter reads an honest 0/6 through the tolerant shape, the Add button stays disabled because &lt;code&gt;partyStateReady&lt;/code&gt; never fired, and the app runs without the slice, which is precisely the state &lt;code&gt;PartySliceShape&lt;/code&gt; was designed to describe.&lt;/p&gt;

&lt;p&gt;One detail from the observed run: the import settles without rejecting, so the &lt;code&gt;.catch&lt;/code&gt; never actually fires for this failure; the runtime contains it and reports it on its own. The catch stays, one line guarding the rejection path so a failed load can never surface as an unhandled rejection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The write arrives as a prop
&lt;/h2&gt;

&lt;p&gt;The Pokédex side of the crossing starts in the detail view, and the detail view is an installed component library: the one place the write must &lt;em&gt;not&lt;/em&gt; live. The ownership essay drew this line: a shared component renders what it is given; a write that crosses a domain boundary is wired by the consumer. So &lt;code&gt;@pokedex/detail&lt;/code&gt; 3.1.0 is an additive minor with three optional props:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailViewProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onAddToParty&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;addDisabled&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;addLabel&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The view renders a button when a consumer hands it &lt;code&gt;onAddToParty&lt;/code&gt; and renders nothing when it does not. It still imports no store, no contract, no action creator. The write crosses a domain boundary, so it arrives as a callback and leaves as a tap.&lt;/p&gt;

&lt;p&gt;The list app's container wires all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokemonDetailRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonDetailQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDispatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokemonDetailView&lt;/span&gt;
      &lt;span class="na"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refetch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onAddToParty&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addToParty&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spriteUri&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;addDisabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;partyReady&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;addLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Party is full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add to party&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read what that tap does. A screen owned by the Pokédex team dispatches an action the contract owns, and that action lands in a reducer owned by the party team. Three parties, none importing another's code. The disabled state reads the same &lt;code&gt;MAX_PARTY&lt;/code&gt; the reducer guards with, so the button and the cap can't drift. It also holds the button down until &lt;code&gt;state.party&lt;/code&gt; exists, so the one dispatch that could be lost is the one that can never be made. And the Pokédex header gets its counter through the identical read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partyCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PartySliceShape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;My Party &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;partyCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;MAX_PARTY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list app edits no version pin for any of this: it was on &lt;code&gt;^3.0.0&lt;/code&gt; for both packages, and &lt;code&gt;npm update @pokedex/contracts @pokedex/detail&lt;/code&gt; walks both carets to the new minors. Only the lagging app touched its &lt;code&gt;package.json&lt;/code&gt;. That is the caret working as intended: a minor arrives on request; a major takes a deliberate edit.&lt;/p&gt;

&lt;p&gt;The party's container is the other consumer of the same view, and it wires none of the three props. A Pokémon opened from inside the party shows no Add button at all:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm88fve6ant0m56nwc3kq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm88fve6ant0m56nwc3kq.webp" alt="The detail screen opened from the Party tab: the same shared view with no Add button, because the party's container wires no write"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One view, two consumers, one of them wiring a write. That single screenshot is the ownership essay's component rule, running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The party needs data of its own
&lt;/h2&gt;

&lt;p&gt;That party-side container is the wall promised earlier. Tapping a party member pushes &lt;code&gt;PokemonDetail&lt;/code&gt; inside the party's stack, and the container behind it needs &lt;code&gt;getPokemonDetail&lt;/code&gt;, which lives in the list app, which the party cannot import. Apps depend on contracts, never on each other, and this is the first time the rule costs something visible.&lt;/p&gt;

&lt;p&gt;The tempting exit is a shared data package: &lt;code&gt;@pokedex/data&lt;/code&gt;, owning the endpoint both apps need. It would delete the duplication, and for two features owned by one team it might even be right. The price is the one the &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;ownership essay&lt;/a&gt; priced: a shared package puts the party's data access on another team's release train. Compatible releases don't force anyone to move, but the day the party needs a fix in that package, or a breaking version lands, the party's ship date waits on another team's review and release. Twenty lines of query definition do not buy that coupling. The party writes its own &lt;code&gt;apps/party/src/detailApi.ts&lt;/code&gt; with the same endpoint name, the same &lt;code&gt;queryFn&lt;/code&gt; and the same &lt;code&gt;parsePokemonDetail&lt;/code&gt; from the contract, copied deliberately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="c1"&gt;// byte for byte, the list app's definition&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonDetailQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two apps now inject an endpoint named &lt;code&gt;getPokemonDetail&lt;/code&gt; into one &lt;code&gt;baseApi&lt;/code&gt;, and RTK notices. Run the app, open the Party tab, and the development console prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;called `injectEndpoints` to override already-existing endpointName getPokemonDetail without specifying `overrideExisting: true`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Left alone, RTK skips the second injection and keeps the first: loudly in development, silently in production, where that guard compiles out. This build declares the duplicate instead: both apps pass &lt;code&gt;overrideExisting: true&lt;/code&gt;, the console error goes away, and the &lt;em&gt;last&lt;/em&gt; injection wins. Either way load order decides, and neither app controls load order. That is the honest reason the two definitions have to stay identical rather than merely similar.&lt;/p&gt;

&lt;p&gt;The cache does not change: one endpoint name means one set of entries, so opening Bulbasaur from the Pokédex and then from the party costs one fetch total. The trap is drift: declared or not, whichever copy load order favours silently wins for both, and once &lt;code&gt;overrideExisting&lt;/code&gt; has silenced the console there is no warning left to ignore. The rule, then: keep the copies byte for byte identical, declare the collision so it is visibly deliberate, or name your endpoint something of your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;The claim to attack: the gate on &lt;code&gt;state.party&lt;/code&gt; is what stands between a slow chunk and a lost tap. First remove the gate (change the container's &lt;code&gt;addDisabled&lt;/code&gt; back to plain &lt;code&gt;full&lt;/code&gt;), then comment out the boot import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useEffect(() =&amp;gt; {&lt;/span&gt;
&lt;span class="c1"&gt;//   import('partyApp/partySlice')&lt;/span&gt;
&lt;span class="c1"&gt;//     .then(() =&amp;gt; store.dispatch(partyStateReady()))&lt;/span&gt;
&lt;span class="c1"&gt;//     .catch(err =&amp;gt; console.warn('party state module failed to load', err));&lt;/span&gt;
&lt;span class="c1"&gt;// }, []);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relaunch fresh, and do not open the Party tab. That matters: &lt;code&gt;PartyScreen&lt;/code&gt; imports &lt;code&gt;remove&lt;/code&gt; from the slice file, so visiting the tab injects the slice as a side effect and hides the bug. A user who goes straight to the Pokédex is how you reproduce it.&lt;/p&gt;

&lt;p&gt;Tap Bulbasaur. Tap &lt;strong&gt;Add to party&lt;/strong&gt;. The tap lands, the counter reads 0/6, and nothing else happens. No warning, no error, no console line. Post 6's break-it at least hung a spinner you could stare at. Here the dispatch reached the store, the store found no reducer registered for &lt;code&gt;party/add&lt;/code&gt;, and Redux did what Redux does with an unmatched action: nothing, by design. The user's tap reached a store with nothing registered to answer it, and Redux is behaving exactly as documented.&lt;/p&gt;

&lt;p&gt;Now say the sharper thing out loud. With the boot import gone, the slice still exists &lt;em&gt;eventually&lt;/em&gt;: one visit to the Party tab injects it, and every add after that works. So the failure has a narrower and nastier shape: every add made before the user happens to open the owner's tab is lost in silence, which reproduces for some users and never for others, depending on tab order. And the boot import alone only shrinks the window, from "until the user opens the Party tab" down to "until the chunk lands". It cannot close it, because the network owes you nothing.&lt;/p&gt;

&lt;p&gt;Closing it is the gate's job: put the gate back, leave the import commented out, and the same cold start shows an Add button that is simply disabled. Ugly, visible, and honest: a tap that cannot happen instead of a tap that lies.&lt;/p&gt;

&lt;p&gt;Restore the import, relaunch, and the button wakes the moment &lt;code&gt;partyStateReady&lt;/code&gt; fires; the same tap ticks the counter to 1/6. The pattern is all three pieces: the boot import for the head start, the marker to surface the slice, and the gate for the window neither can close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs, tests and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. A file you typed correctly is overwritten with itself, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-08-client-state /tmp/pokedex-ref-08
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-08/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sweep is also how the behaviour gets its proof: it carries the two regression tests this post's design earned (the party store walked through the vanish-inject-marker-add window, and the list container's Add gate held down until readiness), the mocks and Jest wiring they run on, and the ambient federation declarations the walkthrough summarised rather than printed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Packages published and installed, so the run is three dev servers and a simulator build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote      &lt;span class="c"&gt;# :8082&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote     &lt;span class="c"&gt;# :8083&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start                 &lt;span class="c"&gt;# :8081&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cold start lands on the Pokédex with the counter reading 0/6. Do not read that as proof the boot import worked: the tolerant selector returns 0/6 whether the slice is registered or still missing, which is the whole point of it. The proof is the Add button being enabled at all, since it only wakes once &lt;code&gt;partyStateReady&lt;/code&gt; has surfaced the slice. Add from a detail, watch the counter tick, and find the member sitting in the Party tab:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep adding to six and the cap arrives from both sides at once: the sixth add flips the open detail's button to a disabled state live, because the same selector that counts for the header counts for &lt;code&gt;addDisabled&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8pwg1s3939f7h1wt22f.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi8pwg1s3939f7h1wt22f.webp" alt="The detail screen at the party cap: the Add button disabled and relabelled Party is full"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remove a member in the Party tab and the freed slot goes dashed again, the counter drops everywhere at once, and the next add works. One slice, one owner, three surfaces agreeing because they all read the same state through the same shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;The app now owns something. The party lives in a slice the party app alone owns, registered at boot into a store the host wired around the contract's &lt;code&gt;rootReducer&lt;/code&gt;. Registered, not populated: &lt;code&gt;state.party&lt;/code&gt; stays absent until the next action reaches the combined reducer. The &lt;code&gt;partyStateReady&lt;/code&gt; marker is that action, and the tolerant read shape covers every moment before it fires. The one interaction that crosses (&lt;code&gt;addToParty&lt;/code&gt;, its cap, its read shape) ships versioned in the contract, the write reaches the shared detail view as a prop its consumer wires, and the party fetches its own data rather than borrow another team's release schedule. You have also watched the failure this design exists to prevent: a dispatch into a slice whose owner never loaded, dropped without a sound.&lt;/p&gt;

&lt;p&gt;One honest limitation stands: restart the app and the party is gone. The slice lives in memory, persistence is a different post's problem, and pretending otherwise would be the kind of quiet scope creep this series tries to avoid.&lt;/p&gt;

&lt;p&gt;The sharper question is the one this stack keeps raising. The party is six items and one rule, and it took a store, a contract minor, an injected reducer and a boot import to cross the seam politely. RTK made that crossing possible; it did not make it small. &lt;a href="https://warrendeleon.com/blog/state-stacks-under-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-client-state" rel="noopener noreferrer"&gt;Next, the series rebuilds this exact app on TanStack Query and Zustand&lt;/a&gt;, the stack most React Native teams reach for first, and watches what federation does to that choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/api/combineSlices" rel="noopener noreferrer"&gt;Redux Toolkit: &lt;code&gt;combineSlices&lt;/code&gt;&lt;/a&gt; — the injectable root reducer and &lt;code&gt;inject&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/api/createAction" rel="noopener noreferrer"&gt;Redux Toolkit: &lt;code&gt;createAction&lt;/code&gt;&lt;/a&gt; — prepare callbacks, and &lt;code&gt;nanoid&lt;/code&gt; shipping with RTK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/code-splitting" rel="noopener noreferrer"&gt;RTK Query: code splitting&lt;/a&gt; — &lt;code&gt;injectEndpoints&lt;/code&gt; and the &lt;code&gt;overrideExisting&lt;/code&gt; guard&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pokeapi.co/" rel="noopener noreferrer"&gt;PokéAPI&lt;/a&gt; — the free REST API the app fetches from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-08-client-state&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Who owns what: boundaries, teams and coupling in a federated React Native app</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 10 Aug 2026 00:09:24 +0000</pubDate>
      <link>https://dev.to/warrendeleon/who-owns-what-boundaries-teams-and-coupling-in-a-federated-react-native-app-5f30</link>
      <guid>https://dev.to/warrendeleon/who-owns-what-boundaries-teams-and-coupling-in-a-federated-react-native-app-5f30</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A plain React Native app usually avoids this argument, because the common shape is one team, one repo, one store. Nothing about React Native requires that; a monorepo with several teams and workspace packages has the same ownership questions without any federation involved. A screen is a folder, a shared component is an import, and the question of who owns a data definition has an answer too obvious to say out loud: the team does, because there is only one.&lt;/p&gt;

&lt;p&gt;Posts 4 to 6 of this series took that arrangement apart on purpose: &lt;a href="https://warrendeleon.com/blog/host-shell-federated-tabs-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;two tab features that build and ship on their own&lt;/a&gt;, a screen that travels between them as a versioned package, one store filled at runtime by code the shell has never seen. Every step forced an ownership call: where the shared screen lives, who may touch the store, where a data definition sits. Each call got a paragraph at the moment the tutorial made it. This post gathers those calls and argues them once, as a whole.&lt;/p&gt;

&lt;p&gt;It is written as a set of tests, not a set of verdicts. Each section states the reasoning and the conditions under which it holds; change the conditions (team count, trust between teams, release cadence, the size of the platform team) and some of the answers flip honestly. Take the tests away, hold them against your own org chart, and disagree with this series precisely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first test: do you need any of this?
&lt;/h2&gt;

&lt;p&gt;Every problem in this essay is bought, not inherited. A single-team app organised &lt;a href="https://warrendeleon.com/blog/feature-first-project-structure-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;feature-first&lt;/a&gt; already draws the boundaries that matter: a folder per domain, with the domain's screens, hooks and API calls inside it, and every crossing visible in review as an import. That model costs nothing to run: no registry, no version numbers, no second dev server. A boundary can be redrawn in an afternoon with a file move, and an emergency can cross one with an import and a TODO.&lt;/p&gt;

&lt;p&gt;Federation is what happens when the folders become teams. The folder model's weakness is the release train, not the boundaries: every folder still ships in one binary, on one date, gated by the slowest feature on it. When separate teams need separate release dates, the folder boundary has to become a deployment boundary, and every question in this essay follows from that promotion.&lt;/p&gt;

&lt;p&gt;So the first test is the cheapest one, and it gates the rest.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The gate.&lt;/strong&gt; Does more than one team ship this app? If not, stop at folders. Every test in this essay assumes the answer is yes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://warrendeleon.com/blog/why-module-federation-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;first post of this series&lt;/a&gt; weighs the trade in full and lands where this paragraph does: federation converts organisational friction into technical machinery, and an app without the friction pays for machinery it never uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  A boundary is a team, not a screen
&lt;/h2&gt;

&lt;p&gt;When the Pokémon detail screen needed a home both tabs could reach, the federated answer suggested itself: a third remote, on its own port, declared in both consumers' maps. &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;Post 5&lt;/a&gt; gave that option a fair hearing: it works mechanically, Module Federation resolves nested remotes without complaint, and the screen stays updatable without touching either consumer. The answer was still no, in the sentence this whole essay grows from: a federation boundary is a deployable, and teams cut deployables along domains.&lt;/p&gt;

&lt;p&gt;The refusal is about running costs. A remote carries costs a folder never does: a dev server to run, a pipeline to keep green, a version to publish, and a failure mode of its own when its bundle fails to arrive over the network. Those costs are fixed per boundary, and a team absorbs them as overhead on a domain it was going to own anyway. A screen has no team behind it. Cut a screen loose as its own deployable and the overhead turns up while the owner never does; grant that status once and there is no principled place to refuse the next screen. The end of that road is &lt;strong&gt;an app per screen&lt;/strong&gt;, and a release process that is all coordination and no shipping.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership#a-boundary-is-a-team-not-a-screen" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the screen shipped as &lt;code&gt;@pokedex/detail&lt;/code&gt; instead: a package on a registry, installed by both tabs, mounted by each inside its own stack. The tabs stay the deployables; the screen is a dependency. This is &lt;a href="https://www.melconway.com/Home/Committees_Paper.html" rel="noopener noreferrer"&gt;Conway's law&lt;/a&gt; used deliberately: if the system is going to mirror the communication structure anyway, draw the module lines where the team lines already are.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 1&lt;/span&gt;&lt;span&gt;Does this boundary have a team behind it?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;The test cuts in both directions: it refuses remotes to screens, and it grants them to domains that have outgrown their folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shared component renders what it is given
&lt;/h2&gt;

&lt;p&gt;Web micro-frontends have &lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;a respectable pattern&lt;/a&gt; in which a shared fragment arrives smart: it fetches its own data, owns its own loading states, and drops into any host as a finished feature. The appeal is real. One team keeps pixels and data behind one seam, consumers write a single line, and a data fix rolls out with no consumer changing code. Plenty of web micro-frontend setups run this way coherently, at web deployment cadence.&lt;/p&gt;

&lt;p&gt;The price is in what the fragment has to carry. A component that fetches carries an opinion about every layer beneath it: an HTTP client, a cache, a retry policy, an auth story. Every consumer inherits those opinions without seeing them. Two such fragments on one screen can disagree about all of it. On mobile the inheritance weighs more, because whatever the fragment carries is duplicated into a bundle a phone has to download.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;Post 6&lt;/a&gt; drew the line the other way. &lt;code&gt;PokemonDetailView&lt;/code&gt; takes a Pokémon, a loading flag, an error flag and a retry callback, and renders them. That is the whole surface, and the whole arrangement fits in the consumer's container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonDetailQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokemonDetailView&lt;/span&gt; &lt;span class="na"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refetch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Going live added no dependencies to the package at all: the same four peers as the static version (the contract, React, React Native, the safe-area context) and Redux nowhere among them, because where the data comes from is the app's business and what it looks like is the library's.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 2&lt;/span&gt;&lt;span&gt;Does this component know where its data comes from?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;A yes is worth stopping on. It can be the right call, for a widget a platform team owns end to end and every consumer wants identical. It is misdrawn when the fetching is incidental to what the component is for, because then what is being shared is a piece of one team's app wearing a component's name.&lt;/p&gt;

&lt;p&gt;The practical check takes a minute: render it in a bare app with hardcoded props. Every provider, client or store it demands before it renders is a piece of one app's runtime the package now asks every consumer to rebuild. A theme context is a reasonable ask; a query client wired to one team's endpoints is an app wearing a component's name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data definitions live with their domain
&lt;/h2&gt;

&lt;p&gt;That last release split the screen in two, the view in a package and the fetch in an app, and the split had to put the endpoint definition somewhere. The somewhere is the rule. &lt;code&gt;getPokemonDetail&lt;/code&gt; landed in the list app, next to &lt;code&gt;getPokemonList&lt;/code&gt;, because Pokémon data belongs to the Pokédex domain and the list app is where that domain lives. The view is fed the result as props and never learns the endpoint exists.&lt;/p&gt;

&lt;p&gt;The counter-instinct says data code is infrastructure and belongs with the rest of the infrastructure, down in the shared layer. The series splits that claim in two. &lt;code&gt;baseApi&lt;/code&gt;, the HTTP layer with its cache and tag machinery, genuinely is infrastructure: it carries no &lt;strong&gt;domain knowledge&lt;/strong&gt;, and every team already depends on it the way they depend on React, so it lives in the contract package and everyone couples to it. A feature endpoint is different. It encodes what one domain fetches, when, and what it exposes, and whoever owns that definition ends up owning the questions, the bugs and the migrations that come with it. Move a domain's endpoints into a shared layer and the layer's owners slowly become the owners of every domain's data plumbing, without the domain knowledge that would make the job possible.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 3&lt;/span&gt;&lt;span&gt;Does this definition live with its domain?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;An endpoint (what a domain fetches and when) travels with the domain. Machinery with no domain knowledge is shared. Models and parsers sit in between, and the split follows the backend: the schema for a wire format every consumer already depends on lives in the contract package (Test 4's logic), while a schema only one domain reads stays with that domain, which is why this series parses PokéAPI's REST payloads in the contract and post 10 keeps its GraphQL parse inside the list app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplication is cheaper than a dependency between teams
&lt;/h2&gt;

&lt;p&gt;In the next post the party remote starts holding state of its own, and its first need is Pokémon data for a tapped slot: data the Pokédex domain already knows how to fetch. The tidy instinct proposes a shared data package: one definition, two consumers, no drift. The series does the untidy thing instead and has the party team write its own endpoint file. It comes to about twenty lines and looks like a failure of housekeeping. The file it duplicates, the Pokédex's own, trimmed to its shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`pokemon/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count what the tidy version costs. A data package shared between two feature teams is &lt;strong&gt;a dependency between peers&lt;/strong&gt;. A compatible release lands quietly, because consumers sit on a caret and adopt when they next install. But the changes worth making to a shared definition are not all compatible, and the day one lands breaking, every consumer has to be moved before the old version can go. Releases chain: bump the package, wait for the other team to take the bump, then ship the thing you actually wanted to ship. And the package itself needs an owner: one of the two teams answers for its bugs, or nobody does. A shared definition between peers does not remove the coordination; it converts drift you can see into scheduling you have to do.&lt;/p&gt;

&lt;p&gt;Post 5's loose peer range let one team's app pair an old contract with a new screen, every compiler stayed green, and a user got a sentence with a hole in it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8up7q9gcd4ooiaqmsje1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8up7q9gcd4ooiaqmsje1.webp" alt="The Pokémon detail screen reading 'Opened from the' with nothing after it, because the list app holds an older contract version and never passed the field the newer screen renders" width="600" height="1305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Twenty duplicated lines cost twenty lines. Each copy follows its own team's needs and ships on its own team's dates, and the drift between them stays visible, because both parse the same wire format from the same backend: the copies can diverge in what they keep and how they shape it, but a divergence shows up against the shared payload rather than hiding behind two different ones. When the duplication grows past a file (five endpoints, ten), the answer changes, and &lt;em&gt;The coupling test&lt;/em&gt; says how to tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The coupling test
&lt;/h2&gt;

&lt;p&gt;The duplication rule needs a limit, because teams couple to shared code all the time and are right to: React, the navigation runtime, &lt;code&gt;baseApi&lt;/code&gt;, the contract types. The line between those and the peer data package is the most reusable test in this essay.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 4&lt;/span&gt;&lt;span&gt;Does this dependency already exist, or does sharing invent it?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;Couple to what you already depend on; refuse the dependency that sharing itself creates.&lt;/p&gt;

&lt;p&gt;A client generated from the backend's API specification is fine to share, because it couples its consumers to the backend, a dependency every one of them &lt;em&gt;already&lt;/em&gt; has. When the specification changes incompatibly, every consumer was affected the moment it changed, and the generated client surfaces the fact at build time instead of in production; an additive change waits politely for each consumer to care. The contract package passes the same test: params and module shapes are agreements the apps were already bound by implicitly, written down where every compiler can see them. The peer data package fails it. Nothing about the party domain depended on the Pokédex team's release schedule before the shared package existed; the package is what would create that dependency.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership#the-coupling-test" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The pull of the shell
&lt;/h2&gt;

&lt;p&gt;Every rule so far pushes code outward, into domains and packages. One argument pushes the other way, and it deserves the longest hearing in the essay, because it is usually made by the most careful engineer in the room.&lt;/p&gt;

&lt;p&gt;Endpoints multiply. Several teams fetch overlapping data. Someone proposes the obvious consolidation: move the shared capability up into the shell, where the platform team can hold one definition of everything. One HTTP layer, one auth story, one set of data definitions, consistent behaviour by construction, drift impossible. Nobody proposes this cynically. On pure technical grounds it delivers exactly what it promises, and one definition really is easier to reason about than four copies.&lt;/p&gt;

&lt;p&gt;The costs arrive from two directions. The first is organisational, and it lands before any code is written: escalating a capability to the shell makes the platform team &lt;strong&gt;a precondition&lt;/strong&gt;. A feature team that needs a shell-level change cannot start until another team schedules the work, builds it and ships it. Its start date now sits inside someone else's sprint. Multiply that by every team with a request, and the platform team becomes a queue in front of the whole programme, while inheriting, request by request, domain knowledge it never owned and cannot keep current. The word to sit with is &lt;em&gt;start&lt;/em&gt;: the feature team is stopped, and it stays stopped until a team with different priorities ships.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Test 5&lt;/span&gt;&lt;span&gt;Can the feature team start without another team shipping first?&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;p&gt;Escalating a capability to the shell fails this one by design.&lt;/p&gt;

&lt;p&gt;The second cost is specific to mobile: &lt;strong&gt;the shell is the binary&lt;/strong&gt;, so shell code ships at App Store cadence. Build, submit, review, staged rollout, then a long tail of users who never update at all.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A capability moved into the shell moves onto the App Store train, and a schema tweak that rides a store review is the exact cost this series adopted federation to escape. Escalation reintroduces it quietly, one capability at a time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The platform layer genuinely earns one kind of code, whether it rides in the shell binary or in a platform-owned package like the contract: the slow-moving kind. The HTTP client, auth, &lt;code&gt;baseApi&lt;/code&gt;'s machinery, observability, the runtimes every remote resolves against. The pattern across that list: capability that changes slowly, carries no domain knowledge, and was already depended on by everyone. The list grows rarely, and each addition needs an argument, because everything on it inherits the binary's cadence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inner source: the workable middle
&lt;/h2&gt;

&lt;p&gt;Between "every team waits on the platform team" and "no shared capability at all" there is a middle that large orgs have a name for: &lt;a href="https://innersourcecommons.org/" rel="noopener noreferrer"&gt;inner source&lt;/a&gt;. Run the shared code like an open-source project that happens to be &lt;em&gt;internal&lt;/em&gt;: the shell, the contract package, a component library, a shared data layer if your org decides it wants one. The platform team are the maintainers and code owners. Product teams send pull requests.&lt;/p&gt;

&lt;p&gt;The blocking mechanism changes shape rather than disappearing. A feature team that needs a shell-level change writes the change itself, against the shared repo's contribution rules, and waits for a review instead of a roadmap slot. The platform team's job shifts from build-everything to review-and-steward: it holds the bar on quality and coherence and stops being the queue. The Pokédex team adding a component to the shared library becomes a pull request, not a ticket in another team's backlog. So does the party team's schema addition, if a shared data layer is the choice your org made after all.&lt;/p&gt;

&lt;p&gt;The costs are real. Review latency does not vanish; a contested pull request can wait as long as a roadmap slot ever did. The platform team ends up maintaining code it did not write, a genuine burden the day the contributing team moves on. And the model only works on real contribution infrastructure:&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Before the first PR&lt;/span&gt;&lt;span&gt;What inner source runs on&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;Contribution conventions a stranger can follow, written down&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;CI a contributing team can run without asking anyone&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;Ownership files that route each review to the right people&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;Inner source pays for itself when contributions come often enough to justify that overhead.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Just open a PR” against an undocumented repo is a politeness, not a process. Without the contribution infrastructure, inner source is the queue with a friendlier name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The tests, and what moves the answers
&lt;/h2&gt;

&lt;p&gt;The whole argument fits on one map, each box holding what it owns and the cadence it ships at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership#the-tests-and-what-moves-the-answers" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Five tests, in the order the series met them:&lt;/p&gt;

&lt;p&gt;&lt;span&gt;The five tests&lt;/span&gt;&lt;span&gt;Who owns what&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this boundary have a team behind it?&lt;/strong&gt; Domains get remotes; screens get packages.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;2&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this component know where its data comes from?&lt;/strong&gt; A shared component renders what it is given; data arrives as props.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this definition live with its domain?&lt;/strong&gt; Machinery is shared; endpoints travel with the domain that owns the data. Parsers and schemas are the judgement call, and this build splits them by who reads them: the REST schemas every app depends on sit in the contract, while the GraphQL schema only the list reads stays in the list app.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;4&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Does this dependency already exist, or does sharing invent it?&lt;/strong&gt; Couple to the backend you already depend on; refuse the peer package that manufactures a schedule dependency between teams.&lt;/span&gt;&lt;br&gt;
  &lt;span&gt;5&lt;/span&gt;&lt;span&gt;&lt;strong&gt;Can the feature team start without another team shipping first?&lt;/strong&gt; If not, the capability sits at the wrong level, or the process around it does.&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;All five sit behind the gate: more than one team ships this app. If not, folders answer every one of them for free.&lt;/p&gt;

&lt;p&gt;None of these produce one answer for every org; they tell you what each option costs in yours. Two teams that trust each other and release together can share a data package and barely feel the coupling. An app with three teams and a quarterly cadence might keep everything in the shell and never notice the price. If your org cut these lines differently and the seams hold, that is a different answer to the same tests, and worth defending on its own terms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;The next post&lt;/a&gt; puts the tests straight to work. The party grid has been empty since post 4, because nothing in the app can reach it. The party team is about to fill it with &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-ownership" rel="noopener noreferrer"&gt;state it owns&lt;/a&gt;, and the first decisions on the way are exactly the ones this essay just argued: who owns the slice, where the endpoint file goes, what the contract carries. The rules stop being prose and start being files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;Micro Frontends&lt;/a&gt; — Cam Jackson's survey, including the self-contained fragment pattern this essay argues against for mobile&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.melconway.com/Home/Committees_Paper.html" rel="noopener noreferrer"&gt;Conway's law&lt;/a&gt; — the 1968 paper: systems mirror the communication structures of the organisations that build them&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://innersourcecommons.org/" rel="noopener noreferrer"&gt;InnerSource Commons&lt;/a&gt; — the practice, its patterns, and the contribution infrastructure it depends on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://teamtopologies.com/" rel="noopener noreferrer"&gt;Team Topologies&lt;/a&gt; — Skelton and Pais on platform teams and stream-aligned teams, the vocabulary behind the shell debate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo whose decisions this essay defends&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>architecture</category>
      <category>engineeringmanagement</category>
    </item>
    <item>
      <title>One shared store: server state across federated remotes in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:07:47 +0000</pubDate>
      <link>https://dev.to/warrendeleon/one-shared-store-server-state-across-federated-remotes-in-react-native-4b5o</link>
      <guid>https://dev.to/warrendeleon/one-shared-store-server-state-across-federated-remotes-in-react-native-4b5o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;React Native Module Federation&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/react-native-module-federation/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://warrendeleon.com/blog/typing-the-seam-between-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;last post&lt;/a&gt; ended with a sentence with a hole in it: a screen rendering "Opened from the " and then nothing, because a type checked at build time is no help against a value that only shows up at runtime. It closed on a promise, too: real data, one store, shared across the remotes. This post keeps the promise, and the hole gets its proper answer along the way.&lt;/p&gt;

&lt;p&gt;It leans on &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;server state and client state&lt;/a&gt; from the short break in the series. The split between data a server owns and data the app owns is assumed here, not re-explained. This post is about one half of it, server state, under federation: one Redux Toolkit (RTK) store in the host, one RTK Query cache, and the Pokédex domain filling it with both its endpoints, while the installed &lt;code&gt;@pokedex/detail&lt;/code&gt; view renders what it is fed. Live PokéAPI data replaces every hardcoded Pokémon in the app, both drifted copies included.&lt;/p&gt;

&lt;p&gt;The shape we're building, before any code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📊 &lt;strong&gt;Diagram:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;view it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The one thing to hold in your head: &lt;code&gt;baseApi&lt;/code&gt; is a single object, and every side imports the exact same one. That is what makes the cache shared. Break that, and the app breaks in a way worth seeing, so we will break it on purpose near the end.&lt;/p&gt;

&lt;p&gt;Carry on from your own post 5 code if you built along. Otherwise, start from its finished state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/warrendeleon/react-native-module-federation
&lt;span class="nb"&gt;cd &lt;/span&gt;react-native-module-federation
git checkout post-05-contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The contract package grows a runtime seam
&lt;/h2&gt;

&lt;p&gt;So far &lt;code&gt;@pokedex/contracts&lt;/code&gt; has held only types. Every export was erased at build, so nothing from it reached a bundle. Now it gains its first runtime export: the RTK Query API object the whole app fetches through.&lt;/p&gt;

&lt;p&gt;The host looks like the natural home for it. The host owns the store, the store owns the cache, and an API object next to the store it feeds is where a single app would put it. In a single app that instinct is right. Under federation it fails on identity: a consumer can only add endpoints to the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;baseApi&lt;/code&gt; instance the host store wired in, and an instance that lives inside the host's source is one nothing else can import. The contract package is the one module every side already resolves to a single copy of, because it is a Module Federation singleton. Put the instance there and a consumer's &lt;code&gt;baseApi.injectEndpoints({...})&lt;/code&gt; registers against the one cache and middleware the store already runs. One instance means one HTTP cache, one deduplication pipeline, one tag graph across every feature, including features shipped long after the shell. &lt;code&gt;packages/contracts/src/api.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchBaseQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit/query/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducerPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fetchBaseQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://pokeapi.co/api/v2/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;tagTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PokemonSummary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokemonListResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// The url is not just any string: the row's id is derived from it, so the schema&lt;/span&gt;
  &lt;span class="c1"&gt;// polices the trailing numeric id a PokéAPI resource url carries.&lt;/span&gt;
  &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\/?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;artworkUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;idFromResourceUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\/?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PokéAPI resource URL has no trailing id: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PokéAPI resource URL id is out of range: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PokemonSummary&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PokemonListResponseSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;idFromResourceUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;artworkUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailResponseSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;spriteUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;artworkUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createApi&lt;/code&gt; with no endpoints builds an empty shell: a reducer, some middleware, and an &lt;code&gt;injectEndpoints&lt;/code&gt; method the consumers will call. &lt;code&gt;fetchBaseQuery&lt;/code&gt; is a small wrapper over &lt;code&gt;fetch&lt;/code&gt; that prepends the base URL and parses JSON. &lt;code&gt;tagTypes&lt;/code&gt; names the one label this app invalidates by; it does nothing yet, and does real work in the last section.&lt;/p&gt;

&lt;p&gt;The two parse functions are the part post 5's broken sentence pointed at. A type is a compile-time promise, and it is gone by the time a response actually lands. A renamed field or a &lt;code&gt;null&lt;/code&gt; where a string used to be slips straight through a hand-written cast and crashes three screens later. So each raw response is validated with a &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; schema right at the seam, and a bad shape becomes a value the screen can handle instead of a crash.&lt;/p&gt;

&lt;p&gt;The seam has to police everything it depends on, which is why the list schema checks the url's trailing id rather than just &lt;code&gt;z.string()&lt;/code&gt;: the id is derived from that url, and a lenient check here would let a malformed payload through as a phantom Pokémon #0. &lt;code&gt;idFromResourceUrl&lt;/code&gt; throws for the same reason: a helper that quietly invents an id on bad input is a parser that passes malformed data instead of failing it. The range check earns its two lines too, since a trailing digit run is not yet an id. &lt;code&gt;/pokemon/0/&lt;/code&gt; names nothing, and a digit string long enough to leave the safe-integer range has stopped being the number printed in the url.&lt;/p&gt;

&lt;p&gt;The detail schema keeps only what the detail screen renders; the full PokéAPI payload for one Pokémon is close to 300 KB of JSON, and parsing fields nothing displays would just be a bigger surface to break on. Runtime validation gets a post of its own later in the series.&lt;/p&gt;

&lt;p&gt;A runtime export and two new peer dependencies are a breaking change, so the version takes a major bump. The number itself needs a glance at your own registry first: post 5's ladder already published &lt;code&gt;1.1.0&lt;/code&gt; and &lt;code&gt;2.0.0&lt;/code&gt;, and a published number is spent for good, because the registry refuses to reuse it and every install relies on that refusal. So the runtime seam ships as &lt;code&gt;3.0.3&lt;/code&gt;. The major is the story; the trailing patch is parser hardening that landed after first publication. &lt;code&gt;packages/contracts/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@pokedex/contracts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.0.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.25.76"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"peerDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@reduxjs/toolkit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=2.10.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react-redux"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=9"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;zod&lt;/code&gt; is a real runtime dependency, so it ships inside the package. &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; are peers: the apps install them, and the contract uses their copies rather than bundling its own. Install the package's dev dependencies and publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages/contracts
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;+ @pokedex/contracts@3.0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caret does here what post 5 showed it doing. Every consumer sits on &lt;code&gt;^1.0.0&lt;/code&gt;, so &lt;code&gt;npm install&lt;/code&gt; leaves all three apps exactly where they are even with &lt;code&gt;3.0.3&lt;/code&gt; on the registry. Picking up the new contract is a deliberate move, one app at a time, and one of the three stays put on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  One store in the host
&lt;/h2&gt;

&lt;p&gt;The host gains a store. &lt;code&gt;apps/host/src/store.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;configureStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;combineSlices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDefaultMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getDefaultMiddleware&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppDispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The split of ownership is the point of the file.&lt;/strong&gt; The store lives in the host: one store per app is the shell's job, like the tab bar. The API instance the store is built around lives in the contract package, for the identity reason above. The host owns the wiring; the seam owns the instance.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;baseApi.middleware&lt;/code&gt; is load-bearing. It runs the cache lifecycle: fetching, deduplication, tag invalidation, cache eviction. Leave it off the store and the first screen to run a query hook throws a full-screen red box in development, with RTK naming the mistake outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: Middleware for RTK-Query API at reducerPath "api" has not been added to the store.
You must add the middleware for RTK-Query to function correctly!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpccfmct7awyqtkxr6xbr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpccfmct7awyqtkxr6xbr.webp" alt="A full-screen React Native error box: middleware for RTK-Query API at reducerPath 'api' has not been added to the store, with the component stack pointing into the list remote's federated chunk"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Worth pausing on the component stack in that capture: the crash site is &lt;code&gt;PokedexScreen&lt;/code&gt; inside &lt;code&gt;__federation_expose_ListStack.chunk.bundle&lt;/code&gt;. A remote's screen, in a chunk the host downloaded at runtime, hits an error caused by one missing line in the host's own store file. The failure crosses the module boundary in the opposite direction to everything else in this series. A loud failure, at least. Keep it in mind, because the &lt;em&gt;Now break it&lt;/em&gt; sabotage gets no error at all.&lt;/p&gt;

&lt;p&gt;The store goes in over the whole tree, so everything federated into it can read the cache. &lt;code&gt;apps/host/App.tsx&lt;/code&gt;, the wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* the tab navigator from post 4 */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;NavigationContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SafeAreaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remotes never create or import a store. They render inside this tree and reach the store through the shared &lt;code&gt;react-redux&lt;/code&gt; singleton, the same way they reached the shared safe-area context back in post 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Share the state trio
&lt;/h2&gt;

&lt;p&gt;For anything outside the host to inject into the shared &lt;code&gt;baseApi&lt;/code&gt;, three packages have to resolve to one copy at runtime: &lt;code&gt;@reduxjs/toolkit&lt;/code&gt;, &lt;code&gt;react-redux&lt;/code&gt;, and &lt;code&gt;@pokedex/contracts&lt;/code&gt; itself. They join the host's &lt;code&gt;shared&lt;/code&gt; map as eager singletons, and post 5's hard-won lesson applies to two of them straight away. &lt;code&gt;apps/host/rspack.config.mjs&lt;/code&gt;, the additions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rtkPkg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;reactReduxPkg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux/package.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ...in the shared map:&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rtkPkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reactReduxPkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A package resolved through an &lt;code&gt;exports&lt;/code&gt; map needs its &lt;code&gt;version&lt;/code&gt; stated by hand.&lt;/strong&gt; Both Redux packages do, and &lt;code&gt;@pokedex/contracts&lt;/code&gt; does not. Post 5 found this in the same setup: rspack reads the version out of the package it is sharing, and skips the provide for a package resolved through an &lt;code&gt;exports&lt;/code&gt; map. It held on these versions, in this configuration, which is the claim being made. &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; both carry an &lt;code&gt;exports&lt;/code&gt; map, so without the explicit &lt;code&gt;version&lt;/code&gt; neither would land in the share scope. The contract package has no &lt;code&gt;exports&lt;/code&gt; map, so it needs nothing. The bundle-grep from post 5 confirms all three provides register, contract included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"@pokedex/contracts"&lt;/span&gt;, version: &lt;span class="s2"&gt;"3.0.3"&lt;/span&gt;
&lt;span class="s2"&gt;"@reduxjs/toolkit"&lt;/span&gt;, version: &lt;span class="s2"&gt;"2.12.0"&lt;/span&gt;
&lt;span class="s2"&gt;"react-redux"&lt;/span&gt;, version: &lt;span class="s2"&gt;"9.3.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list remote mirrors the same three entries without &lt;code&gt;eager&lt;/code&gt;: the host provides the copies, the remote consumes them. This is the first time &lt;code&gt;@pokedex/contracts&lt;/code&gt; appears in any &lt;code&gt;shared&lt;/code&gt; map. Until now it was types-only, erased at build, so there was nothing to share. Now it carries &lt;code&gt;baseApi&lt;/code&gt;, and one instance is the whole point.&lt;/p&gt;

&lt;p&gt;Two of the apps add the packages and bump the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @reduxjs/toolkit@2.12.0 react-redux@9.3.0 @pokedex/contracts@3.0.3 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; @reduxjs/toolkit@2.12.0 react-redux@9.3.0 @pokedex/contracts@3.0.3 &lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The party app is the third, and it gets nothing. No Redux packages, no new shared entries, and both of its installed packages stay where they are: the contract on &lt;code&gt;^1.0.0&lt;/code&gt; (two majors behind the seam it types against) and the detail screen on &lt;code&gt;^1.0.0&lt;/code&gt;, the static version. That is safe for a precise reason: the party app consumes only types from the contract, and types are erased at build. A runtime major changes what ships in the package's JavaScript; a consumer that never imports any of that JavaScript has nothing to break. Remotes opt into shared state; the shell forces nothing on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The list remote: real data
&lt;/h2&gt;

&lt;p&gt;Now the first injection. &lt;code&gt;apps/list/src/listApi.ts&lt;/code&gt;, a new file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PokemonSummary&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonSummary&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pokemon?limit=151&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CUSTOM_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid PokéAPI response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;providesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonListQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;listApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;injectEndpoints&lt;/code&gt; adds &lt;code&gt;getPokemonList&lt;/code&gt; to the shared &lt;code&gt;baseApi&lt;/code&gt; and hands back a typed hook. The endpoint fetches the first 151 Pokémon in one request, hands the raw body to &lt;code&gt;parsePokemonList&lt;/code&gt;, and returns either the shaped rows or a caught error. &lt;code&gt;providesTags: ['PokemonList']&lt;/code&gt; stamps the result with the label the host will invalidate by. The shell knew nothing about this endpoint when it was built; the remote adds it to the running store the first time its code loads.&lt;/p&gt;

&lt;p&gt;The screen drops its hardcoded five-row array and reads the hook. &lt;code&gt;apps/list/src/PokedexScreen.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokedexScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSafeAreaInsets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useNavigation&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NativeStackNavigationProp&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ListParamList&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonListQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;centre&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityIndicator&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;centre&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Couldn't reach PokéAPI.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retry&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;refetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Try again&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlatList&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;contentContainerStyle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paddingBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;insets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonDetail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spriteUri&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sprite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;#&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three states instead of one: a spinner while the request is in flight, an error screen with a retry when PokéAPI is unreachable, and the list. The navigation from post 5 is untouched: tapping a row still pushes &lt;code&gt;PokemonDetail&lt;/code&gt; with &lt;code&gt;{ id }&lt;/code&gt; inside this remote's own stack. What changed is where the rows come from: a cache in the host's store, filled by an endpoint this remote injected, read through a hook that did not exist when the host shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail goes live; the library stays a view
&lt;/h2&gt;

&lt;p&gt;Post 5 left a deliberate smell in the detail package: its own private copy of the Pokémon data, drifted from the list's copy, with a comment promising that live data would delete it. This is that moment, and it takes two releases that draw one line.&lt;/p&gt;

&lt;p&gt;The line: a component library ships pixels, and data definitions belong to the domain that owns the data. &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;The next post argues that line in full&lt;/a&gt;; this one applies it. Pokémon data is the Pokédex domain's, so the second endpoint lands next to the first, in the list app. &lt;code&gt;apps/list/src/detailApi.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectEndpoints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;getPokemonDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PokemonDetail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`pokemon/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePokemonDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CUSTOM_ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid PokéAPI response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonDetailQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;detailApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query that takes the id as its argument, fetches one Pokémon, parses it at the seam. No tags: nothing invalidates a single Pokémon yet.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pokedex/detail&lt;/code&gt; goes to &lt;code&gt;3.0.0&lt;/code&gt;: a major, because the props change shape entirely. The static copy and its lookup are gone; what remains is a view: &lt;code&gt;PokemonDetailView&lt;/code&gt; takes the Pokémon and the three states as props, renders them, and does nothing else. The list app composes hook and view in a small container on its detail route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PokemonDetailView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetPokemonDetailQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./detailApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PokemonDetailRoute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DetailParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetPokemonDetailQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PokemonDetailView&lt;/span&gt; &lt;span class="na"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refetch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the data comes from is the app's business; what it looks like is the library's. Nothing in the package touches the store, and going live added no dependencies to it at all: the same four peers the static version had, Redux nowhere among them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Count the cost of going live:&lt;/strong&gt; one endpoint file in the domain that owns the data, one honest major on the view, and a container of four lines. And the quieter payoff: the list's copy of the data and the package's copy have both gone, so the drift between them has gone too. Both screens now render whatever PokéAPI says, through one cache, and disagreeing with each other is no longer something they know how to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now break it
&lt;/h2&gt;

&lt;p&gt;The claim is that one shared instance holds it all together. The fastest way to trust that is to remove it and watch.&lt;/p&gt;

&lt;p&gt;Delete the &lt;code&gt;@pokedex/contracts&lt;/code&gt; entry from the &lt;code&gt;shared&lt;/code&gt; map in both configs that carry it (host and list), leaving &lt;code&gt;@reduxjs/toolkit&lt;/code&gt; and &lt;code&gt;react-redux&lt;/code&gt; in place. Restart the dev servers and relaunch.&lt;/p&gt;

&lt;p&gt;It spins. Forever. No crash, no red box, and this time nothing in the console either. Twenty seconds in, the only line the dev server has logged is the app starting up. Watch it for as long as you like: nothing else arrives.&lt;/p&gt;

&lt;p&gt;The middleware warning from the store section would seem the natural failure here, and the reason it never fires is the whole lesson. With the contract no longer shared, the host bundles its own copy of &lt;code&gt;@pokedex/contracts&lt;/code&gt; and the list remote bundles a separate one. Two copies means two &lt;code&gt;baseApi&lt;/code&gt; objects. The host store wired the reducer and middleware of &lt;em&gt;its&lt;/em&gt; copy, so from where RTK stands the setup is complete and healthy: nothing missing, nothing to warn about. Both of the domain's endpoints registered in the list's copy, one no store ever wired, so the endpoints exist, the hooks run, and the fetches they should trigger go nowhere. Each copy is internally consistent. The mistake sits between them, and nothing at runtime owns "between".&lt;/p&gt;

&lt;p&gt;This is the quiet failure the shared-singleton posts keep returning to, and it now has a full ladder. Two Reacts crash on launch. A missing middleware throws a red box that names the file to fix. Two &lt;code&gt;baseApi&lt;/code&gt;s give you a spinner over a cache that never fills, and the only diagnostic is the absence of everything else. A loud failure is cheap; the silent one costs the afternoon. Put the shared entries back, restart, and the list fills again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidate across the seam
&lt;/h2&gt;

&lt;p&gt;The tag graph has been sitting unused since the contract declared it. Time to pull it.&lt;/p&gt;

&lt;p&gt;Post 4 hid every header with &lt;code&gt;headerShown: false&lt;/code&gt; on the tab navigator, and post 5 gave each remote's stack its own headers inside the tab. The host now turns the Pokédex tab's header back on, because it is about to put something host-owned in it. &lt;code&gt;apps/host/App.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDispatch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pokedex/contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RefreshButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDispatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateTags&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;hitSlop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;accessibilityRole&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
      &lt;span class="na"&gt;accessibilityLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Refresh Pokédex"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshText&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Refresh&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getFocusedRouteNameFromRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-navigation/native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Screen&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pokédex"&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PokedexTab&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;headerShown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getFocusedRouteNameFromRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PokemonDetail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headerRight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefreshButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail in there deserves a closer look. The detail route brings its own stack header with a back button, and a tab header stacked on top of it would put two bars on screen. So the host hides its bar while the stack sits on the detail, and shows it the moment the stack pops back.&lt;/p&gt;

&lt;p&gt;Checking against &lt;code&gt;'PokemonDetail'&lt;/code&gt; looks like the host reaching into the remote's internal route names, and that is exactly what it would be if the name lived in the remote. But it doesn't. It is the route name from &lt;code&gt;DetailParamList&lt;/code&gt; in &lt;code&gt;@pokedex/contracts&lt;/code&gt;, the same agreement the params come from. &lt;strong&gt;Be honest about how much that buys:&lt;/strong&gt; the host writes the string &lt;code&gt;'PokemonDetail'&lt;/code&gt; by hand, and TypeScript does not connect a bare string to the contract's key. Rename the route in the contract and this comparison keeps compiling while it quietly stops matching. The agreement is real; the compiler is not enforcing this end of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RefreshButton&lt;/code&gt; needs &lt;code&gt;Pressable&lt;/code&gt; and &lt;code&gt;Text&lt;/code&gt; added to the &lt;code&gt;react-native&lt;/code&gt; import, plus two small style entries; the complete file is in the companion tag.&lt;/p&gt;

&lt;p&gt;The host never defined &lt;code&gt;getPokemonList&lt;/code&gt;. It holds no reference to the list remote's endpoint, its hook, or its query. All it dispatches is a tag. &lt;code&gt;invalidateTags(['PokemonList'])&lt;/code&gt; walks the shared cache, finds every query that provided that tag, and refetches the ones with a subscriber on screen. The list refetches; the detail endpoint, which provides no tags, is untouched. One label, declared in the contract, provided by one feature, dispatched by the host, and the refetch lands in code the dispatcher has never seen.&lt;/p&gt;

&lt;p&gt;In a real app the invalidation hangs off a mutation's &lt;code&gt;invalidatesTags&lt;/code&gt; rather than a button, but the reach across the module boundary is the same. Teams agree on tag names in the contract the way they agree on types, and that agreement is the entire refresh protocol between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Land exactly on this post's finished state.&lt;/strong&gt; The walkthrough prints the load-bearing files; manifests, configs, tests and the smaller edits live in the tag. To finish with a tree byte-equal to the tag, sweep the reference copy over yours. A file you typed correctly is overwritten with itself, and the sweep fills in what the prose did not print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit@3.8.0 &lt;span class="nt"&gt;--force&lt;/span&gt; warrendeleon/react-native-module-federation#post-06-shared-store /tmp/pokedex-ref-06
&lt;span class="nb"&gt;rm &lt;/span&gt;packages/detail/src/PokemonDetailScreen.tsx
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /tmp/pokedex-ref-06/. &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rm&lt;/code&gt; matters: this post deletes that screen, and a copy over the tree adds and overwrites but never removes.&lt;/p&gt;

&lt;p&gt;The sweep also brings the Jest transform configuration the store stack needs, the parser regression tests, the per-tab stack changes and the detail refactor this walkthrough described in prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The packages are published and installed, so Verdaccio isn't needed for the run. Three dev servers, one per app, then the simulator build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote      &lt;span class="c"&gt;# :8082&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/party &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start:remote     &lt;span class="c"&gt;# :8083&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start                 &lt;span class="c"&gt;# :8081&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;apps/host &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pokédex tab shows a spinner for a moment, then fills with the first 151 Pokémon, official artwork included, straight from PokéAPI. Tap a row and the detail route fetches its Pokémon through the same cache. Tap Refresh in the header and the list reloads across the seam:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎞️ &lt;strong&gt;Animated demo:&lt;/strong&gt; &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store#run-it" rel="noopener noreferrer"&gt;watch it on warrendeleon.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The party tab still renders its empty grid, contract two majors behind, detail screen still the static 1.0.0, store nowhere in sight, working exactly as it did before this post. Independence includes the freedom to not participate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what's next
&lt;/h2&gt;

&lt;p&gt;The contract package owns the one &lt;code&gt;baseApi&lt;/code&gt; every side injects into, so the cache, the deduplication, and the tag graph are shared across features that were built and shipped on their own. The host owns the store built around it. The Pokédex domain fills the cache with both its endpoints, the installed view renders what the containers feed it, the boundary is guarded with a schema, and the host refreshes data it cannot name by dispatching a tag. When the sharing is removed, the failure is a silent spinner; you have seen it once here, so you will know it when it is expensive.&lt;/p&gt;

&lt;p&gt;Everything crossing the seam is still server state, though: data a server owns and the cache holds a copy of. Nothing the app itself owns has crossed a module boundary yet, and the app is starting to accumulate things it owns. The party grid is still empty because nothing you do in the Pokédex can reach it. Close the app and reopen it, and nothing you did survives: no party, no favourites, no memory.&lt;/p&gt;

&lt;p&gt;Client state is the other half of the split this post started from, and the build-along &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;returns for it in two posts' time&lt;/a&gt;. First, the series &lt;a href="https://warrendeleon.com/blog/who-owns-what-federated-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=module-federation-shared-store" rel="noopener noreferrer"&gt;steps back and argues the ownership calls&lt;/a&gt; the last three posts kept making one at a time: where a boundary goes, what a shared component may know, where a data definition lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/usage/code-splitting" rel="noopener noreferrer"&gt;Redux Toolkit: code splitting&lt;/a&gt; — &lt;code&gt;injectEndpoints&lt;/code&gt; and adding endpoints to an existing API at runtime&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redux-toolkit.js.org/rtk-query/overview" rel="noopener noreferrer"&gt;RTK Query&lt;/a&gt; — the cache, the tags, and the generated hooks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; — the schema library guarding the runtime boundary&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pokeapi.co/" rel="noopener noreferrer"&gt;PokéAPI&lt;/a&gt; — the free REST API the app fetches from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/warrendeleon/react-native-module-federation" rel="noopener noreferrer"&gt;react-native-module-federation&lt;/a&gt; — the companion repo, at the tag &lt;code&gt;post-06-shared-store&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>modulefederation</category>
      <category>repack</category>
      <category>rspack</category>
    </item>
    <item>
      <title>Tags vs query keys: a small API difference that changes how teams coordinate</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 27 Jul 2026 00:12:20 +0000</pubDate>
      <link>https://dev.to/warrendeleon/tags-vs-query-keys-a-small-api-difference-that-changes-how-teams-coordinate-222e</link>
      <guid>https://dev.to/warrendeleon/tags-vs-query-keys-a-small-api-difference-that-changes-how-teams-coordinate-222e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;State Management&lt;/strong&gt; series — &lt;a href="https://warrendeleon.com/blog/series/state-management/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;read it in full on warrendeleon.com&lt;/a&gt;, where new parts land first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tags survive a rename, keys don't
&lt;/h2&gt;

&lt;p&gt;Declarative tags scale to teams. String-based keys don't.&lt;/p&gt;

&lt;p&gt;TanStack Query's keys are shorter, lighter, and pleasant to read inside a single codebase. For a feature team that owns every query, the key-based API is the obvious choice. RTK Query's tag system adds ceremony you don't need at that size.&lt;/p&gt;

&lt;p&gt;The shape that works for one team becomes a liability across team boundaries. The failure mode shifts from "fix it before merge" to "find it from a support ticket."&lt;/p&gt;

&lt;h2&gt;
  
  
  One order, two teams
&lt;/h2&gt;

&lt;p&gt;The orders team ships a &lt;code&gt;placeOrder&lt;/code&gt; mutation. The catalogue team has a &lt;code&gt;getProduct&lt;/code&gt; query that returns an item's details and stock level. When the order succeeds, the cached product is stale (its stock count just changed), and something has to mark it for refetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  With RTK Query tags
&lt;/h2&gt;

&lt;p&gt;The catalogue team declares what their query holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orders team declares what their mutation affects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;invalidatesTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the mutation succeeds, the library walks its cache, finds queries that provide the &lt;code&gt;Product&lt;/code&gt; tag, and refetches the ones currently mounted. The orders team never reads the catalogue's code. They don't need to know what query key the catalogue uses or what shape the cache entry takes. They declare intent at a categorical level and the library wires it.&lt;/p&gt;

&lt;p&gt;Both teams are coupled to the string &lt;code&gt;'Product'&lt;/code&gt;. That coupling lives in a shared &lt;code&gt;tagTypes&lt;/code&gt; list declared in the API module, so it has a single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  With TanStack Query keys
&lt;/h2&gt;

&lt;p&gt;TanStack invalidates by key, not by category. The orders team writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that line to be correct, the orders team has to know the catalogue team's exact query key shape. They go look at the catalogue's code, find the &lt;code&gt;useQuery&lt;/code&gt; call, read the key, and copy it into their own invalidation.&lt;/p&gt;

&lt;p&gt;The teams are coupled to the same string &lt;code&gt;'product'&lt;/code&gt;, but the coupling lives in a different place. It's a magic string in the orders team's mutation handler, with no contract pointing back to the catalogue's query.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when teams drift
&lt;/h2&gt;

&lt;p&gt;Both approaches produce the same outcome when teams stay coordinated. The cached product refetches, the UI updates, the user sees the new stock level. The approaches diverge when coordination slips.&lt;/p&gt;

&lt;p&gt;Take a rename. The catalogue team is refactoring. The domain term has shifted. What they used to call "product" is now called "listing" across the API, the docs, and the team's day-to-day language. They rename the query key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchProduct&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listingId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchListing&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They ship.&lt;/p&gt;

&lt;p&gt;In an RTK Query world with tags, the rename has no effect on the orders team's invalidation. Tags are independent of query keys. The catalogue team's query still provides &lt;code&gt;'Product'&lt;/code&gt;. The orders team's mutation still invalidates &lt;code&gt;'Product'&lt;/code&gt;. The wiring holds.&lt;/p&gt;

&lt;p&gt;In a TanStack Query world with keys, the rename breaks the orders team silently. &lt;code&gt;invalidateQueries({ queryKey: ['product'] })&lt;/code&gt; now matches zero cached queries. The mutation succeeds. The library doesn't surface an error because finding zero matches isn't one. The user's product page stays stale until they navigate away and back, or until a support ticket surfaces the bug.&lt;/p&gt;

&lt;p&gt;No type error. No compile failure. No runtime exception. Stale UI in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the coupling lives
&lt;/h2&gt;

&lt;p&gt;Both approaches encode the same coupling between the two teams. The difference is where the coupling lives, and what surfaces when it breaks.&lt;/p&gt;

&lt;p&gt;Tags couple the teams through a &lt;strong&gt;category name&lt;/strong&gt;: an abstract noun describing a kind of data. It lives in a shared enum. Renames need coordination through that enum, and the rename surfaces at compile time: inside one &lt;code&gt;createApi&lt;/code&gt; module, immediately; across independently shipped apps, at each app's next build against the updated shared package.&lt;/p&gt;

&lt;p&gt;Keys couple the teams through a &lt;strong&gt;string identifier&lt;/strong&gt;: the literal shape that identifies a cached entry. It lives wherever someone types it. Renames don't need coordination because nothing forces it. They surface when the bug hits production.&lt;/p&gt;

&lt;p&gt;Neither behaviour is baked into the words "tag" and "key". A team can hard-code a tag string outside the enum, and a TanStack team can build shared, typed key factories that surface renames just as loudly. The enforcement comes from the shared contract, and the difference is which way each API leans: RTK Query asks for tags from a declared &lt;code&gt;tagTypes&lt;/code&gt; list, so the contract is the path of least resistance; TanStack accepts any array, so the contract is something you have to build and police yourself.&lt;/p&gt;

&lt;p&gt;That gap in surfacing isn't a flaw in TanStack Query's design. The key-based API is shorter, lighter, and well-suited to a single team that holds every query in its head. The shape stops fitting when the people writing the invalidation and the people writing the query stop being the same people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one fits your team
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One team, one codebase.&lt;/strong&gt; If that's your app, the difference is mostly aesthetic. TanStack's key-based API is shorter and adds no ceremony. The risk of silent invalidation drift is small because you have full visibility into every query key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple teams shipping independently.&lt;/strong&gt; Here the failure mode of key-based invalidation starts to compound. Each team's refactor is a possible silent break for any other team that referenced their keys. You can build conventions to soften that (query key factories, shared key constants, code review checklists, integration tests across features), but those are conventions, not enforcement: you're rebuilding what the tag system gives you for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A library choice still open, with federation or multi-team work ahead.&lt;/strong&gt; In that case the tag system is worth understanding before the API shape feels arbitrary. Picking between the two libraries means picking which failure modes you're willing to live with.&lt;/p&gt;

&lt;p&gt;The Module Federation series takes up the broader question of state management across independently shipped remotes, in &lt;a href="https://warrendeleon.com/blog/one-shared-store-across-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;One shared store&lt;/a&gt; for the server half and &lt;a href="https://warrendeleon.com/blog/client-state-across-federated-remotes-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;Client state across the seam&lt;/a&gt; for the client half. If you're new to the server-vs-client-state split this one assumes, &lt;a href="https://warrendeleon.com/blog/server-state-and-client-state-react-native/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=tags-vs-query-keys" rel="noopener noreferrer"&gt;start here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>statemanagement</category>
      <category>rtkquery</category>
      <category>tanstackquery</category>
    </item>
  </channel>
</rss>
