@absolutejs/absolute 0.20.0-beta.114 → 0.20.0-beta.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ import {
22
22
  writeFile as writeFile2
23
23
  } from "fs/promises";
24
24
  import { createHash as createHash3 } from "crypto";
25
- import { basename, dirname as dirname3, join as join6, relative as relative3, resolve as resolve4, sep } from "path";
25
+ import { basename, dirname as dirname3, join as join7, relative as relative3, resolve as resolve4, sep } from "path";
26
26
 
27
27
  // src/mobile/syncSchema.ts
28
28
  import { readFileSync } from "fs";
@@ -902,6 +902,7 @@ var createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
902
902
  },
903
903
  deviceCapabilities: options.deviceCapabilities,
904
904
  engine: options.config.engine,
905
+ ...options.config.engine === "expo" ? { expoEmbeddedAssetsAbi: 1 } : {},
905
906
  format: ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
906
907
  shellAbi: ABSOLUTE_MOBILE_SHELL_ABI,
907
908
  syncSchema: options.syncSchema ?? null,
@@ -1671,6 +1672,265 @@ var absoluteExpoActivityResultRecoveryFiles = (project) => new Map([
1671
1672
  ]
1672
1673
  ]);
1673
1674
 
1675
+ // src/mobile/expoEmbeddedAssets.ts
1676
+ import { join as join5 } from "path";
1677
+ var header = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
1678
+ `;
1679
+ var android = `${header}package expo.modules.absoluteembeddedassets
1680
+
1681
+ import android.content.Context
1682
+ import android.graphics.Bitmap
1683
+ import android.net.Uri
1684
+ import android.util.Log
1685
+ import android.webkit.WebResourceRequest
1686
+ import android.webkit.WebResourceResponse
1687
+ import android.webkit.WebView
1688
+ import androidx.webkit.WebViewAssetLoader
1689
+ import androidx.webkit.WebViewCompat
1690
+ import androidx.webkit.WebViewFeature
1691
+ import androidx.webkit.ScriptHandler
1692
+ import androidx.webkit.WebMessageCompat
1693
+ import com.facebook.react.ReactPackage
1694
+ import com.facebook.react.bridge.NativeModule
1695
+ import com.facebook.react.bridge.ReactApplicationContext
1696
+ import com.facebook.react.bridge.ReadableMap
1697
+ import com.facebook.react.uimanager.ThemedReactContext
1698
+ import com.facebook.react.uimanager.ViewManager
1699
+ import com.reactnativecommunity.webview.RNCWebViewClient
1700
+ import com.reactnativecommunity.webview.RNCWebViewManager
1701
+ import com.reactnativecommunity.webview.RNCWebViewWrapper
1702
+ import expo.modules.kotlin.modules.Module
1703
+ import expo.modules.kotlin.modules.ModuleDefinition
1704
+ import java.io.ByteArrayInputStream
1705
+ import java.io.File
1706
+ import java.util.concurrent.ConcurrentHashMap
1707
+ import java.util.WeakHashMap
1708
+
1709
+ private const val HOST = "appassets.androidplatform.net"
1710
+ private const val PREFIX = "/absolutejs/"
1711
+
1712
+ internal object AbsoluteEmbeddedAssets {
1713
+ private val bundles = ConcurrentHashMap<String, Map<String, File>>()
1714
+
1715
+ fun register(context: Context, rootUri: String, paths: List<String>): String {
1716
+ check(WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) &&
1717
+ WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
1718
+ "Update Android System WebView to load this application."
1719
+ }
1720
+ val uri = Uri.parse(rootUri)
1721
+ require(uri.scheme == "file" && uri.authority.isNullOrEmpty() && uri.query == null && uri.fragment == null)
1722
+ val root = File(requireNotNull(uri.path)).canonicalFile
1723
+ val parent = File(context.filesDir, "absolutejs-web").canonicalFile
1724
+ require(root.parentFile == parent && root.name.matches(Regex("amexpo_[a-f0-9]{64}")))
1725
+ require(paths.isNotEmpty() && paths.size <= 100000 && paths.distinct().size == paths.size)
1726
+ val files = paths.associateWith { path ->
1727
+ require(path.isNotEmpty() && !path.contains('\\\\') && !path.contains('%') &&
1728
+ !path.contains('?') && !path.contains('#') && path.indexOf(0.toChar()) < 0 &&
1729
+ path.split('/').all { it.isNotEmpty() && it != "." && it != ".." })
1730
+ val file = File(root, path).canonicalFile
1731
+ require(file.path.startsWith(root.path + File.separator) && file.isFile)
1732
+ file
1733
+ }
1734
+ require(files.containsKey("index.html") && files.containsKey("absolute-mobile-manifest.json"))
1735
+ bundles[root.name] = files
1736
+ Log.i("AbsoluteJS", "Expo embedded web phase: asset-host-registered")
1737
+ return "https://" + HOST + PREFIX + root.name + "/index.html"
1738
+ }
1739
+
1740
+ private fun empty(status: Int, reason: String) = WebResourceResponse(
1741
+ "text/plain", "UTF-8", status, reason,
1742
+ mapOf("Cache-Control" to "no-store", "X-Content-Type-Options" to "nosniff"),
1743
+ ByteArrayInputStream(ByteArray(0))
1744
+ )
1745
+
1746
+ private fun resource(path: String): WebResourceResponse {
1747
+ val bundle = path.substringBefore('/')
1748
+ val name = path.substringAfter('/', "")
1749
+ val file = bundles[bundle]?.get(name) ?: return empty(404, "Not Found")
1750
+ // Recheck canonical location at read time. Never follow a replaced symlink.
1751
+ if (file.canonicalFile != file || !file.isFile) return empty(404, "Not Found")
1752
+ val mime = when (file.extension.lowercase()) {
1753
+ "html", "htm" -> "text/html"
1754
+ "js", "mjs" -> "text/javascript"
1755
+ "json", "map" -> "application/json"
1756
+ "css" -> "text/css"
1757
+ "svg" -> "image/svg+xml"
1758
+ "wasm" -> "application/wasm"
1759
+ else -> android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension.lowercase()) ?: "application/octet-stream"
1760
+ }
1761
+ return try {
1762
+ WebResourceResponse(mime, "UTF-8", 200, "OK",
1763
+ mapOf("Cache-Control" to "no-store", "X-Content-Type-Options" to "nosniff"), file.inputStream())
1764
+ } catch (_: Exception) { empty(404, "Not Found") }
1765
+ }
1766
+
1767
+ private val loader = WebViewAssetLoader.Builder()
1768
+ .setDomain(HOST).setHttpAllowed(false)
1769
+ .addPathHandler(PREFIX) { path -> resource(path) }.build()
1770
+
1771
+ fun intercept(request: WebResourceRequest, activeBundle: String?): WebResourceResponse? {
1772
+ val uri = request.url
1773
+ if (uri.host != HOST) return null
1774
+ if (uri.path.orEmpty().endsWith("/index.html")) Log.i("AbsoluteJS", "Expo embedded web phase: document-request")
1775
+ if (uri.path.orEmpty().endsWith("/absolute-mobile-bootstrap.js")) Log.i("AbsoluteJS", "Expo embedded web phase: bootstrap-request")
1776
+ // A reserved asset host must never fall through to DNS/network, even for
1777
+ // unknown bundles, wrong ports/schemes, methods, or malformed paths.
1778
+ if (uri.scheme != "https" || uri.port != -1 || uri.userInfo != null ||
1779
+ request.method != "GET") return empty(403, "Forbidden")
1780
+ // Root-relative image/font/CSS URLs retain their web meaning. Scope them
1781
+ // to this WebView's bundle, not the last bundle registered by another view.
1782
+ if (!uri.path.orEmpty().startsWith(PREFIX)) {
1783
+ if (request.isForMainFrame || activeBundle == null) return empty(404, "Not Found")
1784
+ return resource(activeBundle + "/" + uri.path.orEmpty().removePrefix("/"))
1785
+ }
1786
+ return loader.shouldInterceptRequest(uri) ?: empty(404, "Not Found")
1787
+ }
1788
+ }
1789
+
1790
+ class AbsoluteEmbeddedAssetsModule : Module() {
1791
+ override fun definition() = ModuleDefinition {
1792
+ Name("AbsoluteEmbeddedAssets")
1793
+ AsyncFunction("registerBundle") { root: String, paths: List<String> ->
1794
+ AbsoluteEmbeddedAssets.register(requireNotNull(appContext.reactContext), root, paths)
1795
+ }
1796
+ }
1797
+ }
1798
+
1799
+ class AbsoluteEmbeddedWebViewManager : RNCWebViewManager() {
1800
+ private val scripts = WeakHashMap<RNCWebViewWrapper, ScriptHandler>()
1801
+ override fun getName() = "AbsoluteEmbeddedWebView"
1802
+ override fun setNewSource(view: RNCWebViewWrapper, value: ReadableMap?) {
1803
+ Log.i("AbsoluteJS", "Expo embedded web phase: source-assigned")
1804
+ super.setNewSource(view, value)
1805
+ }
1806
+ override fun setMessagingEnabled(view: RNCWebViewWrapper, value: Boolean) {
1807
+ super.setMessagingEnabled(view, value)
1808
+ // Upstream reports sourceOrigin as event.url and accepts subframe messages.
1809
+ // Retain its event dispatch, but bind this bridge to our main-frame origin
1810
+ // and attach the native-observed document URL, never a JS-supplied URL.
1811
+ if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) return
1812
+ WebViewCompat.removeWebMessageListener(view.webView, "ReactNativeWebView")
1813
+ if (!value) return
1814
+ WebViewCompat.addWebMessageListener(view.webView, "ReactNativeWebView", setOf("https://" + HOST)) {
1815
+ webView, message, sourceOrigin, isMainFrame, _ ->
1816
+ if (isMainFrame && sourceOrigin.scheme == "https" && sourceOrigin.host == HOST &&
1817
+ message.type == WebMessageCompat.TYPE_STRING) {
1818
+ val url = webView.url
1819
+ val data = message.data
1820
+ if (url != null && data != null) view.webView.onMessage(data, url)
1821
+ }
1822
+ }
1823
+ }
1824
+ override fun setInjectedJavaScriptBeforeContentLoaded(view: RNCWebViewWrapper, value: String?) {
1825
+ scripts.remove(view)?.remove()
1826
+ if (value == null) return
1827
+ // onPageStarted injection races module execution on Android. Register with
1828
+ // Chromium before navigation, scoped to our virtual asset origin only.
1829
+ check(WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
1830
+ "AbsoluteJS requires an Android System WebView with document-start script support."
1831
+ }
1832
+ scripts[view] = WebViewCompat.addDocumentStartJavaScript(view.webView, value, setOf("https://" + HOST))
1833
+ }
1834
+ override fun onDropViewInstance(view: RNCWebViewWrapper) {
1835
+ scripts.remove(view)?.remove()
1836
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER))
1837
+ WebViewCompat.removeWebMessageListener(view.webView, "ReactNativeWebView")
1838
+ super.onDropViewInstance(view)
1839
+ }
1840
+ override fun addEventEmitters(context: ThemedReactContext, view: RNCWebViewWrapper) {
1841
+ Log.i("AbsoluteJS", "Expo embedded web phase: asset-host-view-created")
1842
+ view.webView.webViewClient = object : RNCWebViewClient() {
1843
+ @Volatile private var activeBundle: String? = null
1844
+ override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
1845
+ Log.i("AbsoluteJS", "Expo embedded web phase: document-started")
1846
+ val uri = Uri.parse(url)
1847
+ activeBundle = if (uri.scheme == "https" && uri.host == HOST && uri.path.orEmpty().startsWith(PREFIX))
1848
+ uri.path!!.removePrefix(PREFIX).substringBefore('/') else null
1849
+ super.onPageStarted(view, url, favicon)
1850
+ }
1851
+ override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? =
1852
+ AbsoluteEmbeddedAssets.intercept(request, activeBundle) ?: super.shouldInterceptRequest(view, request)
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ class AbsoluteEmbeddedWebViewPackage : ReactPackage {
1858
+ override fun createNativeModules(context: ReactApplicationContext): List<NativeModule> = emptyList()
1859
+ override fun createViewManagers(context: ReactApplicationContext): List<ViewManager<*, *>> = listOf(AbsoluteEmbeddedWebViewManager())
1860
+ }
1861
+ `;
1862
+ var plugin = `${header}const { withMainApplication } = require('expo/config-plugins');
1863
+ module.exports = config => withMainApplication(config, value => {
1864
+ if (value.modResults.language !== 'kt') throw new Error('AbsoluteJS embedded assets require the managed Kotlin application.');
1865
+ const name = 'expo.modules.absoluteembeddedassets.AbsoluteEmbeddedWebViewPackage';
1866
+ const registration = 'add(' + name + '())';
1867
+ const source = value.modResults.contents;
1868
+ if (!source.includes(registration)) {
1869
+ const anchor = 'PackageList(this).packages.apply {';
1870
+ if (source.split(anchor).length !== 2) throw new Error('Cannot safely register AbsoluteJS embedded WebView in MainApplication.');
1871
+ value.modResults.contents = source.replace(anchor, anchor + '\\n ' + registration);
1872
+ }
1873
+ return value;
1874
+ });
1875
+ `;
1876
+ var runtime2 = `${header}import { requireNativeModule } from 'expo-modules-core';
1877
+ import { Platform, requireNativeComponent } from 'react-native';
1878
+
1879
+ export const absoluteEmbeddedNativeConfig = Platform.OS === 'android'
1880
+ ? { component: requireNativeComponent('AbsoluteEmbeddedWebView') }
1881
+ : undefined;
1882
+
1883
+ export const registerAbsoluteEmbeddedBundle = async (rootUri: string, paths: string[], indexUri: string): Promise<string> => {
1884
+ if (Platform.OS !== 'android') return indexUri;
1885
+ const native = requireNativeModule<{ registerBundle(root: string, paths: string[]): Promise<string> }>('AbsoluteEmbeddedAssets');
1886
+ return native.registerBundle(rootUri, paths);
1887
+ };
1888
+ `;
1889
+ var absoluteExpoEmbeddedAssetsFiles = (project) => {
1890
+ const root = join5(project, "modules/absolute-embedded-assets");
1891
+ return new Map([
1892
+ [
1893
+ join5(root, "expo-module.config.json"),
1894
+ `${JSON.stringify({
1895
+ android: {
1896
+ modules: [
1897
+ "expo.modules.absoluteembeddedassets.AbsoluteEmbeddedAssetsModule"
1898
+ ]
1899
+ },
1900
+ platforms: ["android"]
1901
+ }, null, 2)}
1902
+ `
1903
+ ],
1904
+ [
1905
+ join5(root, "android/build.gradle"),
1906
+ `${header}plugins { id 'com.android.library'; id 'expo-module-gradle-plugin' }
1907
+ group = 'expo.modules.absoluteembeddedassets'
1908
+ version = '0.0.1'
1909
+ android {
1910
+ namespace 'expo.modules.absoluteembeddedassets'
1911
+ defaultConfig { versionCode 1; versionName '0.0.1' }
1912
+ }
1913
+ dependencies {
1914
+ implementation 'com.facebook.react:react-android'
1915
+ implementation project(':react-native-webview')
1916
+ implementation 'androidx.webkit:webkit:1.14.0'
1917
+ }
1918
+ `
1919
+ ],
1920
+ [
1921
+ join5(root, "android/src/main/AndroidManifest.xml"),
1922
+ `<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
1923
+ `
1924
+ ],
1925
+ [
1926
+ join5(root, "android/src/main/java/expo/modules/absoluteembeddedassets/AbsoluteEmbeddedAssetsModule.kt"),
1927
+ android
1928
+ ],
1929
+ [join5(project, "plugins/withAbsoluteEmbeddedAssets.js"), plugin],
1930
+ [join5(project, "src/generated/AbsoluteEmbeddedAssets.ts"), runtime2]
1931
+ ]);
1932
+ };
1933
+
1674
1934
  // src/mobile/branding.ts
1675
1935
  import { createHash as createHash2 } from "crypto";
1676
1936
  import {
@@ -1682,7 +1942,7 @@ import {
1682
1942
  rm,
1683
1943
  writeFile
1684
1944
  } from "fs/promises";
1685
- import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve3 } from "path";
1945
+ import { dirname as dirname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
1686
1946
  var ABSOLUTE_MOBILE_BRANDING_FORMAT = 1;
1687
1947
  var ABSOLUTE_CAPACITOR_ASSETS_VERSION = "3.0.5";
1688
1948
  var ABSOLUTE_CAPACITOR_ASSETS_SPEC = `@capacitor/assets@${ABSOLUTE_CAPACITOR_ASSETS_VERSION}`;
@@ -1706,7 +1966,7 @@ var exists = async (path) => {
1706
1966
  }
1707
1967
  };
1708
1968
  var portableRelative = (root, path) => relative2(root, path).replaceAll("\\", "/");
1709
- var absoluteMobileBrandingManifestPath = (projectRoot) => join5(resolve3(projectRoot), ".absolutejs", "mobile", "branding", "manifest.json");
1969
+ var absoluteMobileBrandingManifestPath = (projectRoot) => join6(resolve3(projectRoot), ".absolutejs", "mobile", "branding", "manifest.json");
1710
1970
  var brandingEntries = (branding) => [
1711
1971
  { kind: "icon", path: branding.icon, size: ICON_SIZE },
1712
1972
  branding.android.foreground ? {
@@ -1853,10 +2113,10 @@ var writeAbsoluteExpoBrandingInputs = async (config, projectRoot) => {
1853
2113
  if (!branding)
1854
2114
  return;
1855
2115
  const manifest = await createAbsoluteMobileBrandingManifest(config, projectRoot);
1856
- const destination = join5(config.nativeProjectDirectory, "assets", "absolute-branding");
2116
+ const destination = join6(config.nativeProjectDirectory, "assets", "absolute-branding");
1857
2117
  await rm(destination, { force: true, recursive: true });
1858
2118
  await mkdir(destination, { recursive: true });
1859
- await copyManaged(branding.icon, join5(destination, "icon.png"));
2119
+ await copyManaged(branding.icon, join6(destination, "icon.png"));
1860
2120
  const copies = [
1861
2121
  [branding.android.backgroundImage, "android-background.png"],
1862
2122
  [branding.android.foreground, "android-foreground.png"],
@@ -1866,19 +2126,19 @@ var writeAbsoluteExpoBrandingInputs = async (config, projectRoot) => {
1866
2126
  [branding.splash.image, "splash-light.png"],
1867
2127
  [branding.splash.darkImage, "splash-dark.png"]
1868
2128
  ];
1869
- await Promise.all(copies.map(([source, name]) => source ? copyManaged(source, join5(destination, name)) : undefined));
2129
+ await Promise.all(copies.map(([source, name]) => source ? copyManaged(source, join6(destination, name)) : undefined));
1870
2130
  await writeManifest(projectRoot, manifest);
1871
2131
  return { manifest, path: destination };
1872
2132
  };
1873
2133
  var stageCapacitorInputs = async (branding, projectRoot) => {
1874
- const destination = join5(projectRoot, ".absolutejs", "mobile", "branding", "input");
2134
+ const destination = join6(projectRoot, ".absolutejs", "mobile", "branding", "input");
1875
2135
  await rm(destination, { force: true, recursive: true });
1876
2136
  await mkdir(destination, { recursive: true });
1877
- await copyManaged(branding.icon, join5(destination, "logo.png"));
2137
+ await copyManaged(branding.icon, join6(destination, "logo.png"));
1878
2138
  if (branding.splash.image)
1879
- await copyManaged(branding.splash.image, join5(destination, "splash.png"));
2139
+ await copyManaged(branding.splash.image, join6(destination, "splash.png"));
1880
2140
  if (branding.splash.darkImage)
1881
- await copyManaged(branding.splash.darkImage, join5(destination, "splash-dark.png"));
2141
+ await copyManaged(branding.splash.darkImage, join6(destination, "splash-dark.png"));
1882
2142
  return destination;
1883
2143
  };
1884
2144
  var defaultRunner = async (command, options) => {
@@ -1909,16 +2169,16 @@ var resizePng = async (source, destination, size) => {
1909
2169
  await writeFile(destination, output);
1910
2170
  };
1911
2171
  var applyAndroidLayers = async (branding, nativeDirectory) => {
1912
- const resources = join5(nativeDirectory, "android", "app", "src", "main", "res");
2172
+ const resources = join6(nativeDirectory, "android", "app", "src", "main", "res");
1913
2173
  await Promise.all(ANDROID_ADAPTIVE_SIZES.flatMap(([density, size]) => [
1914
2174
  ...branding.android.foreground ? [
1915
- resizePng(branding.android.foreground, join5(resources, `mipmap-${density}`, "ic_launcher_foreground.png"), size)
2175
+ resizePng(branding.android.foreground, join6(resources, `mipmap-${density}`, "ic_launcher_foreground.png"), size)
1916
2176
  ] : [],
1917
2177
  ...branding.android.backgroundImage ? [
1918
- resizePng(branding.android.backgroundImage, join5(resources, `mipmap-${density}`, "ic_launcher_background.png"), size)
2178
+ resizePng(branding.android.backgroundImage, join6(resources, `mipmap-${density}`, "ic_launcher_background.png"), size)
1919
2179
  ] : [],
1920
2180
  ...branding.android.monochrome ? [
1921
- resizePng(branding.android.monochrome, join5(resources, `mipmap-${density}`, "ic_launcher_monochrome.png"), size)
2181
+ resizePng(branding.android.monochrome, join6(resources, `mipmap-${density}`, "ic_launcher_monochrome.png"), size)
1922
2182
  ] : []
1923
2183
  ]));
1924
2184
  if (!branding.android.monochrome)
@@ -1930,13 +2190,13 @@ var applyAndroidLayers = async (branding, nativeDirectory) => {
1930
2190
  <monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
1931
2191
  </adaptive-icon>
1932
2192
  `;
1933
- await Promise.all(["ic_launcher.xml", "ic_launcher_round.xml"].map((name) => writeFile(join5(resources, "mipmap-anydpi-v26", name), iconXml)));
2193
+ await Promise.all(["ic_launcher.xml", "ic_launcher_round.xml"].map((name) => writeFile(join6(resources, "mipmap-anydpi-v26", name), iconXml)));
1934
2194
  };
1935
2195
  var applyIosAppearances = async (branding, nativeDirectory) => {
1936
2196
  if (!branding.ios.darkIcon && !branding.ios.tintedIcon)
1937
2197
  return;
1938
- const assetSet = join5(nativeDirectory, "ios", "App", "App", "Assets.xcassets", "AppIcon.appiconset");
1939
- const contentsPath = join5(assetSet, "Contents.json");
2198
+ const assetSet = join6(nativeDirectory, "ios", "App", "App", "Assets.xcassets", "AppIcon.appiconset");
2199
+ const contentsPath = join6(assetSet, "Contents.json");
1940
2200
  const contents = JSON.parse(await readFile(contentsPath, "utf8"));
1941
2201
  const images = typeof contents === "object" && contents !== null ? Reflect.get(contents, "images") : undefined;
1942
2202
  if (typeof contents !== "object" || contents === null || !Array.isArray(images))
@@ -1949,7 +2209,7 @@ var applyIosAppearances = async (branding, nativeDirectory) => {
1949
2209
  if (!source)
1950
2210
  return;
1951
2211
  const filename = `AppIcon-1024-${appearance}.png`;
1952
- await resizePng(source, join5(assetSet, filename), ICON_SIZE);
2212
+ await resizePng(source, join6(assetSet, filename), ICON_SIZE);
1953
2213
  return {
1954
2214
  appearances: [{ appearance: "luminosity", value: appearance }],
1955
2215
  filename,
@@ -1971,7 +2231,7 @@ var applyIosAppearances = async (branding, nativeDirectory) => {
1971
2231
  await writeFile(contentsPath, `${JSON.stringify(contents, null, 2)}
1972
2232
  `);
1973
2233
  };
1974
- var nativeBrandingSentinels = (config, platforms) => platforms.map((platform) => platform === "android" ? join5(config.nativeProjectDirectory, "android", "app", "src", "main", "res", "mipmap-xxxhdpi", "ic_launcher.png") : join5(config.nativeProjectDirectory, "ios", "App", "App", "Assets.xcassets", "AppIcon.appiconset", "AppIcon-512@2x.png"));
2234
+ var nativeBrandingSentinels = (config, platforms) => platforms.map((platform) => platform === "android" ? join6(config.nativeProjectDirectory, "android", "app", "src", "main", "res", "mipmap-xxxhdpi", "ic_launcher.png") : join6(config.nativeProjectDirectory, "ios", "App", "App", "Assets.xcassets", "AppIcon.appiconset", "AppIcon-512@2x.png"));
1975
2235
  var requireGeneratedBranding = async (config, platforms) => {
1976
2236
  const missing = (await Promise.all(nativeBrandingSentinels(config, platforms).map(async (path) => ({
1977
2237
  path,
@@ -2016,9 +2276,9 @@ var generateAbsoluteCapacitorBranding = async (config, projectRoot, options = {}
2016
2276
  "--assetPath",
2017
2277
  portableRelative(projectRoot, input),
2018
2278
  "--androidProject",
2019
- portableRelative(projectRoot, join5(config.nativeProjectDirectory, "android")),
2279
+ portableRelative(projectRoot, join6(config.nativeProjectDirectory, "android")),
2020
2280
  "--iosProject",
2021
- portableRelative(projectRoot, join5(config.nativeProjectDirectory, "ios", "App")),
2281
+ portableRelative(projectRoot, join6(config.nativeProjectDirectory, "ios", "App")),
2022
2282
  "--iconBackgroundColor",
2023
2283
  branding.android.backgroundColor,
2024
2284
  "--splashBackgroundColor",
@@ -2099,7 +2359,7 @@ var inspectAbsoluteMobileBranding = async (config, projectRoot) => {
2099
2359
  status: "stale"
2100
2360
  });
2101
2361
  const nativeSentinels = config.engine === "expo" ? [
2102
- join5(config.nativeProjectDirectory, "assets", "absolute-branding", "icon.png")
2362
+ join6(config.nativeProjectDirectory, "assets", "absolute-branding", "icon.png")
2103
2363
  ] : nativeBrandingSentinels(config, config.platforms);
2104
2364
  if ((await Promise.all(nativeSentinels.map((path) => exists(path)))).some((present) => !present))
2105
2365
  return brandingInspection({
@@ -2132,7 +2392,7 @@ var renderAbsoluteMobileBrandingPreview = async (config, projectRoot) => {
2132
2392
  const { branding } = config;
2133
2393
  if (!branding)
2134
2394
  throw new TypeError("mobile.branding is not configured.");
2135
- const destination = join5(projectRoot, ".absolutejs", "mobile", "branding", "preview.html");
2395
+ const destination = join6(projectRoot, ".absolutejs", "mobile", "branding", "preview.html");
2136
2396
  const icon = `data:image/png;base64,${(await readFile(branding.icon)).toString("base64")}`;
2137
2397
  const appName = escapeHtml(config.appName);
2138
2398
  const html = `<!doctype html>
@@ -2227,7 +2487,7 @@ var routeSegments = (route) => {
2227
2487
  return segment;
2228
2488
  });
2229
2489
  };
2230
- var routeFile = (project, route) => join6(project, "app", ...routeSegments(route), "index.tsx");
2490
+ var routeFile = (project, route) => join7(project, "app", ...routeSegments(route), "index.tsx");
2231
2491
  var packageDependencies = (plan) => Object.fromEntries(plan.requiredPackages.map((spec) => {
2232
2492
  const separator = spec.lastIndexOf("@");
2233
2493
  return [spec.slice(0, separator), spec.slice(separator + 1)];
@@ -2316,24 +2576,24 @@ var expoAppConfig = (config, auth, sync, devices, runtimeFingerprint) => {
2316
2576
  const devicePlugins = [
2317
2577
  ...new Set(devices.capabilities.flatMap((name) => devices.providers[name]?.plugins ?? []))
2318
2578
  ];
2319
- const configuredPlugins = devicePlugins.map((plugin) => {
2320
- if (plugin === "expo-image-picker")
2579
+ const configuredPlugins = devicePlugins.map((plugin2) => {
2580
+ if (plugin2 === "expo-image-picker")
2321
2581
  return [
2322
- plugin,
2582
+ plugin2,
2323
2583
  {
2324
2584
  cameraPermission: descriptions.NSCameraUsageDescription,
2325
2585
  microphonePermission: false,
2326
2586
  photosPermission: descriptions.NSPhotoLibraryUsageDescription
2327
2587
  }
2328
2588
  ];
2329
- if (plugin === "expo-location")
2589
+ if (plugin2 === "expo-location")
2330
2590
  return [
2331
- plugin,
2591
+ plugin2,
2332
2592
  {
2333
2593
  locationWhenInUsePermission: descriptions.NSLocationWhenInUseUsageDescription
2334
2594
  }
2335
2595
  ];
2336
- return plugin;
2596
+ return plugin2;
2337
2597
  });
2338
2598
  return {
2339
2599
  expo: {
@@ -2355,7 +2615,7 @@ var expoAppConfig = (config, auth, sync, devices, runtimeFingerprint) => {
2355
2615
  package: config.appId,
2356
2616
  permissions: requirements.androidPermissions
2357
2617
  },
2358
- experiments: { typedRoutes: true },
2618
+ experiments: { tsconfigPaths: false, typedRoutes: true },
2359
2619
  ios: {
2360
2620
  ...branding?.ios ?? {},
2361
2621
  associatedDomains: config.deepLinkHosts.map((host2) => `applinks:${host2}`),
@@ -2378,6 +2638,7 @@ var expoAppConfig = (config, auth, sync, devices, runtimeFingerprint) => {
2378
2638
  ["expo-dev-client", { launchMode: "most-recent" }],
2379
2639
  ...branding ? [branding.splashPlugin] : [],
2380
2640
  "./plugins/withAbsoluteActivityResultRecovery",
2641
+ "./plugins/withAbsoluteEmbeddedAssets",
2381
2642
  ...auth || config.updates ? ["expo-secure-store"] : [],
2382
2643
  ...sync ? [
2383
2644
  "expo-sqlite",
@@ -2942,6 +3203,7 @@ import { ActivityIndicator, BackHandler, Platform, Pressable, StyleSheet, Text,
2942
3203
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
2943
3204
  import { WebView, type WebViewMessageEvent } from 'react-native-webview';
2944
3205
  import { materializeAbsoluteWebBundle } from './webAssets';
3206
+ import { absoluteEmbeddedNativeConfig } from './AbsoluteEmbeddedAssets';
2945
3207
  import { markAbsoluteEmbeddedWebPhase, markAbsoluteEmbeddedWebReady } from './AbsoluteActivityResultRecovery';
2946
3208
  import { createExpoDevicesBridgeHost } from '@absolutejs/devices-expo/bridge';
2947
3209
  import { absoluteExpoDevices, beforeAbsoluteExpoDeviceSignOut } from './AbsoluteDevices';
@@ -3061,7 +3323,16 @@ const bridgeBootstrap = (path: string, safeAreaInsets: { bottom: number; left: n
3061
3323
  event.preventDefault();
3062
3324
  send({ format: 3, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
3063
3325
  }, true);
3064
- send({ format: 3, kind: 'event', event: 'ready', path: currentPath });
3326
+ let shellRendered = false;
3327
+ addEventListener('absolute:shell-rendered', () => {
3328
+ shellRendered = true;
3329
+ send({ format: 3, kind: 'event', event: 'shell-rendered', path: currentPath });
3330
+ });
3331
+ const startupFailed = () => {
3332
+ if (!shellRendered) send({ format: 3, kind: 'event', event: 'startup-failed', path: currentPath });
3333
+ };
3334
+ addEventListener('error', startupFailed);
3335
+ addEventListener('unhandledrejection', startupFailed);
3065
3336
  })(); true;\`;
3066
3337
  };
3067
3338
 
@@ -3106,11 +3377,18 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3106
3377
  const canGoBack = useRef(false);
3107
3378
  const [runtimeReady, setRuntimeReady] = useState(!AUTH_ENABLED && !SYNC_ENABLED);
3108
3379
  const [devicesReady, setDevicesReady] = useState(false);
3109
- const [startupError, setStartupError] = useState<'assets' | 'devices'>();
3380
+ const [startupError, setStartupError] = useState<'assets' | 'devices' | 'content'>();
3110
3381
  const [startupRevision, setStartupRevision] = useState(0);
3111
3382
  const activeWebPath = useRef(pathname);
3112
3383
  const embeddedReadyReported = useRef(false);
3113
3384
  const webSource = useMemo(() => indexUri ? { uri: indexUri } : undefined, [indexUri]);
3385
+ useEffect(() => {
3386
+ if (!indexUri || DEV_ORIGIN) return;
3387
+ const timeout = setTimeout(() => {
3388
+ if (!embeddedReadyReported.current) setStartupError('content');
3389
+ }, 60000);
3390
+ return () => clearTimeout(timeout);
3391
+ }, [indexUri]);
3114
3392
 
3115
3393
  useEffect(() => {
3116
3394
  if (!AUTH_ENABLED && !SYNC_ENABLED) return;
@@ -3146,6 +3424,7 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3146
3424
  markAbsoluteEmbeddedWebPhase('assets-start');
3147
3425
  void materializeAbsoluteWebBundle().then(uri => {
3148
3426
  markAbsoluteEmbeddedWebPhase('assets-ready');
3427
+ embeddedReadyReported.current = false;
3149
3428
  setIndexUri(uri + '?absolutePath=' + encodeURIComponent(pathname));
3150
3429
  }).catch(error => {
3151
3430
  const phase = error instanceof Error && (error.message === 'assets-root-failed' || error.message === 'assets-directory-failed' || error.message === 'assets-destination-failed' || error.message === 'assets-module-failed' || error.message === 'assets-source-failed' || error.message === 'assets-download-failed' || error.message === 'assets-copy-failed' || error.message === 'assets-read-failed' || error.message === 'assets-write-failed' || error.message === 'assets-finalize-failed')
@@ -3199,15 +3478,23 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3199
3478
  try { return new URL(source).origin === origin; } catch { return false; }
3200
3479
  };
3201
3480
  const onMessage = async (event: WebViewMessageEvent) => {
3481
+ // Bridge authority belongs only to the currently loaded embedded bundle
3482
+ // (or the explicitly configured development origin), never remote pages.
3483
+ const sourceUrl = event.nativeEvent.url;
3484
+ if (!indexUri || typeof sourceUrl !== 'string' || (DEV_ORIGIN ? !hasOrigin(sourceUrl, DEV_ORIGIN) : !sourceUrl.startsWith(indexUri.slice(0, indexUri.lastIndexOf('/') + 1)))) return;
3202
3485
  const source = event.nativeEvent.data;
3203
3486
  if (new TextEncoder().encode(source).byteLength > MAX_MESSAGE_BYTES) return;
3204
3487
  let message: Record<string, unknown>;
3205
3488
  try { message = JSON.parse(source); } catch { return; }
3206
3489
  if (message.format !== BRIDGE_FORMAT || typeof message.path !== 'string' || !message.path.startsWith('/') || message.path.startsWith('//')) return;
3207
- if (message.kind === 'event' && (message.event === 'navigation' || message.event === 'ready')) {
3490
+ if (message.kind === 'event' && message.event === 'startup-failed') {
3491
+ if (!embeddedReadyReported.current) setStartupError('content');
3492
+ return;
3493
+ }
3494
+ if (message.kind === 'event' && (message.event === 'navigation' || message.event === 'shell-rendered')) {
3208
3495
  const target = new URL(message.path, PRODUCTION_ORIGIN);
3209
3496
  if (target.origin !== PRODUCTION_ORIGIN) return;
3210
- if (message.event === 'ready' && !DEV_ORIGIN && !embeddedReadyReported.current) {
3497
+ if (message.event === 'shell-rendered' && !DEV_ORIGIN && !embeddedReadyReported.current) {
3211
3498
  embeddedReadyReported.current = true;
3212
3499
  markAbsoluteEmbeddedWebReady();
3213
3500
  }
@@ -3261,12 +3548,17 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3261
3548
  </View>;
3262
3549
  if (!webSource || !runtimeReady || !devicesReady) return <View style={styles.loading}><ActivityIndicator /></View>;
3263
3550
  return <WebView
3264
- allowFileAccess
3265
- allowFileAccessFromFileURLs
3551
+ nativeConfig={!DEV_ORIGIN ? absoluteEmbeddedNativeConfig : undefined}
3552
+ allowFileAccess={Platform.OS !== 'android'}
3553
+ allowFileAccessFromFileURLs={Platform.OS !== 'android'}
3266
3554
  allowUniversalAccessFromFileURLs={false}
3555
+ originWhitelist={DEV_ORIGIN ? [DEV_ORIGIN] : [Platform.OS === 'android' ? 'https://appassets.androidplatform.net' : 'file://*']}
3267
3556
  allowingReadAccessToURL={webSource.uri.slice(0, webSource.uri.lastIndexOf('/') + 1)}
3268
3557
  injectedJavaScriptBeforeContentLoaded={bridgeBootstrap(pathname, safeAreaInsets)}
3269
3558
  onMessage={onMessage}
3559
+ onError={() => setStartupError('content')}
3560
+ onRenderProcessGone={() => setStartupError('content')}
3561
+ onContentProcessDidTerminate={() => setStartupError('content')}
3270
3562
  onNavigationStateChange={state => {
3271
3563
  canGoBack.current = state.canGoBack;
3272
3564
  if (!DEV_ORIGIN || !hasOrigin(state.url, DEV_ORIGIN)) return;
@@ -3274,8 +3566,8 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3274
3566
  activeWebPath.current = route.pathname + route.search + route.hash;
3275
3567
  }}
3276
3568
  onShouldStartLoadWithRequest={request => {
3277
- if (request.url.startsWith('file:') || hasOrigin(request.url, PRODUCTION_ORIGIN) || DEV_ORIGIN && hasOrigin(request.url, DEV_ORIGIN)) return true;
3278
- void Linking.openURL(request.url);
3569
+ if (DEV_ORIGIN ? hasOrigin(request.url, DEV_ORIGIN) : request.url.startsWith(webSource.uri.slice(0, webSource.uri.lastIndexOf('/') + 1))) return true;
3570
+ if (/^https?:/.test(request.url)) void Linking.openURL(request.url);
3279
3571
  return false;
3280
3572
  }}
3281
3573
  ref={webView}
@@ -3478,8 +3770,8 @@ const styles = StyleSheet.create({
3478
3770
  error: { color: '#b91c1c', fontSize: 16, textAlign: 'center' }
3479
3771
  });
3480
3772
  `;
3481
- var nativeWrapperSource = (wrapper, module, runtime2, route) => `${EXPO_GENERATED_HEADER}import ApplicationNativeRoute from ${JSON.stringify(portableRelative2(dirname3(wrapper), module).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
3482
- import { createAbsoluteNativeRoute, createAbsoluteNativeRouteErrorBoundary } from ${JSON.stringify(portableRelative2(dirname3(wrapper), runtime2).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
3773
+ var nativeWrapperSource = (wrapper, module, runtime3, route) => `${EXPO_GENERATED_HEADER}import ApplicationNativeRoute from ${JSON.stringify(portableRelative2(dirname3(wrapper), module).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
3774
+ import { createAbsoluteNativeRoute, createAbsoluteNativeRouteErrorBoundary } from ${JSON.stringify(portableRelative2(dirname3(wrapper), runtime3).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
3483
3775
 
3484
3776
  export const ErrorBoundary = createAbsoluteNativeRouteErrorBoundary(${JSON.stringify(route)});
3485
3777
  export default createAbsoluteNativeRoute(ApplicationNativeRoute, ${JSON.stringify(route)});
@@ -3548,7 +3840,7 @@ var pruneManagedFiles = async (paths) => {
3548
3840
  return removed.filter(Boolean).length;
3549
3841
  };
3550
3842
  var pruneStaleManagedExpoRoutes = async (project, expected) => {
3551
- const appDirectory = join6(project, "app");
3843
+ const appDirectory = join7(project, "app");
3552
3844
  if (!await exists2(appDirectory))
3553
3845
  return 0;
3554
3846
  const files = await walkFiles(appDirectory);
@@ -3596,7 +3888,7 @@ var writeAbsoluteExpoProject = async (config, options) => {
3596
3888
  if (missing) {
3597
3889
  throw new TypeError(`Expo native route ${missing.route} references missing module ${missing.module}.`);
3598
3890
  }
3599
- const marker = join6(project, EXPO_PROJECT_MARKER);
3891
+ const marker = join7(project, EXPO_PROJECT_MARKER);
3600
3892
  if (await exists2(project) && !await exists2(marker)) {
3601
3893
  const entries = await readdir(project);
3602
3894
  if (entries.length > 0 && !options.force) {
@@ -3609,7 +3901,7 @@ var writeAbsoluteExpoProject = async (config, options) => {
3609
3901
  const authEnabled = Boolean(auth);
3610
3902
  const files = new Map([
3611
3903
  [
3612
- join6(project, ".gitignore"),
3904
+ join7(project, ".gitignore"),
3613
3905
  `.expo/
3614
3906
  android/
3615
3907
  ios/
@@ -3617,80 +3909,82 @@ node_modules/
3617
3909
  `
3618
3910
  ],
3619
3911
  [
3620
- join6(project, "app.json"),
3912
+ join7(project, "app.json"),
3621
3913
  jsonSource(expoAppConfig(config, authEnabled, syncEnabled, devices, runtimeFingerprint))
3622
3914
  ],
3623
- [join6(project, "app.config.js"), expoDynamicAppConfig],
3915
+ [join7(project, "app.config.js"), expoDynamicAppConfig],
3624
3916
  [
3625
- join6(project, "package.json"),
3917
+ join7(project, "package.json"),
3626
3918
  jsonSource(expoPackage(authEnabled, config.branding !== undefined, syncEnabled, devices, config.updates !== undefined))
3627
3919
  ],
3628
- [join6(project, "metro.config.js"), metroConfig(projectRoot)],
3920
+ [join7(project, "metro.config.js"), metroConfig(projectRoot)],
3629
3921
  [
3630
- join6(project, "plugins", "withAbsoluteDevelopmentCa.js"),
3922
+ join7(project, "plugins", "withAbsoluteDevelopmentCa.js"),
3631
3923
  expoDevelopmentCaPlugin
3632
3924
  ],
3633
3925
  [
3634
- join6(project, "plugins", "withAbsoluteActivityResultRecovery.js"),
3926
+ join7(project, "plugins", "withAbsoluteActivityResultRecovery.js"),
3635
3927
  expoActivityResultRecoveryPlugin
3636
3928
  ],
3637
3929
  [
3638
- join6(project, "tsconfig.json"),
3930
+ join7(project, "tsconfig.json"),
3639
3931
  jsonSource(expoTsConfig(projectRoot, project, authEnabled, syncEnabled))
3640
3932
  ],
3641
3933
  [
3642
- join6(project, "app", "_layout.tsx"),
3934
+ join7(project, "app", "_layout.tsx"),
3643
3935
  layoutSource(authEnabled, syncEnabled, config.updates !== undefined, config.observability !== undefined)
3644
3936
  ],
3645
3937
  [
3646
- join6(project, "app", "__absolute", "native", "index.tsx"),
3938
+ join7(project, "app", "__absolute", "native", "index.tsx"),
3647
3939
  nativeDiagnosticSource
3648
3940
  ],
3649
3941
  [
3650
- join6(project, "src", "generated", "AbsoluteDevices.ts"),
3942
+ join7(project, "src", "generated", "AbsoluteDevices.ts"),
3651
3943
  devicesRuntimeSource(config, devices, authEnabled)
3652
3944
  ],
3653
3945
  [
3654
- join6(project, "src", "generated", "AbsoluteNativeRoute.tsx"),
3946
+ join7(project, "src", "generated", "AbsoluteNativeRoute.tsx"),
3655
3947
  nativeRouteRuntimeSource(authEnabled)
3656
3948
  ],
3657
3949
  [
3658
- join6(project, "src", "generated", "AbsoluteWebHost.tsx"),
3950
+ join7(project, "src", "generated", "AbsoluteWebHost.tsx"),
3659
3951
  webHostSource(config, auth, syncEnabled)
3660
3952
  ]
3661
3953
  ]);
3662
3954
  for (const [path, source] of absoluteExpoActivityResultRecoveryFiles(project))
3663
3955
  files.set(path, source);
3956
+ for (const [path, source] of absoluteExpoEmbeddedAssetsFiles(project))
3957
+ files.set(path, source);
3664
3958
  if (config.observability) {
3665
3959
  for (const [path, source] of absoluteExpoNativeObservabilityFiles(project))
3666
3960
  files.set(path, source);
3667
3961
  }
3668
3962
  if (config.updates) {
3669
- files.set(join6(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource(config));
3963
+ files.set(join7(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource(config));
3670
3964
  }
3671
3965
  const expoCodeSigning = config.updates?.expoCodeSigning;
3672
3966
  if (expoCodeSigning)
3673
- files.set(join6(project, "certs", "absolute-mobile-update.pem"), expoCodeSigning.certificatePem);
3967
+ files.set(join7(project, "certs", "absolute-mobile-update.pem"), expoCodeSigning.certificatePem);
3674
3968
  if (auth) {
3675
- files.set(join6(project, "src", "generated", "AbsoluteAuth.ts"), authRuntimeSource(auth, config.appId));
3969
+ files.set(join7(project, "src", "generated", "AbsoluteAuth.ts"), authRuntimeSource(auth, config.appId));
3676
3970
  }
3677
3971
  if (syncSchema) {
3678
- files.set(join6(project, "src", "generated", "AbsoluteSync.ts"), syncRuntimeSource(config, syncSchema));
3972
+ files.set(join7(project, "src", "generated", "AbsoluteSync.ts"), syncRuntimeSource(config, syncSchema));
3679
3973
  }
3680
- const webAssetsPath = join6(project, "src", "generated", "webAssets.ts");
3974
+ const webAssetsPath = join7(project, "src", "generated", "webAssets.ts");
3681
3975
  if (!await exists2(webAssetsPath)) {
3682
3976
  files.set(webAssetsPath, emptyWebAssetsSource);
3683
3977
  }
3684
3978
  if (!config.expoNativeRoutes["/"]) {
3685
- files.set(join6(project, "app", "index.tsx"), webRouteSource);
3979
+ files.set(join7(project, "app", "index.tsx"), webRouteSource);
3686
3980
  }
3687
- files.set(join6(project, "app", "[...absolute].tsx"), catchAllRouteSource);
3688
- const nativeRouteRuntime = join6(project, "src", "generated", "AbsoluteNativeRoute.tsx");
3981
+ files.set(join7(project, "app", "[...absolute].tsx"), catchAllRouteSource);
3982
+ const nativeRouteRuntime = join7(project, "src", "generated", "AbsoluteNativeRoute.tsx");
3689
3983
  for (const [route, module] of routeModules) {
3690
- const wrapper = route === "/" ? join6(project, "app", "index.tsx") : routeFile(project, route);
3984
+ const wrapper = route === "/" ? join7(project, "app", "index.tsx") : routeFile(project, route);
3691
3985
  files.set(wrapper, nativeWrapperSource(wrapper, module, nativeRouteRuntime, route));
3692
3986
  }
3693
- const removedRoutes = await pruneStaleManagedExpoRoutes(project, new Set([...files.keys()].filter((path) => path.startsWith(`${join6(project, "app")}${sep}`))));
3987
+ const removedRoutes = await pruneStaleManagedExpoRoutes(project, new Set([...files.keys()].filter((path) => path.startsWith(`${join7(project, "app")}${sep}`))));
3694
3988
  const removedObservability = config.observability ? 0 : await pruneManagedFiles(absoluteExpoNativeObservabilityPaths(project));
3695
3989
  const changes = await Promise.all([...files].map(([path, source]) => writeManagedFile(path, source, true)));
3696
3990
  await writeAbsoluteExpoBrandingInputs(config, projectRoot);
@@ -3700,7 +3994,7 @@ node_modules/
3700
3994
  var walkFiles = async (root, directory = root) => {
3701
3995
  const entries = await readdir(directory, { withFileTypes: true });
3702
3996
  const nested = await Promise.all(entries.map((entry) => {
3703
- const path = join6(directory, entry.name);
3997
+ const path = join7(directory, entry.name);
3704
3998
  if (entry.isDirectory())
3705
3999
  return walkFiles(root, path);
3706
4000
  if (entry.isFile())
@@ -3733,13 +4027,16 @@ var installStagedDirectory = async (staging, destination) => {
3733
4027
  throw error;
3734
4028
  }
3735
4029
  };
3736
- var assetModuleSource = (assets, archiveAsset, bundleId, manifest) => `${EXPO_GENERATED_HEADER}import { Asset } from 'expo-asset';
4030
+ var assetModuleSource = (assets, archiveAsset, bundleId, archiveMd5, manifest) => `${EXPO_GENERATED_HEADER}import { Asset } from 'expo-asset';
3737
4031
  import { Directory, File, Paths } from 'expo-file-system';
3738
4032
  import { copyAsync as copyLegacyAsync } from 'expo-file-system/legacy';
4033
+ import { registerAbsoluteEmbeddedBundle } from './AbsoluteEmbeddedAssets';
3739
4034
 
3740
4035
  ${nativeDataManifestTypeSource}
3741
4036
  declare const require: (path: string) => number;
3742
4037
  const BUNDLE_ID = ${JSON.stringify(bundleId)};
4038
+ const ARCHIVE_MD5 = ${JSON.stringify(archiveMd5)};
4039
+ const ARCHIVE_LENGTH = ${assets.reduce((sum, entry) => sum + entry.length, 0)};
3743
4040
  export const ABSOLUTE_MOBILE_MANIFEST: AbsoluteMobileManifest = ${JSON.stringify(manifest)};
3744
4041
  const ARCHIVE_MODULE = require(${JSON.stringify(archiveAsset)});
3745
4042
  const ASSETS = [
@@ -3756,10 +4053,12 @@ export const materializeAbsoluteWebBundle = async () => {
3756
4053
  let archive: File;
3757
4054
  try { archive = new File(root, '.absolutejs-bundle.absasset'); }
3758
4055
  catch { throw new Error('assets-destination-failed'); }
4056
+ if (archive.exists && (archive.size !== ARCHIVE_LENGTH || archive.md5 !== ARCHIVE_MD5)) archive.delete();
3759
4057
  if (!archive.exists) {
3760
4058
  let asset: Asset;
3761
4059
  try { asset = Asset.fromModule(ARCHIVE_MODULE); }
3762
4060
  catch { throw new Error('assets-module-failed'); }
4061
+ if (asset.type !== 'absasset' || asset.hash !== ARCHIVE_MD5) throw new Error('assets-identity-failed');
3763
4062
  try { if (!asset.localUri) asset = await asset.downloadAsync(); }
3764
4063
  catch { throw new Error('assets-download-failed'); }
3765
4064
  if (!asset.localUri) throw new Error('assets-download-failed');
@@ -3772,9 +4071,16 @@ export const materializeAbsoluteWebBundle = async () => {
3772
4071
  catch { throw new Error('assets-copy-failed'); }
3773
4072
  }
3774
4073
  }
4074
+ // Checksums detect accidental corruption/registry mismatches, not authenticity;
4075
+ // authenticity comes from the signed application/update containing this table.
4076
+ if (archive.size !== ARCHIVE_LENGTH || archive.md5 !== ARCHIVE_MD5) {
4077
+ archive.delete();
4078
+ throw new Error('assets-integrity-failed');
4079
+ }
3775
4080
  let contents: Uint8Array;
3776
4081
  try { contents = await archive.bytes(); }
3777
4082
  catch { throw new Error('assets-read-failed'); }
4083
+ if (contents.byteLength !== ARCHIVE_LENGTH) throw new Error('assets-integrity-failed');
3778
4084
  for (const entry of ASSETS) {
3779
4085
  const parts = entry.path.split('/');
3780
4086
  const name = parts.pop();
@@ -3788,27 +4094,28 @@ export const materializeAbsoluteWebBundle = async () => {
3788
4094
  let destination: File;
3789
4095
  try {
3790
4096
  destination = new File(directory, name);
3791
- if (destination.exists && destination.size === entry.length) continue;
4097
+ if (destination.exists && destination.size === entry.length && destination.md5 === entry.md5) continue;
3792
4098
  if (destination.exists) destination.delete();
3793
4099
  } catch { throw new Error('assets-destination-failed'); }
3794
4100
  try { destination.write(contents.slice(entry.offset, entry.offset + entry.length)); }
3795
4101
  catch { throw new Error('assets-write-failed'); }
4102
+ if (destination.size !== entry.length || destination.md5 !== entry.md5) throw new Error('assets-integrity-failed');
3796
4103
  }
3797
4104
  try { archive.delete(); }
3798
4105
  catch { throw new Error('assets-finalize-failed'); }
3799
4106
 
3800
- try { return new File(root, 'index.html').uri; }
4107
+ try { return await registerAbsoluteEmbeddedBundle(root.uri, ASSETS.map(entry => entry.path), new File(root, 'index.html').uri); }
3801
4108
  catch { throw new Error('assets-finalize-failed'); }
3802
4109
  };
3803
4110
  `;
3804
4111
  var syncAbsoluteExpoWebAssets = async (config) => {
3805
4112
  if (config.engine !== "expo")
3806
4113
  throw new TypeError("Expo asset sync requires mobile.engine: expo.");
3807
- const marker = join6(config.nativeProjectDirectory, EXPO_PROJECT_MARKER);
4114
+ const marker = join7(config.nativeProjectDirectory, EXPO_PROJECT_MARKER);
3808
4115
  if (!await exists2(marker)) {
3809
4116
  throw new TypeError("Expo asset sync requires an AbsoluteJS-managed Expo project. Run mobile init first.");
3810
4117
  }
3811
- const manifestPath = join6(config.bundleDirectory, "absolute-mobile-manifest.json");
4118
+ const manifestPath = join7(config.bundleDirectory, "absolute-mobile-manifest.json");
3812
4119
  const manifest = JSON.parse(await readFile2(manifestPath, "utf8"));
3813
4120
  const nativeDataManifest = expoNativeDataManifest(manifest);
3814
4121
  const { appBuild } = nativeDataManifest;
@@ -3822,16 +4129,18 @@ var syncAbsoluteExpoWebAssets = async (config) => {
3822
4129
  bundleHash.update("\x00");
3823
4130
  });
3824
4131
  const bundleId = `amexpo_${bundleHash.digest("hex")}`;
3825
- const destination = join6(config.nativeProjectDirectory, "assets", "absolute");
4132
+ const destination = join7(config.nativeProjectDirectory, "assets", "absolute");
3826
4133
  await mkdir2(dirname3(destination), { recursive: true });
3827
- const staging = await mkdtemp(join6(dirname3(destination), `.${basename(destination)}.stage-`));
4134
+ const staging = await mkdtemp(join7(dirname3(destination), `.${basename(destination)}.stage-`));
3828
4135
  let assets;
3829
4136
  let archiveAsset;
4137
+ let archiveMd5;
3830
4138
  try {
3831
4139
  let offset = 0;
3832
4140
  assets = filesWithContents.map(({ contents, file }) => {
3833
4141
  const entry = {
3834
4142
  length: contents.byteLength,
4143
+ md5: createHash3("md5").update(contents).digest("hex"),
3835
4144
  offset,
3836
4145
  path: relative3(config.bundleDirectory, file).replaceAll("\\", "/")
3837
4146
  };
@@ -3839,15 +4148,17 @@ var syncAbsoluteExpoWebAssets = async (config) => {
3839
4148
  return entry;
3840
4149
  });
3841
4150
  const name = `bundle${EXPO_ASSET_EXTENSION}`;
3842
- await writeFile2(join6(staging, name), Buffer.concat(filesWithContents.map(({ contents }) => contents)));
3843
- archiveAsset = portableRelative2(join6(config.nativeProjectDirectory, "src", "generated"), join6(destination, name));
4151
+ const archiveContents = Buffer.concat(filesWithContents.map(({ contents }) => contents));
4152
+ archiveMd5 = createHash3("md5").update(archiveContents).digest("hex");
4153
+ await writeFile2(join7(staging, name), archiveContents);
4154
+ archiveAsset = portableRelative2(join7(config.nativeProjectDirectory, "src", "generated"), join7(destination, name));
3844
4155
  await installStagedDirectory(staging, destination);
3845
4156
  } catch (error) {
3846
4157
  await rm2(staging, { force: true, recursive: true });
3847
4158
  throw error;
3848
4159
  }
3849
- const generated = join6(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
3850
- await writeManagedFile(generated, assetModuleSource(assets, archiveAsset, bundleId, nativeDataManifest), true);
4160
+ const generated = join7(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
4161
+ await writeManagedFile(generated, assetModuleSource(assets, archiveAsset, bundleId, archiveMd5, nativeDataManifest), true);
3851
4162
  return { appBuild, assets: assets.length, bundleId, path: destination };
3852
4163
  };
3853
4164