% rn-tooling-release.tex — Expo vs bare, EAS, OTA updates, Metro, upgrades, native build + crash reporting.
% Source: own knowledge (no repo sources). Build ONLY with:
%   tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/react-native/rn-tooling-release.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=react-native kind=process level=senior platform=cross-platform new=no round=react-native-2026-09-24 topic=build,tooling,debugging
% @tags: expo, eas-build, eas-update, ota-updates, runtimeversion, continuous-native-generation, config-plugins, metro, expo-dev-client, source-maps, upgrade-helper, codepush
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{JSONSheet}{
  morekeywords={true,false,null},
  sensitive=true, morecomment=[l]{//}, morestring=[b]"}

\tikzset{
  pb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=8mm, text width=22mm},
  st/.style={pb, draw=sheetGrey, fill=black!4},
  ot/.style={pb, draw=sheetGreen!70!black, fill=sheetGreen!10},
  gate/.style={diamond, draw=sheetOrange, thick, fill=sheetOrange!8, aspect=2.2,
               inner sep=0.5pt, font=\scriptsize, align=center},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
}

\begin{document}

\sheettitle{Tooling \& release — Expo, EAS, OTA, Metro, native builds}{react-native · memo}

\oneliner{An RN app is a \textbf{native binary} (reviewed, signed, store-shipped) that
\textbf{runs a JS bundle} made by Metro. Change native code $\Rightarrow$ new store build;
change only JS/assets $\Rightarrow$ an \textbf{OTA update} may replace the bundle — but only
for binaries with the same \textbf{runtimeVersion}.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  \node[pb] (src) at (1.1,2.95) {\textbf{TS/JS + assets}\\\texttt{src/}, images, fonts};
  \node[pb] (metro) at (3.75,2.95) {\textbf{Metro}: resolve $\to$ transform (Babel) $\to$ bundle + source map};
  \node[pb] (hbc) at (6.4,2.95) {\textbf{hermesc}\\bundle $\to$ \texttt{.hbc} bytecode};
  \node[gate] (g) at (8.95,2.95) {native\\change?};
  \node[st] (nat) at (3.75,4.05) {\texttt{ios/} \texttt{android/}: committed (bare) or
     \texttt{expo prebuild} (CNG)};
  \node[st] (build) at (11.45,3.75) {\textbf{Xcode / Gradle}\\(EAS Build): sign, embed bundle};
  \node[st] (store) at (13.9,3.75) {IPA / AAB $\to$\\TestFlight · Play review};
  \node[ot] (upd) at (11.45,2.15) {\texttt{eas update}\\\texttt{--branch production}};
  \node[ot] (srv) at (13.9,2.15) {EAS Update:\\channel $\to$ branch};
  \node[box, font=\scriptsize, text width=14mm, minimum height=24mm, inner sep=1.5pt]
       (dev) at (16.0,2.95) {\textbf{device}\\binary + embedded bundle\\[2pt]OTA taken only if
       \textbf{runtime\-Version} matches; applied next launch};
  \draw[flow] (src) -- (metro); \draw[flow] (metro) -- (hbc); \draw[flow] (hbc) -- (g);
  \draw[flow] (nat.north) -- ++(0,0.12) -| (build.north);
  \draw[flow] (g.north) |- node[lbl, pos=0.25, left]{yes} (build.west);
  \draw[hot] (g.south) |- node[lbl, pos=0.25, left, text=sheetGreen!50!black]{no: JS/assets} (upd.west);
  \draw[flow] (build) -- (store); \draw[flow] (store.east) -- (store.east -| dev.west);
  \draw[hot] (upd) -- (srv); \draw[hot] (srv.east) -- (srv.east -| dev.west);
  % symbols band
  \draw[sheetGrey!40] (0,1.6) -- (16.9,1.6);
  \node[lbl, anchor=west, text=sheetRed] at (0,1.4) {\textbf{per build AND per OTA update} upload to the
    crash reporter: JS \textbf{source map} (Hermes: composed with the hermesc map) · iOS \textbf{dSYM} ·
    Android R8 \textbf{mapping.txt} — else prod stacks are unreadable};
  \node[lbl, anchor=west, text=sheetGrey] at (0,1.1) {native change = new pod/Gradle dep, permission,
    Info.plist/entitlement, icon/splash, RN/Expo SDK bump, config-plugin change · otherwise the gate says OTA};
\end{tikzpicture}

\begin{multicols}{2}

\section{Expo vs bare — when each}
\begin{itemize}
  \item \textbf{CNG} (Continuous Native Generation): \texttt{ios/} \texttt{android/} are
        \textbf{build output}, regenerated by \texttt{npx expo prebuild} (\texttt{--clean})
        from \texttt{app.json}/\texttt{app.config.ts} + \textbf{config plugins}
        (\texttt{withInfoPlist}, \texttt{withEntitlementsPlist},
        \texttt{withAndroidManifest}). Native tweaks live in a plugin — a hand edit is
        lost on the next prebuild. Upgrades become ``regenerate''.
  \item \textbf{Expo Go} = a prebuilt sandbox with one SDK's modules: no custom native
        code. A \textbf{dev build} (\texttt{expo-dev-client}) is \emph{your} binary + dev
        menu — needed once any native library is added.
  \item \textbf{Bare} (\texttt{npx @react-native-community/cli init}): you own the
        Xcode/Gradle projects. Choose it for \textbf{brownfield} (RN inside an existing
        native app) or when a native team owns the projects. Default for new apps: Expo
        (the RN docs recommend a framework). Expo $\ne$ ``no native code''.
\end{itemize}

\section{EAS · OTA updates}
\begin{itemize}
  \item \textbf{Build}: cloud builders, \texttt{eas.json} profiles, managed certificates /
        provisioning profiles / keystore (\texttt{--local} to build on your Mac).
        \textbf{Submit}: \texttt{eas submit -p ios} $\to$ App Store Connect / TestFlight;
        Android $\to$ a Play track.
  \item \textbf{Update}: a build carries a \textbf{channel}; a channel points at a
        \textbf{branch} of updates. Promote/roll back by re-pointing the channel or
        republishing an older update. \texttt{expo-updates} checks at launch, downloads,
        applies on the \emph{next} cold start (\texttt{checkForUpdateAsync} /
        \texttt{fetchUpdateAsync} / \texttt{reloadAsync} for urgent fixes); a launch
        crash falls back to the previous update.
  \item \textbf{runtimeVersion} policies: \texttt{appVersion}, \texttt{nativeVersion},
        \texttt{fingerprint} (hash of native inputs — safest), or a manual string.
  \item \textbf{Apple}: downloaded JS is allowed if it does not change the app's
        \textbf{primary purpose}, add a storefront, or bypass security — fixes and tweaks,
        not a different app. \textbf{CodePush} died with App Center (retired 31 Mar 2025).
\end{itemize}

\section{Example — eas.json + app.json}
\begin{lstlisting}[language=JSONSheet]
{ "build": {                       // eas.json
    "development": { "developmentClient": true },
    "preview":     { "channel": "preview" },
    "production":  { "channel": "production" } } }
// app.json > expo:
"runtimeVersion": { "policy": "fingerprint" },
"updates": { "url": "https://u.expo.dev/<project-id>" }
\end{lstlisting}

\columnbreak

\section{Metro · env · upgrades · debugging}
\begin{itemize}
  \item \textbf{Metro} resolves \texttt{.ios.tsx}/\texttt{.android.tsx}/\texttt{.native.tsx}
        first, caches transforms (stale after a Babel/env change: \texttt{npx expo start -c}
        · \texttt{--reset-cache}); monorepos need \texttt{watchFolders}. Debug loads from
        Metro (:8081).
  \item \textbf{Fast Refresh} re-runs the edited module, keeps component state; a file
        exporting non-components remounts.
  \item \textbf{Secrets}: the bundle is public. \texttt{EXPO\_PUBLIC\_*} /
        \texttt{react-native-config} values are \emph{inlined}. Keys stay on a server.
  \item \textbf{Upgrades hurt}: the native template moves (AppDelegate, Podfile, AGP,
        Kotlin, min iOS), every native dep must support the new RN, Xcode bumps too.
        \textbf{Upgrade Helper} = template diff per version. Small steps; Expo:
        \texttt{npx expo install --fix} + \texttt{npx expo-doctor}.
  \item \textbf{Debug}: \textbf{React Native DevTools} (Chrome DevTools on Hermes) —
        default since 0.76; \textbf{Flipper} left the template in 0.74. Native: Xcode,
        Instruments.
\end{itemize}

\section{iOS · Android specifics}
\begin{itemize}
  \item \textbf{iOS}: Podfile \texttt{use\_react\_native!} + \texttt{use\_native\_modules!};
        \texttt{bundle exec pod install}; open the \textbf{.xcworkspace}. Envs =
        schemes/configurations + xcconfig (own bundle ID). Build phase ``Bundle React
        Native code and images'' embeds \texttt{main.jsbundle} in Release.
  \item \textbf{Android}: \texttt{gradle.properties} (\texttt{newArchEnabled},
        \texttt{hermesEnabled}); envs = \texttt{productFlavors}; ship an \textbf{AAB} with
        Play App Signing; \textbf{R8} (\texttt{enableProguardInReleaseBuilds}) shrinks and
        obfuscates — reflection-using libs need keep rules.
\end{itemize}

\section{Interview traps}
\begin{itemize}
  \trap{OTA JS needing a new native module $\to$ crash; use fingerprint.}
  \trap{Hand-editing \texttt{ios/} in a CNG project — gone after prebuild.}
  \trap{API key in \texttt{.env} ``hidden'' — it is in the bundle.}
  \trap{No source maps for an OTA update $\to$ unreadable prod stacks.}
\end{itemize}

\section{Remember}
\textbf{Native $\Rightarrow$ store · JS $\Rightarrow$ OTA · runtimeVersion is the contract.}

\section{Likely questions}
\begin{enumerate}
  \item What can OTA change? — JS + assets, same runtimeVersion.
  \item Expo or bare? — Expo + CNG + EAS; bare for brownfield.
  \item Why do upgrades hurt? — template, deps, toolchains move.
  \item Symbolicate a JS crash? — source maps per build and update.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} rn-native-modules (spec = ABI) ·
rn-navigation · Hermes · code signing · TestFlight · CI/CD}

\end{document}
