@absolutejs/absolute 0.20.0-beta.115 → 0.20.0-beta.117

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/changelog.json +38 -1
  3. package/dist/angular/components/core/streamingSlotRegistrar.js +1 -1
  4. package/dist/angular/components/core/streamingSlotRegistry.js +2 -2
  5. package/dist/build.js +9 -4
  6. package/dist/build.js.map +5 -4
  7. package/dist/cli/{androidEmulatorController-nybhzc6c.js → androidEmulatorController-v6chbhem.js} +2 -2
  8. package/dist/cli/{androidNativeWatcher-2930tb8c.js → androidNativeWatcher-mgqws6ag.js} +2 -2
  9. package/dist/cli/{compile-ya4kd2sg.js → compile-5m830csy.js} +2 -2
  10. package/dist/cli/{dev-5cqmf7za.js → dev-ww30tdrv.js} +10 -7
  11. package/dist/cli/{expoProject-f5j20xms.js → expoProject-w2pzjwnx.js} +1 -1
  12. package/dist/cli/{index-28pbrd3q.js → index-18q9qc7f.js} +15 -2
  13. package/dist/cli/{index-5fmgn0nx.js → index-5268nwjx.js} +1 -1
  14. package/dist/cli/{index-5skbydh0.js → index-hneb973j.js} +1 -1
  15. package/dist/cli/{index-6mbm4n3g.js → index-k8r00xh9.js} +1 -1
  16. package/dist/cli/{index-dr21p6ae.js → index-nxcwg9ma.js} +590 -152
  17. package/dist/cli/{index-qr0pc777.js → index-wtyg9cy0.js} +1 -1
  18. package/dist/cli/index.js +5 -5
  19. package/dist/cli/{iosNativeWatcher-t6gv0p1w.js → iosNativeWatcher-8w1gawg4.js} +2 -2
  20. package/dist/cli/{iosSimulatorController-5n2a9fxv.js → iosSimulatorController-5vhqvhg6.js} +2 -2
  21. package/dist/cli/{mobile-fg2226vv.js → mobile-21vsmrjd.js} +47 -56
  22. package/dist/cli/{start-jt61f40d.js → start-3bhf56tj.js} +3 -3
  23. package/dist/index.js +12 -6
  24. package/dist/index.js.map +6 -5
  25. package/dist/mobile/index.js +692 -274
  26. package/dist/mobile/index.js.map +8 -6
  27. package/dist/mobile/remoteMacAgentEntry.js +564 -231
  28. package/dist/mobile/shellBootstrap.js +6 -1
  29. package/dist/react/hooks/index.js.map +2 -2
  30. package/dist/src/dev/serverEntryCopies.d.ts +2 -0
  31. package/dist/src/mobile/capacitorProject.d.ts +4 -0
  32. package/dist/src/mobile/expoEmbeddedAssets.d.ts +1 -0
  33. package/dist/src/mobile/nativePackages.d.ts +10 -0
  34. package/dist/src/mobile/updateRuntime.d.ts +1 -0
  35. package/package.json +6 -2
@@ -16,17 +16,80 @@ import {
16
16
  mkdir as mkdir2,
17
17
  mkdtemp,
18
18
  readdir,
19
- readFile as readFile2,
19
+ readFile as readFile3,
20
20
  rename as rename2,
21
21
  rm as rm2,
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 dirname4, join as join8, relative as relative3, resolve as resolve5, sep } from "path";
26
+
27
+ // src/mobile/nativePackages.ts
28
+ import { readFile } from "fs/promises";
29
+ import { dirname, join, resolve } from "path";
30
+ var ABSOLUTE_CAPACITOR_DEVICES_VERSION = "0.8.0";
31
+ var ABSOLUTE_CAPACITOR_SYNC_VERSION = "0.9.3";
32
+ var ABSOLUTE_DEVICES_VERSION = "0.7.0";
33
+ var ABSOLUTE_EXPO_DEVICES_VERSION = "0.0.11";
34
+ var ABSOLUTE_NATIVE_EXACT_PACKAGES = new Set([
35
+ "@absolutejs/devices",
36
+ "@absolutejs/devices-capacitor",
37
+ "@absolutejs/sync-capacitor"
38
+ ]);
39
+ var CAPACITOR_PACKAGE_SPECS = [
40
+ "@capacitor/core@8.5.0",
41
+ "@capacitor/app@8.1.1",
42
+ "@capacitor/browser@8.0.4",
43
+ "@capacitor/network@8.0.1",
44
+ "@capacitor/preferences@8.0.1",
45
+ "@capacitor/cli@8.5.0",
46
+ "@capacitor/android@8.5.0",
47
+ "@capacitor/ios@8.5.0",
48
+ `@absolutejs/devices@${ABSOLUTE_DEVICES_VERSION}`,
49
+ `@absolutejs/devices-capacitor@${ABSOLUTE_CAPACITOR_DEVICES_VERSION}`
50
+ ];
51
+ var CAPACITOR_SYNC_PACKAGE_SPECS = [
52
+ `@absolutejs/sync-capacitor@${ABSOLUTE_CAPACITOR_SYNC_VERSION}`,
53
+ "@capacitor-community/sqlite@8.1.1"
54
+ ];
55
+ var resolvedPackageVersion = async (projectRoot, name) => {
56
+ const directory = resolve(projectRoot);
57
+ let source;
58
+ try {
59
+ source = await readFile(join(directory, "node_modules", name, "package.json"), "utf8");
60
+ } catch (error) {
61
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT")
62
+ return;
63
+ const parent = dirname(directory);
64
+ return parent === directory ? undefined : resolvedPackageVersion(parent, name);
65
+ }
66
+ try {
67
+ const manifest = JSON.parse(source);
68
+ return typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "version") : undefined;
69
+ } catch {
70
+ return;
71
+ }
72
+ };
73
+ var assertNativePackageVersions = async (projectRoot, specs, exactPackages) => {
74
+ const stale = await packagesNeedingExactInstall(projectRoot, specs, new Set(specs.map(packageNameFromSpec)), exactPackages);
75
+ if (stale.length > 0)
76
+ throw new TypeError(`Native dependency installation did not resolve the tested versions: ${stale.join(", ")}. A stale workspace-local installation may be shadowing hoisted packages. Restore a clean dependency installation for this app/workspace with your package manager, then retry absolute mobile init or sync. A forced install alone may leave obsolete workspace-local copies in place.`);
77
+ };
78
+ var packageNameFromSpec = (spec) => spec.slice(0, spec.lastIndexOf("@"));
79
+ var packagesNeedingExactInstall = async (projectRoot, specs, installed, exactPackages) => (await Promise.all(specs.map(async (spec) => {
80
+ const name = packageNameFromSpec(spec);
81
+ if (!installed.has(name))
82
+ return spec;
83
+ if (!exactPackages.has(name))
84
+ return;
85
+ if (await resolvedPackageVersion(projectRoot, name) === spec.slice(spec.lastIndexOf("@") + 1))
86
+ return;
87
+ return spec;
88
+ }))).filter((spec) => spec !== undefined);
26
89
 
27
90
  // src/mobile/syncSchema.ts
28
91
  import { readFileSync } from "fs";
29
- import { dirname, join, resolve } from "path";
92
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
30
93
 
31
94
  // node_modules/@absolutejs/sync/dist/client/index.js
32
95
  var RUNTIME_TRANSPORT = Symbol.for("@absolutejs/sync/client-runtime-transport");
@@ -183,13 +246,13 @@ var localSchemaMetadata = (manifest) => {
183
246
  return Reflect.get(sync, "localSchema");
184
247
  };
185
248
  var packageManifestPath = (projectRoot, packageName) => {
186
- let directory = resolve(projectRoot);
249
+ let directory = resolve2(projectRoot);
187
250
  while (true) {
188
- const candidate = join(directory, "node_modules", packageName, "package.json");
251
+ const candidate = join2(directory, "node_modules", packageName, "package.json");
189
252
  const manifest = manifestAt(candidate);
190
253
  if (manifest && Reflect.get(manifest, "name") === packageName)
191
254
  return candidate;
192
- const parent = dirname(directory);
255
+ const parent = dirname2(directory);
193
256
  if (parent === directory)
194
257
  return;
195
258
  directory = parent;
@@ -428,7 +491,7 @@ var dependencyNames = (manifest) => [
428
491
  Reflect.get(manifest, "peerDependencies")
429
492
  ].flatMap((dependencies) => object(dependencies) ? Object.keys(dependencies) : []).filter((name, index, names) => names.indexOf(name) === index).sort();
430
493
  var discoverAbsoluteSyncSchema = (projectRoot) => {
431
- const appManifestPath = join(resolve(projectRoot), "package.json");
494
+ const appManifestPath = join2(resolve2(projectRoot), "package.json");
432
495
  const appManifest = manifestAt(appManifestPath);
433
496
  if (!appManifest)
434
497
  return {
@@ -469,7 +532,7 @@ var discoverAbsoluteSyncSchema = (projectRoot) => {
469
532
 
470
533
  // src/mobile/deviceCapabilities.ts
471
534
  import { existsSync, readFileSync as readFileSync2 } from "fs";
472
- import { extname, join as join2, relative, resolve as resolve2 } from "path";
535
+ import { extname, join as join3, relative, resolve as resolve3 } from "path";
473
536
  import { fileURLToPath } from "url";
474
537
  import ts from "typescript";
475
538
  var DEVICES_PACKAGE = "@absolutejs/devices";
@@ -644,7 +707,7 @@ var absoluteDeviceNativeRequirements = (plan) => {
644
707
  };
645
708
  var loadAbsoluteDeviceCapabilityProviders = (projectRoot, provider = "capacitor") => {
646
709
  const adapter = ADAPTERS[provider];
647
- let path = join2(resolve2(projectRoot), "node_modules", adapter, "package.json");
710
+ let path = join3(resolve3(projectRoot), "node_modules", adapter, "package.json");
648
711
  try {
649
712
  readFileSync2(path, "utf8");
650
713
  } catch {
@@ -702,7 +765,7 @@ var assertAbsoluteDeviceCapabilityPackages = (projectRoot, plan) => {
702
765
  if (missing.includes(spec))
703
766
  return false;
704
767
  try {
705
- return readJson(join2(resolve2(projectRoot), "node_modules", packageName, "package.json")).version !== spec.slice(separator + 1);
768
+ return readJson(join3(resolve3(projectRoot), "node_modules", packageName, "package.json")).version !== spec.slice(separator + 1);
706
769
  } catch {
707
770
  return true;
708
771
  }
@@ -712,7 +775,7 @@ var assertAbsoluteDeviceCapabilityPackages = (projectRoot, plan) => {
712
775
  throw new TypeError(`Device capabilities ${plan.capabilities.join(", ")} require ${unmet.join(", ")}. Run absolute mobile sync and approve the detected capability plugins.`);
713
776
  };
714
777
  var directAbsoluteProjectPackages = (projectRoot) => {
715
- const manifest = readJson(join2(resolve2(projectRoot), "package.json"));
778
+ const manifest = readJson(join3(resolve3(projectRoot), "package.json"));
716
779
  const packages = new Set;
717
780
  for (const field of ["dependencies", "devDependencies"]) {
718
781
  const dependencies = manifest[field];
@@ -723,16 +786,16 @@ var directAbsoluteProjectPackages = (projectRoot) => {
723
786
  return packages;
724
787
  };
725
788
  var discoverAbsoluteDeviceCapabilities = (projectRoot, providers = loadAbsoluteDeviceCapabilityProviders(projectRoot)) => {
726
- const root = resolve2(projectRoot);
789
+ const root = resolve3(projectRoot);
727
790
  if (!existsSync(root))
728
791
  return [];
729
792
  const known = new Set(Object.keys(providers));
730
793
  const capabilities = new Set;
731
794
  for (const path of SOURCE_GLOB.scanSync({ cwd: root })) {
732
- const portable = relative(root, resolve2(root, path)).replaceAll("\\", "/");
795
+ const portable = relative(root, resolve3(root, path)).replaceAll("\\", "/");
733
796
  if (isIgnored(portable))
734
797
  continue;
735
- const source = readFileSync2(resolve2(root, portable), "utf8");
798
+ const source = readFileSync2(resolve3(root, portable), "utf8");
736
799
  for (const name of importedCapabilities(source, portable))
737
800
  if (known.has(name))
738
801
  capabilities.add(name);
@@ -902,6 +965,7 @@ var createAbsoluteMobileUpdateRuntimeDescriptor = (options) => ({
902
965
  },
903
966
  deviceCapabilities: options.deviceCapabilities,
904
967
  engine: options.config.engine,
968
+ ...options.config.engine === "expo" ? { expoEmbeddedAssetsAbi: 1 } : {},
905
969
  format: ABSOLUTE_MOBILE_UPDATE_RUNTIME_FORMAT,
906
970
  shellAbi: ABSOLUTE_MOBILE_SHELL_ABI,
907
971
  syncSchema: options.syncSchema ?? null,
@@ -930,7 +994,7 @@ var resolveAbsoluteMobileUpdateRuntime = (config, projectRoot) => {
930
994
  };
931
995
 
932
996
  // src/mobile/expoNativeObservability.ts
933
- import { join as join3 } from "path";
997
+ import { join as join4 } from "path";
934
998
  var HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
935
999
  `;
936
1000
  var XML_HEADER = `<!-- Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync. -->
@@ -1372,31 +1436,31 @@ export const enqueueAbsoluteExpoNativeObservabilityForTesting = async (testId: s
1372
1436
  `;
1373
1437
  var absoluteExpoNativeObservabilityFiles = (project) => new Map([
1374
1438
  [
1375
- join3(project, "modules/absolute-mobile-observability/expo-module.config.json"),
1439
+ join4(project, "modules/absolute-mobile-observability/expo-module.config.json"),
1376
1440
  moduleConfig
1377
1441
  ],
1378
1442
  [
1379
- join3(project, "modules/absolute-mobile-observability/ios/AbsoluteMobileObservability.podspec"),
1443
+ join4(project, "modules/absolute-mobile-observability/ios/AbsoluteMobileObservability.podspec"),
1380
1444
  podspec
1381
1445
  ],
1382
1446
  [
1383
- join3(project, "modules/absolute-mobile-observability/ios/AbsoluteMobileObservabilityModule.swift"),
1447
+ join4(project, "modules/absolute-mobile-observability/ios/AbsoluteMobileObservabilityModule.swift"),
1384
1448
  iosModule
1385
1449
  ],
1386
1450
  [
1387
- join3(project, "modules/absolute-mobile-observability/android/build.gradle"),
1451
+ join4(project, "modules/absolute-mobile-observability/android/build.gradle"),
1388
1452
  androidBuild
1389
1453
  ],
1390
1454
  [
1391
- join3(project, "modules/absolute-mobile-observability/android/src/main/AndroidManifest.xml"),
1455
+ join4(project, "modules/absolute-mobile-observability/android/src/main/AndroidManifest.xml"),
1392
1456
  androidManifest
1393
1457
  ],
1394
1458
  [
1395
- join3(project, "modules/absolute-mobile-observability/android/src/main/java/expo/modules/absolutemobileobservability/AbsoluteMobileObservabilityModule.kt"),
1459
+ join4(project, "modules/absolute-mobile-observability/android/src/main/java/expo/modules/absolutemobileobservability/AbsoluteMobileObservabilityModule.kt"),
1396
1460
  androidModule
1397
1461
  ],
1398
1462
  [
1399
- join3(project, "src/generated/AbsoluteNativeObservability.ts"),
1463
+ join4(project, "src/generated/AbsoluteNativeObservability.ts"),
1400
1464
  expoNativeObservabilityRuntimeSource
1401
1465
  ]
1402
1466
  ]);
@@ -1405,7 +1469,7 @@ var absoluteExpoNativeObservabilityPaths = (project) => [
1405
1469
  ];
1406
1470
 
1407
1471
  // src/mobile/expoActivityResultRecovery.ts
1408
- import { join as join4 } from "path";
1472
+ import { join as join5 } from "path";
1409
1473
  var HEADER2 = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
1410
1474
  `;
1411
1475
  var XML_HEADER2 = `<!-- Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync. -->
@@ -1642,47 +1706,306 @@ export const takeAbsoluteActivityResultCancellation = () =>
1642
1706
  `;
1643
1707
  var absoluteExpoActivityResultRecoveryFiles = (project) => new Map([
1644
1708
  [
1645
- join4(project, "modules/absolute-activity-result-recovery/expo-module.config.json"),
1709
+ join5(project, "modules/absolute-activity-result-recovery/expo-module.config.json"),
1646
1710
  moduleConfig2
1647
1711
  ],
1648
1712
  [
1649
- join4(project, "modules/absolute-activity-result-recovery/ios/AbsoluteActivityResultRecovery.podspec"),
1713
+ join5(project, "modules/absolute-activity-result-recovery/ios/AbsoluteActivityResultRecovery.podspec"),
1650
1714
  podspec2
1651
1715
  ],
1652
1716
  [
1653
- join4(project, "modules/absolute-activity-result-recovery/ios/AbsoluteActivityResultRecoveryModule.swift"),
1717
+ join5(project, "modules/absolute-activity-result-recovery/ios/AbsoluteActivityResultRecoveryModule.swift"),
1654
1718
  iosModule2
1655
1719
  ],
1656
1720
  [
1657
- join4(project, "modules/absolute-activity-result-recovery/android/build.gradle"),
1721
+ join5(project, "modules/absolute-activity-result-recovery/android/build.gradle"),
1658
1722
  androidBuild2
1659
1723
  ],
1660
1724
  [
1661
- join4(project, "modules/absolute-activity-result-recovery/android/src/main/AndroidManifest.xml"),
1725
+ join5(project, "modules/absolute-activity-result-recovery/android/src/main/AndroidManifest.xml"),
1662
1726
  androidManifest2
1663
1727
  ],
1664
1728
  [
1665
- join4(project, "modules/absolute-activity-result-recovery/android/src/main/java/expo/modules/absoluteactivityresultrecovery/AbsoluteActivityResultRecoveryModule.kt"),
1729
+ join5(project, "modules/absolute-activity-result-recovery/android/src/main/java/expo/modules/absoluteactivityresultrecovery/AbsoluteActivityResultRecoveryModule.kt"),
1666
1730
  androidModule2
1667
1731
  ],
1668
1732
  [
1669
- join4(project, "src/generated/AbsoluteActivityResultRecovery.ts"),
1733
+ join5(project, "src/generated/AbsoluteActivityResultRecovery.ts"),
1670
1734
  runtime
1671
1735
  ]
1672
1736
  ]);
1673
1737
 
1738
+ // src/mobile/expoEmbeddedAssets.ts
1739
+ import { join as join6 } from "path";
1740
+ var header = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
1741
+ `;
1742
+ var android = `${header}package expo.modules.absoluteembeddedassets
1743
+
1744
+ import android.content.Context
1745
+ import android.graphics.Bitmap
1746
+ import android.net.Uri
1747
+ import android.util.Log
1748
+ import android.webkit.WebResourceRequest
1749
+ import android.webkit.WebResourceResponse
1750
+ import android.webkit.WebView
1751
+ import androidx.webkit.WebViewAssetLoader
1752
+ import androidx.webkit.WebViewCompat
1753
+ import androidx.webkit.WebViewFeature
1754
+ import androidx.webkit.ScriptHandler
1755
+ import androidx.webkit.WebMessageCompat
1756
+ import com.facebook.react.ReactPackage
1757
+ import com.facebook.react.bridge.NativeModule
1758
+ import com.facebook.react.bridge.ReactApplicationContext
1759
+ import com.facebook.react.bridge.ReadableMap
1760
+ import com.facebook.react.uimanager.ThemedReactContext
1761
+ import com.facebook.react.uimanager.ViewManager
1762
+ import com.reactnativecommunity.webview.RNCWebViewClient
1763
+ import com.reactnativecommunity.webview.RNCWebViewManager
1764
+ import com.reactnativecommunity.webview.RNCWebViewWrapper
1765
+ import expo.modules.kotlin.modules.Module
1766
+ import expo.modules.kotlin.modules.ModuleDefinition
1767
+ import java.io.ByteArrayInputStream
1768
+ import java.io.File
1769
+ import java.util.concurrent.ConcurrentHashMap
1770
+ import java.util.WeakHashMap
1771
+
1772
+ private const val HOST = "appassets.androidplatform.net"
1773
+ private const val PREFIX = "/absolutejs/"
1774
+
1775
+ internal object AbsoluteEmbeddedAssets {
1776
+ private val bundles = ConcurrentHashMap<String, Map<String, File>>()
1777
+
1778
+ fun register(context: Context, rootUri: String, paths: List<String>): String {
1779
+ check(WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) &&
1780
+ WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
1781
+ "Update Android System WebView to load this application."
1782
+ }
1783
+ val uri = Uri.parse(rootUri)
1784
+ require(uri.scheme == "file" && uri.authority.isNullOrEmpty() && uri.query == null && uri.fragment == null)
1785
+ val root = File(requireNotNull(uri.path)).canonicalFile
1786
+ val parent = File(context.filesDir, "absolutejs-web").canonicalFile
1787
+ require(root.parentFile == parent && root.name.matches(Regex("amexpo_[a-f0-9]{64}")))
1788
+ require(paths.isNotEmpty() && paths.size <= 100000 && paths.distinct().size == paths.size)
1789
+ val files = paths.associateWith { path ->
1790
+ require(path.isNotEmpty() && !path.contains('\\\\') && !path.contains('%') &&
1791
+ !path.contains('?') && !path.contains('#') && path.indexOf(0.toChar()) < 0 &&
1792
+ path.split('/').all { it.isNotEmpty() && it != "." && it != ".." })
1793
+ val file = File(root, path).canonicalFile
1794
+ require(file.path.startsWith(root.path + File.separator) && file.isFile)
1795
+ file
1796
+ }
1797
+ require(files.containsKey("index.html") && files.containsKey("absolute-mobile-manifest.json"))
1798
+ bundles[root.name] = files
1799
+ Log.i("AbsoluteJS", "Expo embedded web phase: asset-host-registered")
1800
+ return "https://" + HOST + PREFIX + root.name + "/index.html"
1801
+ }
1802
+
1803
+ private fun empty(status: Int, reason: String) = WebResourceResponse(
1804
+ "text/plain", "UTF-8", status, reason,
1805
+ mapOf("Cache-Control" to "no-store", "X-Content-Type-Options" to "nosniff"),
1806
+ ByteArrayInputStream(ByteArray(0))
1807
+ )
1808
+
1809
+ private fun resource(path: String): WebResourceResponse {
1810
+ val bundle = path.substringBefore('/')
1811
+ val name = path.substringAfter('/', "")
1812
+ val file = bundles[bundle]?.get(name) ?: return empty(404, "Not Found")
1813
+ // Recheck canonical location at read time. Never follow a replaced symlink.
1814
+ if (file.canonicalFile != file || !file.isFile) return empty(404, "Not Found")
1815
+ val mime = when (file.extension.lowercase()) {
1816
+ "html", "htm" -> "text/html"
1817
+ "js", "mjs" -> "text/javascript"
1818
+ "json", "map" -> "application/json"
1819
+ "css" -> "text/css"
1820
+ "svg" -> "image/svg+xml"
1821
+ "wasm" -> "application/wasm"
1822
+ else -> android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension.lowercase()) ?: "application/octet-stream"
1823
+ }
1824
+ return try {
1825
+ WebResourceResponse(mime, "UTF-8", 200, "OK",
1826
+ mapOf("Cache-Control" to "no-store", "X-Content-Type-Options" to "nosniff"), file.inputStream())
1827
+ } catch (_: Exception) { empty(404, "Not Found") }
1828
+ }
1829
+
1830
+ private val loader = WebViewAssetLoader.Builder()
1831
+ .setDomain(HOST).setHttpAllowed(false)
1832
+ .addPathHandler(PREFIX) { path -> resource(path) }.build()
1833
+
1834
+ fun intercept(request: WebResourceRequest, activeBundle: String?): WebResourceResponse? {
1835
+ val uri = request.url
1836
+ if (uri.host != HOST) return null
1837
+ if (uri.path.orEmpty().endsWith("/index.html")) Log.i("AbsoluteJS", "Expo embedded web phase: document-request")
1838
+ if (uri.path.orEmpty().endsWith("/absolute-mobile-bootstrap.js")) Log.i("AbsoluteJS", "Expo embedded web phase: bootstrap-request")
1839
+ // A reserved asset host must never fall through to DNS/network, even for
1840
+ // unknown bundles, wrong ports/schemes, methods, or malformed paths.
1841
+ if (uri.scheme != "https" || uri.port != -1 || uri.userInfo != null ||
1842
+ request.method != "GET") return empty(403, "Forbidden")
1843
+ // Root-relative image/font/CSS URLs retain their web meaning. Scope them
1844
+ // to this WebView's bundle, not the last bundle registered by another view.
1845
+ if (!uri.path.orEmpty().startsWith(PREFIX)) {
1846
+ if (request.isForMainFrame || activeBundle == null) return empty(404, "Not Found")
1847
+ return resource(activeBundle + "/" + uri.path.orEmpty().removePrefix("/"))
1848
+ }
1849
+ return loader.shouldInterceptRequest(uri) ?: empty(404, "Not Found")
1850
+ }
1851
+ }
1852
+
1853
+ class AbsoluteEmbeddedAssetsModule : Module() {
1854
+ override fun definition() = ModuleDefinition {
1855
+ Name("AbsoluteEmbeddedAssets")
1856
+ AsyncFunction("registerBundle") { root: String, paths: List<String> ->
1857
+ AbsoluteEmbeddedAssets.register(requireNotNull(appContext.reactContext), root, paths)
1858
+ }
1859
+ }
1860
+ }
1861
+
1862
+ class AbsoluteEmbeddedWebViewManager : RNCWebViewManager() {
1863
+ private val scripts = WeakHashMap<RNCWebViewWrapper, ScriptHandler>()
1864
+ override fun getName() = "AbsoluteEmbeddedWebView"
1865
+ override fun setNewSource(view: RNCWebViewWrapper, value: ReadableMap?) {
1866
+ Log.i("AbsoluteJS", "Expo embedded web phase: source-assigned")
1867
+ super.setNewSource(view, value)
1868
+ }
1869
+ override fun setMessagingEnabled(view: RNCWebViewWrapper, value: Boolean) {
1870
+ super.setMessagingEnabled(view, value)
1871
+ // Upstream reports sourceOrigin as event.url and accepts subframe messages.
1872
+ // Retain its event dispatch, but bind this bridge to our main-frame origin
1873
+ // and attach the native-observed document URL, never a JS-supplied URL.
1874
+ if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) return
1875
+ WebViewCompat.removeWebMessageListener(view.webView, "ReactNativeWebView")
1876
+ if (!value) return
1877
+ WebViewCompat.addWebMessageListener(view.webView, "ReactNativeWebView", setOf("https://" + HOST)) {
1878
+ webView, message, sourceOrigin, isMainFrame, _ ->
1879
+ if (isMainFrame && sourceOrigin.scheme == "https" && sourceOrigin.host == HOST &&
1880
+ message.type == WebMessageCompat.TYPE_STRING) {
1881
+ val url = webView.url
1882
+ val data = message.data
1883
+ if (url != null && data != null) view.webView.onMessage(data, url)
1884
+ }
1885
+ }
1886
+ }
1887
+ override fun setInjectedJavaScriptBeforeContentLoaded(view: RNCWebViewWrapper, value: String?) {
1888
+ scripts.remove(view)?.remove()
1889
+ if (value == null) return
1890
+ // onPageStarted injection races module execution on Android. Register with
1891
+ // Chromium before navigation, scoped to our virtual asset origin only.
1892
+ check(WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
1893
+ "AbsoluteJS requires an Android System WebView with document-start script support."
1894
+ }
1895
+ scripts[view] = WebViewCompat.addDocumentStartJavaScript(view.webView, value, setOf("https://" + HOST))
1896
+ }
1897
+ override fun onDropViewInstance(view: RNCWebViewWrapper) {
1898
+ scripts.remove(view)?.remove()
1899
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER))
1900
+ WebViewCompat.removeWebMessageListener(view.webView, "ReactNativeWebView")
1901
+ super.onDropViewInstance(view)
1902
+ }
1903
+ override fun addEventEmitters(context: ThemedReactContext, view: RNCWebViewWrapper) {
1904
+ Log.i("AbsoluteJS", "Expo embedded web phase: asset-host-view-created")
1905
+ view.webView.webViewClient = object : RNCWebViewClient() {
1906
+ @Volatile private var activeBundle: String? = null
1907
+ override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
1908
+ Log.i("AbsoluteJS", "Expo embedded web phase: document-started")
1909
+ val uri = Uri.parse(url)
1910
+ activeBundle = if (uri.scheme == "https" && uri.host == HOST && uri.path.orEmpty().startsWith(PREFIX))
1911
+ uri.path!!.removePrefix(PREFIX).substringBefore('/') else null
1912
+ super.onPageStarted(view, url, favicon)
1913
+ }
1914
+ override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? =
1915
+ AbsoluteEmbeddedAssets.intercept(request, activeBundle) ?: super.shouldInterceptRequest(view, request)
1916
+ }
1917
+ }
1918
+ }
1919
+
1920
+ class AbsoluteEmbeddedWebViewPackage : ReactPackage {
1921
+ override fun createNativeModules(context: ReactApplicationContext): List<NativeModule> = emptyList()
1922
+ override fun createViewManagers(context: ReactApplicationContext): List<ViewManager<*, *>> = listOf(AbsoluteEmbeddedWebViewManager())
1923
+ }
1924
+ `;
1925
+ var plugin = `${header}const { withMainApplication } = require('expo/config-plugins');
1926
+ module.exports = config => withMainApplication(config, value => {
1927
+ if (value.modResults.language !== 'kt') throw new Error('AbsoluteJS embedded assets require the managed Kotlin application.');
1928
+ const name = 'expo.modules.absoluteembeddedassets.AbsoluteEmbeddedWebViewPackage';
1929
+ const registration = 'add(' + name + '())';
1930
+ const source = value.modResults.contents;
1931
+ if (!source.includes(registration)) {
1932
+ const anchor = 'PackageList(this).packages.apply {';
1933
+ if (source.split(anchor).length !== 2) throw new Error('Cannot safely register AbsoluteJS embedded WebView in MainApplication.');
1934
+ value.modResults.contents = source.replace(anchor, anchor + '\\n ' + registration);
1935
+ }
1936
+ return value;
1937
+ });
1938
+ `;
1939
+ var runtime2 = `${header}import { requireNativeModule } from 'expo-modules-core';
1940
+ import { Platform, requireNativeComponent } from 'react-native';
1941
+
1942
+ export const absoluteEmbeddedNativeConfig = Platform.OS === 'android'
1943
+ ? { component: requireNativeComponent('AbsoluteEmbeddedWebView') }
1944
+ : undefined;
1945
+
1946
+ export const registerAbsoluteEmbeddedBundle = async (rootUri: string, paths: string[], indexUri: string): Promise<string> => {
1947
+ if (Platform.OS !== 'android') return indexUri;
1948
+ const native = requireNativeModule<{ registerBundle(root: string, paths: string[]): Promise<string> }>('AbsoluteEmbeddedAssets');
1949
+ return native.registerBundle(rootUri, paths);
1950
+ };
1951
+ `;
1952
+ var absoluteExpoEmbeddedAssetsFiles = (project) => {
1953
+ const root = join6(project, "modules/absolute-embedded-assets");
1954
+ return new Map([
1955
+ [
1956
+ join6(root, "expo-module.config.json"),
1957
+ `${JSON.stringify({
1958
+ android: {
1959
+ modules: [
1960
+ "expo.modules.absoluteembeddedassets.AbsoluteEmbeddedAssetsModule"
1961
+ ]
1962
+ },
1963
+ platforms: ["android"]
1964
+ }, null, 2)}
1965
+ `
1966
+ ],
1967
+ [
1968
+ join6(root, "android/build.gradle"),
1969
+ `${header}plugins { id 'com.android.library'; id 'expo-module-gradle-plugin' }
1970
+ group = 'expo.modules.absoluteembeddedassets'
1971
+ version = '0.0.1'
1972
+ android {
1973
+ namespace 'expo.modules.absoluteembeddedassets'
1974
+ defaultConfig { versionCode 1; versionName '0.0.1' }
1975
+ }
1976
+ dependencies {
1977
+ implementation 'com.facebook.react:react-android'
1978
+ implementation project(':react-native-webview')
1979
+ implementation 'androidx.webkit:webkit:1.14.0'
1980
+ }
1981
+ `
1982
+ ],
1983
+ [
1984
+ join6(root, "android/src/main/AndroidManifest.xml"),
1985
+ `<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
1986
+ `
1987
+ ],
1988
+ [
1989
+ join6(root, "android/src/main/java/expo/modules/absoluteembeddedassets/AbsoluteEmbeddedAssetsModule.kt"),
1990
+ android
1991
+ ],
1992
+ [join6(project, "plugins/withAbsoluteEmbeddedAssets.js"), plugin],
1993
+ [join6(project, "src/generated/AbsoluteEmbeddedAssets.ts"), runtime2]
1994
+ ]);
1995
+ };
1996
+
1674
1997
  // src/mobile/branding.ts
1675
1998
  import { createHash as createHash2 } from "crypto";
1676
1999
  import {
1677
2000
  access,
1678
2001
  copyFile,
1679
2002
  mkdir,
1680
- readFile,
2003
+ readFile as readFile2,
1681
2004
  rename,
1682
2005
  rm,
1683
2006
  writeFile
1684
2007
  } from "fs/promises";
1685
- import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve3 } from "path";
2008
+ import { dirname as dirname3, join as join7, relative as relative2, resolve as resolve4 } from "path";
1686
2009
  var ABSOLUTE_MOBILE_BRANDING_FORMAT = 1;
1687
2010
  var ABSOLUTE_CAPACITOR_ASSETS_VERSION = "3.0.5";
1688
2011
  var ABSOLUTE_CAPACITOR_ASSETS_SPEC = `@capacitor/assets@${ABSOLUTE_CAPACITOR_ASSETS_VERSION}`;
@@ -1706,7 +2029,7 @@ var exists = async (path) => {
1706
2029
  }
1707
2030
  };
1708
2031
  var portableRelative = (root, path) => relative2(root, path).replaceAll("\\", "/");
1709
- var absoluteMobileBrandingManifestPath = (projectRoot) => join5(resolve3(projectRoot), ".absolutejs", "mobile", "branding", "manifest.json");
2032
+ var absoluteMobileBrandingManifestPath = (projectRoot) => join7(resolve4(projectRoot), ".absolutejs", "mobile", "branding", "manifest.json");
1710
2033
  var brandingEntries = (branding) => [
1711
2034
  { kind: "icon", path: branding.icon, size: ICON_SIZE },
1712
2035
  branding.android.foreground ? {
@@ -1746,7 +2069,7 @@ var brandingEntries = (branding) => [
1746
2069
  } : undefined
1747
2070
  ].filter((entry) => entry !== undefined);
1748
2071
  var inspectSource = async (projectRoot, entry) => {
1749
- const buffer = await readFile(entry.path);
2072
+ const buffer = await readFile2(entry.path);
1750
2073
  if (buffer.byteLength > MAX_SOURCE_BYTES)
1751
2074
  throw new TypeError(`mobile.branding ${entry.kind} exceeds the 32 MiB source limit.`);
1752
2075
  let metadata;
@@ -1799,7 +2122,7 @@ var createAbsoluteMobileBrandingManifest = async (config, projectRoot) => {
1799
2122
  };
1800
2123
  var writeManifest = async (projectRoot, manifest) => {
1801
2124
  const destination = absoluteMobileBrandingManifestPath(projectRoot);
1802
- await mkdir(dirname2(destination), { recursive: true });
2125
+ await mkdir(dirname3(destination), { recursive: true });
1803
2126
  const temporary = `${destination}.${crypto.randomUUID()}.tmp`;
1804
2127
  await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}
1805
2128
  `, {
@@ -1809,7 +2132,7 @@ var writeManifest = async (projectRoot, manifest) => {
1809
2132
  return destination;
1810
2133
  };
1811
2134
  var copyManaged = async (source, destination) => {
1812
- await mkdir(dirname2(destination), { recursive: true });
2135
+ await mkdir(dirname3(destination), { recursive: true });
1813
2136
  await copyFile(source, destination);
1814
2137
  };
1815
2138
  var absoluteExpoBrandingConfig = (config) => {
@@ -1853,10 +2176,10 @@ var writeAbsoluteExpoBrandingInputs = async (config, projectRoot) => {
1853
2176
  if (!branding)
1854
2177
  return;
1855
2178
  const manifest = await createAbsoluteMobileBrandingManifest(config, projectRoot);
1856
- const destination = join5(config.nativeProjectDirectory, "assets", "absolute-branding");
2179
+ const destination = join7(config.nativeProjectDirectory, "assets", "absolute-branding");
1857
2180
  await rm(destination, { force: true, recursive: true });
1858
2181
  await mkdir(destination, { recursive: true });
1859
- await copyManaged(branding.icon, join5(destination, "icon.png"));
2182
+ await copyManaged(branding.icon, join7(destination, "icon.png"));
1860
2183
  const copies = [
1861
2184
  [branding.android.backgroundImage, "android-background.png"],
1862
2185
  [branding.android.foreground, "android-foreground.png"],
@@ -1866,19 +2189,19 @@ var writeAbsoluteExpoBrandingInputs = async (config, projectRoot) => {
1866
2189
  [branding.splash.image, "splash-light.png"],
1867
2190
  [branding.splash.darkImage, "splash-dark.png"]
1868
2191
  ];
1869
- await Promise.all(copies.map(([source, name]) => source ? copyManaged(source, join5(destination, name)) : undefined));
2192
+ await Promise.all(copies.map(([source, name]) => source ? copyManaged(source, join7(destination, name)) : undefined));
1870
2193
  await writeManifest(projectRoot, manifest);
1871
2194
  return { manifest, path: destination };
1872
2195
  };
1873
2196
  var stageCapacitorInputs = async (branding, projectRoot) => {
1874
- const destination = join5(projectRoot, ".absolutejs", "mobile", "branding", "input");
2197
+ const destination = join7(projectRoot, ".absolutejs", "mobile", "branding", "input");
1875
2198
  await rm(destination, { force: true, recursive: true });
1876
2199
  await mkdir(destination, { recursive: true });
1877
- await copyManaged(branding.icon, join5(destination, "logo.png"));
2200
+ await copyManaged(branding.icon, join7(destination, "logo.png"));
1878
2201
  if (branding.splash.image)
1879
- await copyManaged(branding.splash.image, join5(destination, "splash.png"));
2202
+ await copyManaged(branding.splash.image, join7(destination, "splash.png"));
1880
2203
  if (branding.splash.darkImage)
1881
- await copyManaged(branding.splash.darkImage, join5(destination, "splash-dark.png"));
2204
+ await copyManaged(branding.splash.darkImage, join7(destination, "splash-dark.png"));
1882
2205
  return destination;
1883
2206
  };
1884
2207
  var defaultRunner = async (command, options) => {
@@ -1904,21 +2227,21 @@ ${stderr}`))
1904
2227
  throw new TypeError("Capacitor asset generation reported an incomplete projection.");
1905
2228
  };
1906
2229
  var resizePng = async (source, destination, size) => {
1907
- const output = await new Bun.Image(await readFile(source)).resize(size, size).png({ compressionLevel: 9 }).toBuffer();
1908
- await mkdir(dirname2(destination), { recursive: true });
2230
+ const output = await new Bun.Image(await readFile2(source)).resize(size, size).png({ compressionLevel: 9 }).toBuffer();
2231
+ await mkdir(dirname3(destination), { recursive: true });
1909
2232
  await writeFile(destination, output);
1910
2233
  };
1911
2234
  var applyAndroidLayers = async (branding, nativeDirectory) => {
1912
- const resources = join5(nativeDirectory, "android", "app", "src", "main", "res");
2235
+ const resources = join7(nativeDirectory, "android", "app", "src", "main", "res");
1913
2236
  await Promise.all(ANDROID_ADAPTIVE_SIZES.flatMap(([density, size]) => [
1914
2237
  ...branding.android.foreground ? [
1915
- resizePng(branding.android.foreground, join5(resources, `mipmap-${density}`, "ic_launcher_foreground.png"), size)
2238
+ resizePng(branding.android.foreground, join7(resources, `mipmap-${density}`, "ic_launcher_foreground.png"), size)
1916
2239
  ] : [],
1917
2240
  ...branding.android.backgroundImage ? [
1918
- resizePng(branding.android.backgroundImage, join5(resources, `mipmap-${density}`, "ic_launcher_background.png"), size)
2241
+ resizePng(branding.android.backgroundImage, join7(resources, `mipmap-${density}`, "ic_launcher_background.png"), size)
1919
2242
  ] : [],
1920
2243
  ...branding.android.monochrome ? [
1921
- resizePng(branding.android.monochrome, join5(resources, `mipmap-${density}`, "ic_launcher_monochrome.png"), size)
2244
+ resizePng(branding.android.monochrome, join7(resources, `mipmap-${density}`, "ic_launcher_monochrome.png"), size)
1922
2245
  ] : []
1923
2246
  ]));
1924
2247
  if (!branding.android.monochrome)
@@ -1930,14 +2253,14 @@ var applyAndroidLayers = async (branding, nativeDirectory) => {
1930
2253
  <monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
1931
2254
  </adaptive-icon>
1932
2255
  `;
1933
- await Promise.all(["ic_launcher.xml", "ic_launcher_round.xml"].map((name) => writeFile(join5(resources, "mipmap-anydpi-v26", name), iconXml)));
2256
+ await Promise.all(["ic_launcher.xml", "ic_launcher_round.xml"].map((name) => writeFile(join7(resources, "mipmap-anydpi-v26", name), iconXml)));
1934
2257
  };
1935
2258
  var applyIosAppearances = async (branding, nativeDirectory) => {
1936
2259
  if (!branding.ios.darkIcon && !branding.ios.tintedIcon)
1937
2260
  return;
1938
- const assetSet = join5(nativeDirectory, "ios", "App", "App", "Assets.xcassets", "AppIcon.appiconset");
1939
- const contentsPath = join5(assetSet, "Contents.json");
1940
- const contents = JSON.parse(await readFile(contentsPath, "utf8"));
2261
+ const assetSet = join7(nativeDirectory, "ios", "App", "App", "Assets.xcassets", "AppIcon.appiconset");
2262
+ const contentsPath = join7(assetSet, "Contents.json");
2263
+ const contents = JSON.parse(await readFile2(contentsPath, "utf8"));
1941
2264
  const images = typeof contents === "object" && contents !== null ? Reflect.get(contents, "images") : undefined;
1942
2265
  if (typeof contents !== "object" || contents === null || !Array.isArray(images))
1943
2266
  throw new TypeError("Generated iOS AppIcon Contents.json is invalid.");
@@ -1949,7 +2272,7 @@ var applyIosAppearances = async (branding, nativeDirectory) => {
1949
2272
  if (!source)
1950
2273
  return;
1951
2274
  const filename = `AppIcon-1024-${appearance}.png`;
1952
- await resizePng(source, join5(assetSet, filename), ICON_SIZE);
2275
+ await resizePng(source, join7(assetSet, filename), ICON_SIZE);
1953
2276
  return {
1954
2277
  appearances: [{ appearance: "luminosity", value: appearance }],
1955
2278
  filename,
@@ -1971,7 +2294,7 @@ var applyIosAppearances = async (branding, nativeDirectory) => {
1971
2294
  await writeFile(contentsPath, `${JSON.stringify(contents, null, 2)}
1972
2295
  `);
1973
2296
  };
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"));
2297
+ var nativeBrandingSentinels = (config, platforms) => platforms.map((platform) => platform === "android" ? join7(config.nativeProjectDirectory, "android", "app", "src", "main", "res", "mipmap-xxxhdpi", "ic_launcher.png") : join7(config.nativeProjectDirectory, "ios", "App", "App", "Assets.xcassets", "AppIcon.appiconset", "AppIcon-512@2x.png"));
1975
2298
  var requireGeneratedBranding = async (config, platforms) => {
1976
2299
  const missing = (await Promise.all(nativeBrandingSentinels(config, platforms).map(async (path) => ({
1977
2300
  path,
@@ -1991,7 +2314,7 @@ var reusableManifestPlatforms = (value, fingerprint) => {
1991
2314
  var completedManifestPlatforms = async (manifest, manifestPath, generated) => {
1992
2315
  let previousPlatforms = [];
1993
2316
  try {
1994
- const previous = JSON.parse(await readFile(manifestPath, "utf8"));
2317
+ const previous = JSON.parse(await readFile2(manifestPath, "utf8"));
1995
2318
  previousPlatforms = reusableManifestPlatforms(previous, manifest.fingerprint);
1996
2319
  } catch {}
1997
2320
  const completed = new Set([...generated, ...previousPlatforms]);
@@ -2016,9 +2339,9 @@ var generateAbsoluteCapacitorBranding = async (config, projectRoot, options = {}
2016
2339
  "--assetPath",
2017
2340
  portableRelative(projectRoot, input),
2018
2341
  "--androidProject",
2019
- portableRelative(projectRoot, join5(config.nativeProjectDirectory, "android")),
2342
+ portableRelative(projectRoot, join7(config.nativeProjectDirectory, "android")),
2020
2343
  "--iosProject",
2021
- portableRelative(projectRoot, join5(config.nativeProjectDirectory, "ios", "App")),
2344
+ portableRelative(projectRoot, join7(config.nativeProjectDirectory, "ios", "App")),
2022
2345
  "--iconBackgroundColor",
2023
2346
  branding.android.backgroundColor,
2024
2347
  "--splashBackgroundColor",
@@ -2080,7 +2403,7 @@ var inspectAbsoluteMobileBranding = async (config, projectRoot) => {
2080
2403
  });
2081
2404
  }
2082
2405
  try {
2083
- const current = parseManifestState(JSON.parse(await readFile(manifestPath, "utf8")));
2406
+ const current = parseManifestState(JSON.parse(await readFile2(manifestPath, "utf8")));
2084
2407
  if (current.fingerprint !== expected.fingerprint)
2085
2408
  return brandingInspection({
2086
2409
  configured: true,
@@ -2099,7 +2422,7 @@ var inspectAbsoluteMobileBranding = async (config, projectRoot) => {
2099
2422
  status: "stale"
2100
2423
  });
2101
2424
  const nativeSentinels = config.engine === "expo" ? [
2102
- join5(config.nativeProjectDirectory, "assets", "absolute-branding", "icon.png")
2425
+ join7(config.nativeProjectDirectory, "assets", "absolute-branding", "icon.png")
2103
2426
  ] : nativeBrandingSentinels(config, config.platforms);
2104
2427
  if ((await Promise.all(nativeSentinels.map((path) => exists(path)))).some((present) => !present))
2105
2428
  return brandingInspection({
@@ -2132,8 +2455,8 @@ var renderAbsoluteMobileBrandingPreview = async (config, projectRoot) => {
2132
2455
  const { branding } = config;
2133
2456
  if (!branding)
2134
2457
  throw new TypeError("mobile.branding is not configured.");
2135
- const destination = join5(projectRoot, ".absolutejs", "mobile", "branding", "preview.html");
2136
- const icon = `data:image/png;base64,${(await readFile(branding.icon)).toString("base64")}`;
2458
+ const destination = join7(projectRoot, ".absolutejs", "mobile", "branding", "preview.html");
2459
+ const icon = `data:image/png;base64,${(await readFile2(branding.icon)).toString("base64")}`;
2137
2460
  const appName = escapeHtml(config.appName);
2138
2461
  const html = `<!doctype html>
2139
2462
  <meta charset="utf-8">
@@ -2142,7 +2465,7 @@ var renderAbsoluteMobileBrandingPreview = async (config, projectRoot) => {
2142
2465
  <style>body{font:16px system-ui;margin:2rem;background:#eee;color:#111}main{display:flex;flex-wrap:wrap;gap:2rem}.card{background:white;border-radius:20px;padding:1.5rem;box-shadow:0 8px 30px #0002}.icon{width:192px;height:192px;object-fit:cover}.circle{border-radius:50%}.squircle{border-radius:24%}.splash{width:240px;height:480px;display:grid;place-items:center;background:${branding.splash.backgroundColor}}.splash img{width:${branding.splash.logoScale * 100}%}code{display:block;margin-top:1rem}</style>
2143
2466
  <h1>${appName}</h1><main><section class="card"><img class="icon squircle" src="${icon}" alt="Square app icon preview"><code>Apple / legacy Android</code></section><section class="card"><img class="icon circle" src="${icon}" alt="Circular Android icon preview"><code>Android circular mask</code></section><section class="card splash"><img src="${icon}" alt="Launch screen preview"></section></main><p>Fingerprint: <code>${manifest.fingerprint}</code></p>
2144
2467
  `;
2145
- await mkdir(dirname2(destination), { recursive: true });
2468
+ await mkdir(dirname3(destination), { recursive: true });
2146
2469
  await writeFile(destination, html);
2147
2470
  return destination;
2148
2471
  };
@@ -2227,15 +2550,15 @@ var routeSegments = (route) => {
2227
2550
  return segment;
2228
2551
  });
2229
2552
  };
2230
- var routeFile = (project, route) => join6(project, "app", ...routeSegments(route), "index.tsx");
2553
+ var routeFile = (project, route) => join8(project, "app", ...routeSegments(route), "index.tsx");
2231
2554
  var packageDependencies = (plan) => Object.fromEntries(plan.requiredPackages.map((spec) => {
2232
2555
  const separator = spec.lastIndexOf("@");
2233
2556
  return [spec.slice(0, separator), spec.slice(separator + 1)];
2234
2557
  }));
2235
2558
  var expoPackage = (auth, branding, sync, devices, updates) => ({
2236
2559
  dependencies: {
2237
- "@absolutejs/devices": "0.7.0",
2238
- "@absolutejs/devices-expo": "0.0.11",
2560
+ "@absolutejs/devices": ABSOLUTE_DEVICES_VERSION,
2561
+ "@absolutejs/devices-expo": ABSOLUTE_EXPO_DEVICES_VERSION,
2239
2562
  ...auth ? {
2240
2563
  "@absolutejs/auth": ABSOLUTE_EXPO_AUTH_CORE_VERSION,
2241
2564
  [ABSOLUTE_EXPO_AUTH_PACKAGE]: ABSOLUTE_EXPO_AUTH_VERSION
@@ -2316,24 +2639,24 @@ var expoAppConfig = (config, auth, sync, devices, runtimeFingerprint) => {
2316
2639
  const devicePlugins = [
2317
2640
  ...new Set(devices.capabilities.flatMap((name) => devices.providers[name]?.plugins ?? []))
2318
2641
  ];
2319
- const configuredPlugins = devicePlugins.map((plugin) => {
2320
- if (plugin === "expo-image-picker")
2642
+ const configuredPlugins = devicePlugins.map((plugin2) => {
2643
+ if (plugin2 === "expo-image-picker")
2321
2644
  return [
2322
- plugin,
2645
+ plugin2,
2323
2646
  {
2324
2647
  cameraPermission: descriptions.NSCameraUsageDescription,
2325
2648
  microphonePermission: false,
2326
2649
  photosPermission: descriptions.NSPhotoLibraryUsageDescription
2327
2650
  }
2328
2651
  ];
2329
- if (plugin === "expo-location")
2652
+ if (plugin2 === "expo-location")
2330
2653
  return [
2331
- plugin,
2654
+ plugin2,
2332
2655
  {
2333
2656
  locationWhenInUsePermission: descriptions.NSLocationWhenInUseUsageDescription
2334
2657
  }
2335
2658
  ];
2336
- return plugin;
2659
+ return plugin2;
2337
2660
  });
2338
2661
  return {
2339
2662
  expo: {
@@ -2355,7 +2678,7 @@ var expoAppConfig = (config, auth, sync, devices, runtimeFingerprint) => {
2355
2678
  package: config.appId,
2356
2679
  permissions: requirements.androidPermissions
2357
2680
  },
2358
- experiments: { typedRoutes: true },
2681
+ experiments: { tsconfigPaths: false, typedRoutes: true },
2359
2682
  ios: {
2360
2683
  ...branding?.ios ?? {},
2361
2684
  associatedDomains: config.deepLinkHosts.map((host2) => `applinks:${host2}`),
@@ -2378,6 +2701,7 @@ var expoAppConfig = (config, auth, sync, devices, runtimeFingerprint) => {
2378
2701
  ["expo-dev-client", { launchMode: "most-recent" }],
2379
2702
  ...branding ? [branding.splashPlugin] : [],
2380
2703
  "./plugins/withAbsoluteActivityResultRecovery",
2704
+ "./plugins/withAbsoluteEmbeddedAssets",
2381
2705
  ...auth || config.updates ? ["expo-secure-store"] : [],
2382
2706
  ...sync ? [
2383
2707
  "expo-sqlite",
@@ -2835,7 +3159,7 @@ import {
2835
3159
  installExpoSyncLifecycle,
2836
3160
  registerExpoSyncBackgroundTask
2837
3161
  } from '@absolutejs/sync-expo';
2838
- import * as Updates from 'expo-updates';
3162
+ import { reloadAppAsync } from 'expo';
2839
3163
  import { absoluteExpoAuth, startAbsoluteExpoAuth } from './AbsoluteAuth';
2840
3164
 
2841
3165
  const BACKGROUND_TASK = ${JSON.stringify(backgroundTask)};
@@ -2894,37 +3218,98 @@ absoluteExpoAuth.onPrincipalChange(principal => {
2894
3218
  if (!started || principal?.namespace === activeNamespace) return;
2895
3219
  const previousNamespace = activeNamespace;
2896
3220
  installRuntimeTransport(principal);
2897
- if (previousNamespace !== undefined) void Updates.reloadAsync();
3221
+ if (previousNamespace !== undefined) void reloadAppAsync('absolutejs-auth-principal-changed').catch(() => {
3222
+ console.warn('AbsoluteJS could not reset the account runtime. Restart the app before continuing.');
3223
+ });
2898
3224
  });
2899
3225
 
2900
3226
  export const createAbsoluteExpoSyncBridge = async (
2901
3227
  emit: (event: 'sync.socket' | 'sync.wake', payload: Record<string, unknown>) => void
2902
3228
  ) => {
2903
3229
  await startAbsoluteExpoSync();
2904
- const principal = await absoluteExpoAuth.principal();
2905
- if (!principal) return {
2906
- close: () => undefined,
2907
- request: async () => { throw new Error('Expo Sync requires an authenticated principal.'); }
3230
+ const bind = (namespace: string, generation: number) => {
3231
+ const emitCurrent = (event: 'sync.socket' | 'sync.wake', payload: Record<string, unknown>) => {
3232
+ if (!closed && generation === revision) emit(event, payload);
3233
+ };
3234
+ const transactions = createExpoSyncBridgeHost({ createId: expoSyncRandomId, namespace, store });
3235
+ const sockets = createExpoSyncSocketBridgeHost({
3236
+ allowedOrigin: PRODUCTION_ORIGIN,
3237
+ emit: payload => emitCurrent('sync.socket', payload),
3238
+ socketTicket: async audience => {
3239
+ if (closed || generation !== revision) throw new Error('Expo Sync principal changed.');
3240
+ const ticket = await absoluteExpoAuth.socketTicket(audience);
3241
+ if (closed || generation !== revision) throw new Error('Expo Sync principal changed.');
3242
+ return ticket;
3243
+ }
3244
+ });
3245
+ const removeLifecycle = installExpoSyncLifecycle({
3246
+ client: { reconnect: () => emitCurrent('sync.wake', {}) }
3247
+ });
3248
+ return {
3249
+ close: async () => {
3250
+ removeLifecycle();
3251
+ await Promise.all([transactions.close(), sockets.close()]);
3252
+ },
3253
+ request: (method: string, params: Record<string, unknown>) =>
3254
+ method.startsWith('sync.socket.') ? sockets.request(method, params) : transactions.request(method, params)
3255
+ };
2908
3256
  };
2909
- const transactions = createExpoSyncBridgeHost({ createId: expoSyncRandomId, namespace: principal.namespace, store });
2910
- const sockets = createExpoSyncSocketBridgeHost({
2911
- allowedOrigin: PRODUCTION_ORIGIN,
2912
- emit: payload => emit('sync.socket', payload),
2913
- socketTicket: audience => absoluteExpoAuth.socketTicket(audience)
2914
- });
2915
- const removeLifecycle = installExpoSyncLifecycle({
2916
- client: { reconnect: () => emit('sync.wake', {}) }
2917
- });
3257
+ let closed = false;
3258
+ let revoked = false;
3259
+ let boundNamespace: string | undefined;
3260
+ let revision = 0;
3261
+ let bridge: ReturnType<typeof bind> | undefined;
3262
+ let ready = Promise.resolve();
3263
+ const replace = (principal: Awaited<ReturnType<typeof absoluteExpoAuth.principal>>) => {
3264
+ if (closed) return;
3265
+ // A mounted page may bind its first login, but must never inherit another
3266
+ // account's authority. Account changes require the existing runtime reload.
3267
+ if (boundNamespace !== undefined && principal?.namespace !== boundNamespace) revoked = true;
3268
+ const generation = ++revision;
3269
+ const previous = bridge;
3270
+ bridge = undefined;
3271
+ ready = ready.then(async () => {
3272
+ await previous?.close();
3273
+ if (!closed && !revoked && generation === revision && principal) {
3274
+ boundNamespace = principal.namespace;
3275
+ bridge = bind(principal.namespace, generation);
3276
+ }
3277
+ });
3278
+ // Keep background transitions handled; requests still observe setup failures.
3279
+ void ready.catch(() => undefined);
3280
+ };
3281
+ const stop = absoluteExpoAuth.onPrincipalChange(replace);
3282
+ try {
3283
+ const initialRevision = revision;
3284
+ const principal = await absoluteExpoAuth.principal();
3285
+ if (revision === initialRevision) replace(principal);
3286
+ await ready;
3287
+ } catch (error) {
3288
+ stop();
3289
+ closed = true;
3290
+ await bridge?.close();
3291
+ throw error;
3292
+ }
2918
3293
 
2919
3294
  return {
2920
3295
  close: async () => {
2921
- removeLifecycle();
2922
- await Promise.all([transactions.close(), sockets.close()]);
3296
+ if (closed) return;
3297
+ closed = true;
3298
+ ++revision;
3299
+ stop();
3300
+ const previous = bridge;
3301
+ bridge = undefined;
3302
+ await Promise.all([ready, previous?.close()]);
2923
3303
  },
2924
- request: (method: string, params: Record<string, unknown>) =>
2925
- method.startsWith('sync.socket.')
2926
- ? sockets.request(method, params)
2927
- : transactions.request(method, params)
3304
+ request: async (method: string, params: Record<string, unknown>) => {
3305
+ const generation = revision;
3306
+ await ready;
3307
+ if (closed || revoked || generation !== revision) throw new Error('Expo Sync principal changed.');
3308
+ if (!bridge) throw new Error('Expo Sync requires an authenticated principal.');
3309
+ const result = await bridge.request(method, params);
3310
+ if (closed || revoked || generation !== revision) throw new Error('Expo Sync principal changed.');
3311
+ return result;
3312
+ }
2928
3313
  };
2929
3314
  };
2930
3315
  `;
@@ -2942,6 +3327,7 @@ import { ActivityIndicator, BackHandler, Platform, Pressable, StyleSheet, Text,
2942
3327
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
2943
3328
  import { WebView, type WebViewMessageEvent } from 'react-native-webview';
2944
3329
  import { materializeAbsoluteWebBundle } from './webAssets';
3330
+ import { absoluteEmbeddedNativeConfig } from './AbsoluteEmbeddedAssets';
2945
3331
  import { markAbsoluteEmbeddedWebPhase, markAbsoluteEmbeddedWebReady } from './AbsoluteActivityResultRecovery';
2946
3332
  import { createExpoDevicesBridgeHost } from '@absolutejs/devices-expo/bridge';
2947
3333
  import { absoluteExpoDevices, beforeAbsoluteExpoDeviceSignOut } from './AbsoluteDevices';
@@ -3061,7 +3447,16 @@ const bridgeBootstrap = (path: string, safeAreaInsets: { bottom: number; left: n
3061
3447
  event.preventDefault();
3062
3448
  send({ format: 3, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
3063
3449
  }, true);
3064
- send({ format: 3, kind: 'event', event: 'ready', path: currentPath });
3450
+ let shellRendered = false;
3451
+ addEventListener('absolute:shell-rendered', () => {
3452
+ shellRendered = true;
3453
+ send({ format: 3, kind: 'event', event: 'shell-rendered', path: currentPath });
3454
+ });
3455
+ const startupFailed = () => {
3456
+ if (!shellRendered) send({ format: 3, kind: 'event', event: 'startup-failed', path: currentPath });
3457
+ };
3458
+ addEventListener('error', startupFailed);
3459
+ addEventListener('unhandledrejection', startupFailed);
3065
3460
  })(); true;\`;
3066
3461
  };
3067
3462
 
@@ -3106,11 +3501,18 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3106
3501
  const canGoBack = useRef(false);
3107
3502
  const [runtimeReady, setRuntimeReady] = useState(!AUTH_ENABLED && !SYNC_ENABLED);
3108
3503
  const [devicesReady, setDevicesReady] = useState(false);
3109
- const [startupError, setStartupError] = useState<'assets' | 'devices'>();
3504
+ const [startupError, setStartupError] = useState<'assets' | 'devices' | 'content'>();
3110
3505
  const [startupRevision, setStartupRevision] = useState(0);
3111
3506
  const activeWebPath = useRef(pathname);
3112
3507
  const embeddedReadyReported = useRef(false);
3113
3508
  const webSource = useMemo(() => indexUri ? { uri: indexUri } : undefined, [indexUri]);
3509
+ useEffect(() => {
3510
+ if (!indexUri || DEV_ORIGIN) return;
3511
+ const timeout = setTimeout(() => {
3512
+ if (!embeddedReadyReported.current) setStartupError('content');
3513
+ }, 60000);
3514
+ return () => clearTimeout(timeout);
3515
+ }, [indexUri]);
3114
3516
 
3115
3517
  useEffect(() => {
3116
3518
  if (!AUTH_ENABLED && !SYNC_ENABLED) return;
@@ -3121,10 +3523,13 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3121
3523
  webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
3122
3524
  });
3123
3525
  void ${sync ? "startAbsoluteExpoSync" : "startAbsoluteExpoAuth"}().then(async () => {
3124
- ${sync ? `syncBridge.current = await createAbsoluteExpoSyncBridge((event, payload) => {
3526
+ ${sync ? `const bridge = await createAbsoluteExpoSyncBridge((event, payload) => {
3527
+ if (!active) return;
3125
3528
  const source = JSON.stringify({ event, format: BRIDGE_FORMAT, kind: 'event', path: activeWebPath.current, payload });
3126
3529
  webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
3127
- });` : ""}
3530
+ });
3531
+ if (!active) { await bridge.close(); return; }
3532
+ syncBridge.current = bridge;` : ""}
3128
3533
  if (active) setRuntimeReady(true);
3129
3534
  });
3130
3535
  return () => {
@@ -3146,6 +3551,7 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3146
3551
  markAbsoluteEmbeddedWebPhase('assets-start');
3147
3552
  void materializeAbsoluteWebBundle().then(uri => {
3148
3553
  markAbsoluteEmbeddedWebPhase('assets-ready');
3554
+ embeddedReadyReported.current = false;
3149
3555
  setIndexUri(uri + '?absolutePath=' + encodeURIComponent(pathname));
3150
3556
  }).catch(error => {
3151
3557
  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 +3605,23 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3199
3605
  try { return new URL(source).origin === origin; } catch { return false; }
3200
3606
  };
3201
3607
  const onMessage = async (event: WebViewMessageEvent) => {
3608
+ // Bridge authority belongs only to the currently loaded embedded bundle
3609
+ // (or the explicitly configured development origin), never remote pages.
3610
+ const sourceUrl = event.nativeEvent.url;
3611
+ if (!indexUri || typeof sourceUrl !== 'string' || (DEV_ORIGIN ? !hasOrigin(sourceUrl, DEV_ORIGIN) : !sourceUrl.startsWith(indexUri.slice(0, indexUri.lastIndexOf('/') + 1)))) return;
3202
3612
  const source = event.nativeEvent.data;
3203
3613
  if (new TextEncoder().encode(source).byteLength > MAX_MESSAGE_BYTES) return;
3204
3614
  let message: Record<string, unknown>;
3205
3615
  try { message = JSON.parse(source); } catch { return; }
3206
3616
  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')) {
3617
+ if (message.kind === 'event' && message.event === 'startup-failed') {
3618
+ if (!embeddedReadyReported.current) setStartupError('content');
3619
+ return;
3620
+ }
3621
+ if (message.kind === 'event' && (message.event === 'navigation' || message.event === 'shell-rendered')) {
3208
3622
  const target = new URL(message.path, PRODUCTION_ORIGIN);
3209
3623
  if (target.origin !== PRODUCTION_ORIGIN) return;
3210
- if (message.event === 'ready' && !DEV_ORIGIN && !embeddedReadyReported.current) {
3624
+ if (message.event === 'shell-rendered' && !DEV_ORIGIN && !embeddedReadyReported.current) {
3211
3625
  embeddedReadyReported.current = true;
3212
3626
  markAbsoluteEmbeddedWebReady();
3213
3627
  }
@@ -3261,12 +3675,17 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3261
3675
  </View>;
3262
3676
  if (!webSource || !runtimeReady || !devicesReady) return <View style={styles.loading}><ActivityIndicator /></View>;
3263
3677
  return <WebView
3264
- allowFileAccess
3265
- allowFileAccessFromFileURLs
3678
+ nativeConfig={!DEV_ORIGIN ? absoluteEmbeddedNativeConfig : undefined}
3679
+ allowFileAccess={Platform.OS !== 'android'}
3680
+ allowFileAccessFromFileURLs={Platform.OS !== 'android'}
3266
3681
  allowUniversalAccessFromFileURLs={false}
3682
+ originWhitelist={DEV_ORIGIN ? [DEV_ORIGIN] : [Platform.OS === 'android' ? 'https://appassets.androidplatform.net' : 'file://*']}
3267
3683
  allowingReadAccessToURL={webSource.uri.slice(0, webSource.uri.lastIndexOf('/') + 1)}
3268
3684
  injectedJavaScriptBeforeContentLoaded={bridgeBootstrap(pathname, safeAreaInsets)}
3269
3685
  onMessage={onMessage}
3686
+ onError={() => setStartupError('content')}
3687
+ onRenderProcessGone={() => setStartupError('content')}
3688
+ onContentProcessDidTerminate={() => setStartupError('content')}
3270
3689
  onNavigationStateChange={state => {
3271
3690
  canGoBack.current = state.canGoBack;
3272
3691
  if (!DEV_ORIGIN || !hasOrigin(state.url, DEV_ORIGIN)) return;
@@ -3274,8 +3693,8 @@ export function AbsoluteWebHost({ path }: AbsoluteWebHostProps = {}) {
3274
3693
  activeWebPath.current = route.pathname + route.search + route.hash;
3275
3694
  }}
3276
3695
  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);
3696
+ if (DEV_ORIGIN ? hasOrigin(request.url, DEV_ORIGIN) : request.url.startsWith(webSource.uri.slice(0, webSource.uri.lastIndexOf('/') + 1))) return true;
3697
+ if (/^https?:/.test(request.url)) void Linking.openURL(request.url);
3279
3698
  return false;
3280
3699
  }}
3281
3700
  ref={webView}
@@ -3478,8 +3897,8 @@ const styles = StyleSheet.create({
3478
3897
  error: { color: '#b91c1c', fontSize: 16, textAlign: 'center' }
3479
3898
  });
3480
3899
  `;
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, ""))};
3900
+ var nativeWrapperSource = (wrapper, module, runtime3, route) => `${EXPO_GENERATED_HEADER}import ApplicationNativeRoute from ${JSON.stringify(portableRelative2(dirname4(wrapper), module).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
3901
+ import { createAbsoluteNativeRoute, createAbsoluteNativeRouteErrorBoundary } from ${JSON.stringify(portableRelative2(dirname4(wrapper), runtime3).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
3483
3902
 
3484
3903
  export const ErrorBoundary = createAbsoluteNativeRouteErrorBoundary(${JSON.stringify(route)});
3485
3904
  export default createAbsoluteNativeRoute(ApplicationNativeRoute, ${JSON.stringify(route)});
@@ -3521,9 +3940,9 @@ var expoTsConfig = (projectRoot, project, auth, sync) => ({
3521
3940
  extends: "expo/tsconfig.base"
3522
3941
  });
3523
3942
  var writeManagedFile = async (path, source, force) => {
3524
- await mkdir2(dirname3(path), { recursive: true });
3943
+ await mkdir2(dirname4(path), { recursive: true });
3525
3944
  if (await exists2(path)) {
3526
- const current = await readFile2(path, "utf8");
3945
+ const current = await readFile3(path, "utf8");
3527
3946
  if (current === source)
3528
3947
  return false;
3529
3948
  if (!force && !current.startsWith(EXPO_GENERATED_HEADER)) {
@@ -3539,7 +3958,7 @@ var pruneManagedFiles = async (paths) => {
3539
3958
  const removed = await Promise.all(paths.map(async (path) => {
3540
3959
  if (!await exists2(path))
3541
3960
  return false;
3542
- const source = await readFile2(path, "utf8");
3961
+ const source = await readFile3(path, "utf8");
3543
3962
  if (!source.startsWith(EXPO_GENERATED_HEADER) && !path.endsWith("expo-module.config.json"))
3544
3963
  return false;
3545
3964
  await rm2(path, { force: true });
@@ -3548,12 +3967,12 @@ var pruneManagedFiles = async (paths) => {
3548
3967
  return removed.filter(Boolean).length;
3549
3968
  };
3550
3969
  var pruneStaleManagedExpoRoutes = async (project, expected) => {
3551
- const appDirectory = join6(project, "app");
3970
+ const appDirectory = join8(project, "app");
3552
3971
  if (!await exists2(appDirectory))
3553
3972
  return 0;
3554
3973
  const files = await walkFiles(appDirectory);
3555
3974
  const stale = (await Promise.all(files.map(async (path) => ({
3556
- managed: path.endsWith(".tsx") && (await readFile2(path, "utf8")).startsWith(EXPO_GENERATED_HEADER),
3975
+ managed: path.endsWith(".tsx") && (await readFile3(path, "utf8")).startsWith(EXPO_GENERATED_HEADER),
3557
3976
  path
3558
3977
  })))).filter(({ managed, path }) => managed && !expected.has(path));
3559
3978
  await Promise.all(stale.map(({ path }) => rm2(path, { force: true })));
@@ -3579,7 +3998,7 @@ export const ABSOLUTE_MOBILE_MANIFEST: AbsoluteMobileManifest | undefined = unde
3579
3998
  var writeAbsoluteExpoProject = async (config, options) => {
3580
3999
  if (config.engine !== "expo")
3581
4000
  throw new TypeError("Expo project generation requires mobile.engine: expo.");
3582
- const projectRoot = resolve4(options.projectRoot);
4001
+ const projectRoot = resolve5(options.projectRoot);
3583
4002
  const project = config.nativeProjectDirectory;
3584
4003
  const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
3585
4004
  const devices = resolveAbsoluteDeviceCapabilityPlan(projectRoot, "expo");
@@ -3596,7 +4015,7 @@ var writeAbsoluteExpoProject = async (config, options) => {
3596
4015
  if (missing) {
3597
4016
  throw new TypeError(`Expo native route ${missing.route} references missing module ${missing.module}.`);
3598
4017
  }
3599
- const marker = join6(project, EXPO_PROJECT_MARKER);
4018
+ const marker = join8(project, EXPO_PROJECT_MARKER);
3600
4019
  if (await exists2(project) && !await exists2(marker)) {
3601
4020
  const entries = await readdir(project);
3602
4021
  if (entries.length > 0 && !options.force) {
@@ -3609,7 +4028,7 @@ var writeAbsoluteExpoProject = async (config, options) => {
3609
4028
  const authEnabled = Boolean(auth);
3610
4029
  const files = new Map([
3611
4030
  [
3612
- join6(project, ".gitignore"),
4031
+ join8(project, ".gitignore"),
3613
4032
  `.expo/
3614
4033
  android/
3615
4034
  ios/
@@ -3617,80 +4036,82 @@ node_modules/
3617
4036
  `
3618
4037
  ],
3619
4038
  [
3620
- join6(project, "app.json"),
4039
+ join8(project, "app.json"),
3621
4040
  jsonSource(expoAppConfig(config, authEnabled, syncEnabled, devices, runtimeFingerprint))
3622
4041
  ],
3623
- [join6(project, "app.config.js"), expoDynamicAppConfig],
4042
+ [join8(project, "app.config.js"), expoDynamicAppConfig],
3624
4043
  [
3625
- join6(project, "package.json"),
4044
+ join8(project, "package.json"),
3626
4045
  jsonSource(expoPackage(authEnabled, config.branding !== undefined, syncEnabled, devices, config.updates !== undefined))
3627
4046
  ],
3628
- [join6(project, "metro.config.js"), metroConfig(projectRoot)],
4047
+ [join8(project, "metro.config.js"), metroConfig(projectRoot)],
3629
4048
  [
3630
- join6(project, "plugins", "withAbsoluteDevelopmentCa.js"),
4049
+ join8(project, "plugins", "withAbsoluteDevelopmentCa.js"),
3631
4050
  expoDevelopmentCaPlugin
3632
4051
  ],
3633
4052
  [
3634
- join6(project, "plugins", "withAbsoluteActivityResultRecovery.js"),
4053
+ join8(project, "plugins", "withAbsoluteActivityResultRecovery.js"),
3635
4054
  expoActivityResultRecoveryPlugin
3636
4055
  ],
3637
4056
  [
3638
- join6(project, "tsconfig.json"),
4057
+ join8(project, "tsconfig.json"),
3639
4058
  jsonSource(expoTsConfig(projectRoot, project, authEnabled, syncEnabled))
3640
4059
  ],
3641
4060
  [
3642
- join6(project, "app", "_layout.tsx"),
4061
+ join8(project, "app", "_layout.tsx"),
3643
4062
  layoutSource(authEnabled, syncEnabled, config.updates !== undefined, config.observability !== undefined)
3644
4063
  ],
3645
4064
  [
3646
- join6(project, "app", "__absolute", "native", "index.tsx"),
4065
+ join8(project, "app", "__absolute", "native", "index.tsx"),
3647
4066
  nativeDiagnosticSource
3648
4067
  ],
3649
4068
  [
3650
- join6(project, "src", "generated", "AbsoluteDevices.ts"),
4069
+ join8(project, "src", "generated", "AbsoluteDevices.ts"),
3651
4070
  devicesRuntimeSource(config, devices, authEnabled)
3652
4071
  ],
3653
4072
  [
3654
- join6(project, "src", "generated", "AbsoluteNativeRoute.tsx"),
4073
+ join8(project, "src", "generated", "AbsoluteNativeRoute.tsx"),
3655
4074
  nativeRouteRuntimeSource(authEnabled)
3656
4075
  ],
3657
4076
  [
3658
- join6(project, "src", "generated", "AbsoluteWebHost.tsx"),
4077
+ join8(project, "src", "generated", "AbsoluteWebHost.tsx"),
3659
4078
  webHostSource(config, auth, syncEnabled)
3660
4079
  ]
3661
4080
  ]);
3662
4081
  for (const [path, source] of absoluteExpoActivityResultRecoveryFiles(project))
3663
4082
  files.set(path, source);
4083
+ for (const [path, source] of absoluteExpoEmbeddedAssetsFiles(project))
4084
+ files.set(path, source);
3664
4085
  if (config.observability) {
3665
4086
  for (const [path, source] of absoluteExpoNativeObservabilityFiles(project))
3666
4087
  files.set(path, source);
3667
4088
  }
3668
4089
  if (config.updates) {
3669
- files.set(join6(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource(config));
4090
+ files.set(join8(project, "src", "generated", "AbsoluteUpdates.ts"), updatesRuntimeSource(config));
3670
4091
  }
3671
4092
  const expoCodeSigning = config.updates?.expoCodeSigning;
3672
4093
  if (expoCodeSigning)
3673
- files.set(join6(project, "certs", "absolute-mobile-update.pem"), expoCodeSigning.certificatePem);
4094
+ files.set(join8(project, "certs", "absolute-mobile-update.pem"), expoCodeSigning.certificatePem);
3674
4095
  if (auth) {
3675
- files.set(join6(project, "src", "generated", "AbsoluteAuth.ts"), authRuntimeSource(auth, config.appId));
4096
+ files.set(join8(project, "src", "generated", "AbsoluteAuth.ts"), authRuntimeSource(auth, config.appId));
3676
4097
  }
3677
4098
  if (syncSchema) {
3678
- files.set(join6(project, "src", "generated", "AbsoluteSync.ts"), syncRuntimeSource(config, syncSchema));
4099
+ files.set(join8(project, "src", "generated", "AbsoluteSync.ts"), syncRuntimeSource(config, syncSchema));
3679
4100
  }
3680
- const webAssetsPath = join6(project, "src", "generated", "webAssets.ts");
4101
+ const webAssetsPath = join8(project, "src", "generated", "webAssets.ts");
3681
4102
  if (!await exists2(webAssetsPath)) {
3682
4103
  files.set(webAssetsPath, emptyWebAssetsSource);
3683
4104
  }
3684
4105
  if (!config.expoNativeRoutes["/"]) {
3685
- files.set(join6(project, "app", "index.tsx"), webRouteSource);
4106
+ files.set(join8(project, "app", "index.tsx"), webRouteSource);
3686
4107
  }
3687
- files.set(join6(project, "app", "[...absolute].tsx"), catchAllRouteSource);
3688
- const nativeRouteRuntime = join6(project, "src", "generated", "AbsoluteNativeRoute.tsx");
4108
+ files.set(join8(project, "app", "[...absolute].tsx"), catchAllRouteSource);
4109
+ const nativeRouteRuntime = join8(project, "src", "generated", "AbsoluteNativeRoute.tsx");
3689
4110
  for (const [route, module] of routeModules) {
3690
- const wrapper = route === "/" ? join6(project, "app", "index.tsx") : routeFile(project, route);
4111
+ const wrapper = route === "/" ? join8(project, "app", "index.tsx") : routeFile(project, route);
3691
4112
  files.set(wrapper, nativeWrapperSource(wrapper, module, nativeRouteRuntime, route));
3692
4113
  }
3693
- const removedRoutes = await pruneStaleManagedExpoRoutes(project, new Set([...files.keys()].filter((path) => path.startsWith(`${join6(project, "app")}${sep}`))));
4114
+ const removedRoutes = await pruneStaleManagedExpoRoutes(project, new Set([...files.keys()].filter((path) => path.startsWith(`${join8(project, "app")}${sep}`))));
3694
4115
  const removedObservability = config.observability ? 0 : await pruneManagedFiles(absoluteExpoNativeObservabilityPaths(project));
3695
4116
  const changes = await Promise.all([...files].map(([path, source]) => writeManagedFile(path, source, true)));
3696
4117
  await writeAbsoluteExpoBrandingInputs(config, projectRoot);
@@ -3700,7 +4121,7 @@ node_modules/
3700
4121
  var walkFiles = async (root, directory = root) => {
3701
4122
  const entries = await readdir(directory, { withFileTypes: true });
3702
4123
  const nested = await Promise.all(entries.map((entry) => {
3703
- const path = join6(directory, entry.name);
4124
+ const path = join8(directory, entry.name);
3704
4125
  if (entry.isDirectory())
3705
4126
  return walkFiles(root, path);
3706
4127
  if (entry.isFile())
@@ -3733,13 +4154,16 @@ var installStagedDirectory = async (staging, destination) => {
3733
4154
  throw error;
3734
4155
  }
3735
4156
  };
3736
- var assetModuleSource = (assets, archiveAsset, bundleId, manifest) => `${EXPO_GENERATED_HEADER}import { Asset } from 'expo-asset';
4157
+ var assetModuleSource = (assets, archiveAsset, bundleId, archiveMd5, manifest) => `${EXPO_GENERATED_HEADER}import { Asset } from 'expo-asset';
3737
4158
  import { Directory, File, Paths } from 'expo-file-system';
3738
4159
  import { copyAsync as copyLegacyAsync } from 'expo-file-system/legacy';
4160
+ import { registerAbsoluteEmbeddedBundle } from './AbsoluteEmbeddedAssets';
3739
4161
 
3740
4162
  ${nativeDataManifestTypeSource}
3741
4163
  declare const require: (path: string) => number;
3742
4164
  const BUNDLE_ID = ${JSON.stringify(bundleId)};
4165
+ const ARCHIVE_MD5 = ${JSON.stringify(archiveMd5)};
4166
+ const ARCHIVE_LENGTH = ${assets.reduce((sum, entry) => sum + entry.length, 0)};
3743
4167
  export const ABSOLUTE_MOBILE_MANIFEST: AbsoluteMobileManifest = ${JSON.stringify(manifest)};
3744
4168
  const ARCHIVE_MODULE = require(${JSON.stringify(archiveAsset)});
3745
4169
  const ASSETS = [
@@ -3756,10 +4180,12 @@ export const materializeAbsoluteWebBundle = async () => {
3756
4180
  let archive: File;
3757
4181
  try { archive = new File(root, '.absolutejs-bundle.absasset'); }
3758
4182
  catch { throw new Error('assets-destination-failed'); }
4183
+ if (archive.exists && (archive.size !== ARCHIVE_LENGTH || archive.md5 !== ARCHIVE_MD5)) archive.delete();
3759
4184
  if (!archive.exists) {
3760
4185
  let asset: Asset;
3761
4186
  try { asset = Asset.fromModule(ARCHIVE_MODULE); }
3762
4187
  catch { throw new Error('assets-module-failed'); }
4188
+ if (asset.type !== 'absasset' || asset.hash !== ARCHIVE_MD5) throw new Error('assets-identity-failed');
3763
4189
  try { if (!asset.localUri) asset = await asset.downloadAsync(); }
3764
4190
  catch { throw new Error('assets-download-failed'); }
3765
4191
  if (!asset.localUri) throw new Error('assets-download-failed');
@@ -3772,9 +4198,16 @@ export const materializeAbsoluteWebBundle = async () => {
3772
4198
  catch { throw new Error('assets-copy-failed'); }
3773
4199
  }
3774
4200
  }
4201
+ // Checksums detect accidental corruption/registry mismatches, not authenticity;
4202
+ // authenticity comes from the signed application/update containing this table.
4203
+ if (archive.size !== ARCHIVE_LENGTH || archive.md5 !== ARCHIVE_MD5) {
4204
+ archive.delete();
4205
+ throw new Error('assets-integrity-failed');
4206
+ }
3775
4207
  let contents: Uint8Array;
3776
4208
  try { contents = await archive.bytes(); }
3777
4209
  catch { throw new Error('assets-read-failed'); }
4210
+ if (contents.byteLength !== ARCHIVE_LENGTH) throw new Error('assets-integrity-failed');
3778
4211
  for (const entry of ASSETS) {
3779
4212
  const parts = entry.path.split('/');
3780
4213
  const name = parts.pop();
@@ -3788,33 +4221,34 @@ export const materializeAbsoluteWebBundle = async () => {
3788
4221
  let destination: File;
3789
4222
  try {
3790
4223
  destination = new File(directory, name);
3791
- if (destination.exists && destination.size === entry.length) continue;
4224
+ if (destination.exists && destination.size === entry.length && destination.md5 === entry.md5) continue;
3792
4225
  if (destination.exists) destination.delete();
3793
4226
  } catch { throw new Error('assets-destination-failed'); }
3794
4227
  try { destination.write(contents.slice(entry.offset, entry.offset + entry.length)); }
3795
4228
  catch { throw new Error('assets-write-failed'); }
4229
+ if (destination.size !== entry.length || destination.md5 !== entry.md5) throw new Error('assets-integrity-failed');
3796
4230
  }
3797
4231
  try { archive.delete(); }
3798
4232
  catch { throw new Error('assets-finalize-failed'); }
3799
4233
 
3800
- try { return new File(root, 'index.html').uri; }
4234
+ try { return await registerAbsoluteEmbeddedBundle(root.uri, ASSETS.map(entry => entry.path), new File(root, 'index.html').uri); }
3801
4235
  catch { throw new Error('assets-finalize-failed'); }
3802
4236
  };
3803
4237
  `;
3804
4238
  var syncAbsoluteExpoWebAssets = async (config) => {
3805
4239
  if (config.engine !== "expo")
3806
4240
  throw new TypeError("Expo asset sync requires mobile.engine: expo.");
3807
- const marker = join6(config.nativeProjectDirectory, EXPO_PROJECT_MARKER);
4241
+ const marker = join8(config.nativeProjectDirectory, EXPO_PROJECT_MARKER);
3808
4242
  if (!await exists2(marker)) {
3809
4243
  throw new TypeError("Expo asset sync requires an AbsoluteJS-managed Expo project. Run mobile init first.");
3810
4244
  }
3811
- const manifestPath = join6(config.bundleDirectory, "absolute-mobile-manifest.json");
3812
- const manifest = JSON.parse(await readFile2(manifestPath, "utf8"));
4245
+ const manifestPath = join8(config.bundleDirectory, "absolute-mobile-manifest.json");
4246
+ const manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
3813
4247
  const nativeDataManifest = expoNativeDataManifest(manifest);
3814
4248
  const { appBuild } = nativeDataManifest;
3815
4249
  const files = await walkFiles(config.bundleDirectory);
3816
4250
  const bundleHash = createHash3("sha256");
3817
- const filesWithContents = await Promise.all(files.map(async (file) => ({ contents: await readFile2(file), file })));
4251
+ const filesWithContents = await Promise.all(files.map(async (file) => ({ contents: await readFile3(file), file })));
3818
4252
  filesWithContents.forEach(({ contents, file }) => {
3819
4253
  bundleHash.update(relative3(config.bundleDirectory, file).replaceAll("\\", "/"));
3820
4254
  bundleHash.update("\x00");
@@ -3822,16 +4256,18 @@ var syncAbsoluteExpoWebAssets = async (config) => {
3822
4256
  bundleHash.update("\x00");
3823
4257
  });
3824
4258
  const bundleId = `amexpo_${bundleHash.digest("hex")}`;
3825
- const destination = join6(config.nativeProjectDirectory, "assets", "absolute");
3826
- await mkdir2(dirname3(destination), { recursive: true });
3827
- const staging = await mkdtemp(join6(dirname3(destination), `.${basename(destination)}.stage-`));
4259
+ const destination = join8(config.nativeProjectDirectory, "assets", "absolute");
4260
+ await mkdir2(dirname4(destination), { recursive: true });
4261
+ const staging = await mkdtemp(join8(dirname4(destination), `.${basename(destination)}.stage-`));
3828
4262
  let assets;
3829
4263
  let archiveAsset;
4264
+ let archiveMd5;
3830
4265
  try {
3831
4266
  let offset = 0;
3832
4267
  assets = filesWithContents.map(({ contents, file }) => {
3833
4268
  const entry = {
3834
4269
  length: contents.byteLength,
4270
+ md5: createHash3("md5").update(contents).digest("hex"),
3835
4271
  offset,
3836
4272
  path: relative3(config.bundleDirectory, file).replaceAll("\\", "/")
3837
4273
  };
@@ -3839,16 +4275,18 @@ var syncAbsoluteExpoWebAssets = async (config) => {
3839
4275
  return entry;
3840
4276
  });
3841
4277
  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));
4278
+ const archiveContents = Buffer.concat(filesWithContents.map(({ contents }) => contents));
4279
+ archiveMd5 = createHash3("md5").update(archiveContents).digest("hex");
4280
+ await writeFile2(join8(staging, name), archiveContents);
4281
+ archiveAsset = portableRelative2(join8(config.nativeProjectDirectory, "src", "generated"), join8(destination, name));
3844
4282
  await installStagedDirectory(staging, destination);
3845
4283
  } catch (error) {
3846
4284
  await rm2(staging, { force: true, recursive: true });
3847
4285
  throw error;
3848
4286
  }
3849
- const generated = join6(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
3850
- await writeManagedFile(generated, assetModuleSource(assets, archiveAsset, bundleId, nativeDataManifest), true);
4287
+ const generated = join8(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
4288
+ await writeManagedFile(generated, assetModuleSource(assets, archiveAsset, bundleId, archiveMd5, nativeDataManifest), true);
3851
4289
  return { appBuild, assets: assets.length, bundleId, path: destination };
3852
4290
  };
3853
4291
 
3854
- export { discoverAbsoluteSyncSchema, absoluteDeviceNativeRequirements, assertAbsoluteDeviceCapabilityPackages, resolveAbsoluteDeviceCapabilityPlan, ABSOLUTE_MOBILE_UPDATE_FORMAT, absoluteMobileUpdateSigningPayload, canonicalizeAbsoluteMobileUpdate, parseAbsoluteMobileUpdateManifest, unsignedAbsoluteMobileUpdate, resolveAbsoluteMobileUpdateRuntime, ABSOLUTE_CAPACITOR_ASSETS_SPEC, generateAbsoluteCapacitorBranding, inspectAbsoluteMobileBranding, renderAbsoluteMobileBrandingPreview, writeAbsoluteExpoProject, syncAbsoluteExpoWebAssets };
4292
+ export { ABSOLUTE_NATIVE_EXACT_PACKAGES, CAPACITOR_PACKAGE_SPECS, CAPACITOR_SYNC_PACKAGE_SPECS, assertNativePackageVersions, packageNameFromSpec, packagesNeedingExactInstall, discoverAbsoluteSyncSchema, absoluteDeviceNativeRequirements, assertAbsoluteDeviceCapabilityPackages, resolveAbsoluteDeviceCapabilityPlan, ABSOLUTE_MOBILE_UPDATE_FORMAT, absoluteMobileUpdateSigningPayload, canonicalizeAbsoluteMobileUpdate, parseAbsoluteMobileUpdateManifest, unsignedAbsoluteMobileUpdate, resolveAbsoluteMobileUpdateRuntime, ABSOLUTE_CAPACITOR_ASSETS_SPEC, generateAbsoluteCapacitorBranding, inspectAbsoluteMobileBranding, renderAbsoluteMobileBrandingPreview, writeAbsoluteExpoProject, syncAbsoluteExpoWebAssets };