@absolutejs/absolute 0.20.0-beta.44 → 0.20.0-beta.46

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.
@@ -307,7 +307,7 @@ var normalizeSlug = (str) => str.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-
307
307
  // src/mobile/nativeAuth.ts
308
308
  import { readFileSync as readFileSync2 } from "fs";
309
309
  import { join as join11 } from "path";
310
- var ABSOLUTE_AUTH_PACKAGE = "@absolutejs/auth", ABSOLUTE_EXPO_AUTH_CORE_VERSION = "0.75.6", ABSOLUTE_EXPO_AUTH_PACKAGE = "@absolutejs/auth-expo", ABSOLUTE_EXPO_AUTH_VERSION = "0.0.2", ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV = "ABSOLUTE_AUTH_NATIVE_CLIENTS", ABSOLUTE_NATIVE_AUTH_SCOPES, ABSOLUTE_SYNC_PACKAGE = "@absolutejs/sync", readPackageManifest = (projectRoot) => {
310
+ var ABSOLUTE_AUTH_PACKAGE = "@absolutejs/auth", ABSOLUTE_EXPO_AUTH_CORE_VERSION = "0.75.6", ABSOLUTE_EXPO_AUTH_PACKAGE = "@absolutejs/auth-expo", ABSOLUTE_EXPO_AUTH_VERSION = "0.0.2", ABSOLUTE_EXPO_SYNC_CORE_VERSION = "2.31.0", ABSOLUTE_EXPO_SYNC_PACKAGE = "@absolutejs/sync-expo", ABSOLUTE_EXPO_SYNC_VERSION = "0.0.2", ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV = "ABSOLUTE_AUTH_NATIVE_CLIENTS", ABSOLUTE_NATIVE_AUTH_SCOPES, ABSOLUTE_SYNC_PACKAGE = "@absolutejs/sync", readPackageManifest = (projectRoot) => {
311
311
  try {
312
312
  return JSON.parse(readFileSync2(join11(projectRoot, "package.json"), "utf8"));
313
313
  } catch {
@@ -349,7 +349,7 @@ var init_nativeAuth = __esm(() => {
349
349
  });
350
350
 
351
351
  // node_modules/@absolutejs/sync/dist/client/index.js
352
- var RUNTIME_TRANSPORT, host, isRegistry = (value) => typeof value === "object" && value !== null && Array.isArray(Reflect.get(value, "installations")) && Array.isArray(Reflect.get(value, "clients")), registry, SyncLocalDataPolicyError, SyncLocalStoreSchemaError, positiveVersion = (value, label) => {
352
+ var RUNTIME_TRANSPORT, host, isRegistry = (value) => typeof value === "object" && value !== null && Array.isArray(Reflect.get(value, "installations")) && Array.isArray(Reflect.get(value, "clients")), registry, pools, SyncLocalDataPolicyError, SyncLocalStoreSchemaError, positiveVersion = (value, label) => {
353
353
  if (!Number.isSafeInteger(value) || value < 1)
354
354
  throw new SyncLocalStoreSchemaError("INVALID_PLAN", `${label} must be a positive safe integer`);
355
355
  return value;
@@ -454,6 +454,7 @@ var init_client = __esm(() => {
454
454
  });
455
455
  return created;
456
456
  })();
457
+ pools = new Map;
457
458
  SyncLocalDataPolicyError = class SyncLocalDataPolicyError extends Error {
458
459
  code;
459
460
  constructor(code, message) {
@@ -771,10 +772,11 @@ var init_syncSchema = __esm(() => {
771
772
  });
772
773
 
773
774
  // src/mobile/deviceCapabilities.ts
774
- import { readFileSync as readFileSync4 } from "fs";
775
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
775
776
  import { extname as extname3, join as join13, relative as relative9, resolve as resolve11 } from "path";
777
+ import { fileURLToPath } from "url";
776
778
  import ts from "typescript";
777
- var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/devices-capacitor", SOURCE_GLOB, IGNORED_DIRECTORIES, IDENTIFIER_PATTERN, CAPACITOR_MODULE_PATTERN, CAPACITOR_PACKAGE_PATTERN, ANDROID_PERMISSION_PATTERN, IOS_USAGE_DESCRIPTIONS, IOS_PRIVACY_ACCESSED_API_REASONS, IOS_PRIVACY_ACCESSED_APIS, object2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), readJson = (path) => {
779
+ var DEVICES_PACKAGE = "@absolutejs/devices", ADAPTERS, SOURCE_GLOB, IGNORED_DIRECTORIES, IDENTIFIER_PATTERN, providerModulePattern = (provider) => new RegExp(`^@absolutejs/devices-${provider}/[a-z][a-z0-9-]*$`, "u"), providerPackagePattern = (provider) => provider === "capacitor" ? /^@capacitor\/[a-z][a-z0-9-]*@\d+\.\d+\.\d+$/u : /^(?:expo-[a-z][a-z0-9-]*|@react-native-[a-z0-9-]+\/[a-z][a-z0-9-]*)@\d+\.\d+\.\d+$/u, providerLabel = (provider) => provider === "capacitor" ? "Capacitor" : "Expo", ANDROID_PERMISSION_PATTERN, IOS_USAGE_DESCRIPTIONS, IOS_PRIVACY_ACCESSED_API_REASONS, IOS_PRIVACY_ACCESSED_APIS, object2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), readJson = (path) => {
778
780
  const value = JSON.parse(readFileSync4(path, "utf8"));
779
781
  if (!object2(value))
780
782
  throw new TypeError(`${path} must contain an object.`);
@@ -834,7 +836,7 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
834
836
  ...systemBars === true ? { systemBars: true } : {},
835
837
  ...usageDescriptions === undefined ? {} : { usageDescriptions: [...usageDescriptions] }
836
838
  };
837
- }, parseProvider = (name, value) => {
839
+ }, parseProvider = (name, value, providerName) => {
838
840
  if (!IDENTIFIER_PATTERN.test(name))
839
841
  throw new TypeError("Device capability names must be identifiers.");
840
842
  if (!object2(value))
@@ -843,10 +845,13 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
843
845
  const module = text(value.module, `${name}.module`);
844
846
  if (!IDENTIFIER_PATTERN.test(factory))
845
847
  throw new TypeError(`${name}.factory must be a JavaScript identifier.`);
846
- if (!CAPACITOR_MODULE_PATTERN.test(module))
847
- throw new TypeError(`${name}.module must be an official devices-capacitor subpath.`);
848
- if (!Array.isArray(value.packages) || !value.packages.every((spec) => typeof spec === "string" && CAPACITOR_PACKAGE_PATTERN.test(spec)))
849
- throw new TypeError(`${name}.packages must contain exact official Capacitor package versions.`);
848
+ if (!providerModulePattern(providerName).test(module))
849
+ throw new TypeError(`${name}.module must be an official devices-${providerName} subpath.`);
850
+ if (!Array.isArray(value.packages) || !value.packages.every((spec) => typeof spec === "string" && providerPackagePattern(providerName).test(spec)))
851
+ throw new TypeError(`${name}.packages must contain exact official ${providerLabel(providerName)} package versions.`);
852
+ const { plugins } = value;
853
+ if (plugins !== undefined && (!Array.isArray(plugins) || !plugins.every((plugin) => typeof plugin === "string" && /^expo-[a-z][a-z0-9-]*$/u.test(plugin))))
854
+ throw new TypeError(`${name}.plugins must contain Expo config plugin names.`);
850
855
  let native;
851
856
  const { native: nativeMetadata } = value;
852
857
  if (nativeMetadata !== undefined) {
@@ -864,6 +869,7 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
864
869
  factory,
865
870
  module,
866
871
  ...native === undefined ? {} : { native },
872
+ ...plugins === undefined ? {} : { plugins: [...plugins] },
867
873
  packages: [...value.packages]
868
874
  };
869
875
  }, absoluteDeviceNativeRequirements = (plan) => {
@@ -893,18 +899,27 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
893
899
  ...new Set(plan.capabilities.flatMap((name) => plan.providers[name]?.native?.ios?.usageDescriptions ?? []))
894
900
  ].sort()
895
901
  };
896
- }, loadAbsoluteDeviceCapabilityProviders = (projectRoot) => {
897
- const path = join13(resolve11(projectRoot), "node_modules", CAPACITOR_ADAPTER, "package.json");
902
+ }, loadAbsoluteDeviceCapabilityProviders = (projectRoot, provider = "capacitor") => {
903
+ const adapter = ADAPTERS[provider];
904
+ let path = join13(resolve11(projectRoot), "node_modules", adapter, "package.json");
905
+ try {
906
+ readFileSync4(path, "utf8");
907
+ } catch {
908
+ path = fileURLToPath(import.meta.resolve(`${adapter}/package.json`));
909
+ }
898
910
  const manifest = readJson(path);
899
911
  const { absolutejs } = manifest;
900
912
  const devices = object2(absolutejs) ? absolutejs.devices : undefined;
901
- if (!object2(devices) || devices.format !== 1 || devices.provider !== "capacitor" || !object2(devices.capabilities))
902
- throw new TypeError(`${CAPACITOR_ADAPTER} does not publish supported capability metadata.`);
903
- const entries = Object.entries(devices.capabilities).map(([name, provider]) => ({
913
+ if (!object2(devices) || devices.format !== 1 || devices.provider !== provider || !object2(devices.capabilities))
914
+ throw new TypeError(`${adapter} does not publish supported capability metadata.`);
915
+ const entries = Object.entries(devices.capabilities).map(([name, capability]) => ({
904
916
  name,
905
- provider: parseProvider(name, provider)
917
+ provider: parseProvider(name, capability, provider)
906
918
  }));
907
- return Object.fromEntries(entries.sort((left, right) => left.name.localeCompare(right.name)).map(({ name, provider }) => [name, provider]));
919
+ return Object.fromEntries(entries.sort((left, right) => left.name.localeCompare(right.name)).map(({ name, provider: capabilityProvider }) => [
920
+ name,
921
+ capabilityProvider
922
+ ]));
908
923
  }, isIgnored = (path) => path.split("/").some((segment) => IGNORED_DIRECTORIES.has(segment)), importedCapabilities = (source, file) => {
909
924
  const names = new Set;
910
925
  const namespaces = new Set;
@@ -961,6 +976,8 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
961
976
  return packages;
962
977
  }, discoverAbsoluteDeviceCapabilities = (projectRoot, providers = loadAbsoluteDeviceCapabilityProviders(projectRoot)) => {
963
978
  const root = resolve11(projectRoot);
979
+ if (!existsSync3(root))
980
+ return [];
964
981
  const known = new Set(Object.keys(providers));
965
982
  const capabilities = new Set;
966
983
  for (const path of SOURCE_GLOB.scanSync({ cwd: root })) {
@@ -987,14 +1004,14 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
987
1004
  return true;
988
1005
  }
989
1006
  return false;
990
- }, resolveAbsoluteDeviceCapabilityPlan = (projectRoot) => {
991
- const allProviders = loadAbsoluteDeviceCapabilityProviders(projectRoot);
1007
+ }, resolveAbsoluteDeviceCapabilityPlan = (projectRoot, provider = "capacitor") => {
1008
+ const allProviders = loadAbsoluteDeviceCapabilityProviders(projectRoot, provider);
992
1009
  const capabilities = discoverAbsoluteDeviceCapabilities(projectRoot, allProviders);
993
1010
  const providers = {};
994
1011
  for (const name of capabilities) {
995
- const provider = allProviders[name];
996
- if (provider)
997
- providers[name] = provider;
1012
+ const capabilityProvider = allProviders[name];
1013
+ if (capabilityProvider)
1014
+ providers[name] = capabilityProvider;
998
1015
  }
999
1016
  return {
1000
1017
  capabilities,
@@ -1005,6 +1022,10 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
1005
1022
  };
1006
1023
  };
1007
1024
  var init_deviceCapabilities = __esm(() => {
1025
+ ADAPTERS = {
1026
+ capacitor: "@absolutejs/devices-capacitor",
1027
+ expo: "@absolutejs/devices-expo"
1028
+ };
1008
1029
  SOURCE_GLOB = new Bun.Glob("**/*.{js,jsx,ts,tsx,svelte,vue}");
1009
1030
  IGNORED_DIRECTORIES = new Set([
1010
1031
  ".absolutejs",
@@ -1018,8 +1039,6 @@ var init_deviceCapabilities = __esm(() => {
1018
1039
  "tests"
1019
1040
  ]);
1020
1041
  IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/u;
1021
- CAPACITOR_MODULE_PATTERN = /^@absolutejs\/devices-capacitor\/[a-z][a-z0-9-]*$/u;
1022
- CAPACITOR_PACKAGE_PATTERN = /^@capacitor\/[a-z][a-z0-9-]*@\d+\.\d+\.\d+$/u;
1023
1042
  ANDROID_PERMISSION_PATTERN = /^android\.permission\.[A-Z][A-Z0-9_]*$/u;
1024
1043
  IOS_USAGE_DESCRIPTIONS = new Set([
1025
1044
  "camera",
@@ -1037,12 +1056,12 @@ var init_deviceCapabilities = __esm(() => {
1037
1056
  });
1038
1057
 
1039
1058
  // src/cli/scripts/telemetry.ts
1040
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
1059
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
1041
1060
  import { homedir as homedir3 } from "os";
1042
1061
  import { join as join17 } from "path";
1043
1062
  var configDir, configPath, getTelemetryConfig = () => {
1044
1063
  try {
1045
- if (!existsSync4(configPath))
1064
+ if (!existsSync5(configPath))
1046
1065
  return null;
1047
1066
  const raw = readFileSync5(configPath, "utf-8");
1048
1067
  const config = JSON.parse(raw);
@@ -1057,11 +1076,11 @@ var init_telemetry = __esm(() => {
1057
1076
  });
1058
1077
 
1059
1078
  // src/cli/telemetryEvent.ts
1060
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
1079
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1061
1080
  import { arch, platform } from "os";
1062
1081
  import { dirname as dirname12, join as join18, parse } from "path";
1063
1082
  var checkCandidate = (candidate) => {
1064
- if (!existsSync5(candidate)) {
1083
+ if (!existsSync6(candidate)) {
1065
1084
  return null;
1066
1085
  }
1067
1086
  const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
@@ -5615,6 +5634,12 @@ var shellSyncModule = () => {
5615
5634
  return candidate;
5616
5635
  throw new TypeError("AbsoluteJS mobile Sync shell module is missing.");
5617
5636
  };
5637
+ var shellExpoSyncModule = () => {
5638
+ const candidate = ["js", "ts"].map((extension) => join9(import.meta.dir, `shellExpoSync.${extension}`)).find(existsSync2);
5639
+ if (candidate)
5640
+ return candidate;
5641
+ throw new TypeError("AbsoluteJS Expo Sync bridge module is missing.");
5642
+ };
5618
5643
  var shellPushModule = () => {
5619
5644
  const candidate = ["js", "ts"].map((extension) => join9(import.meta.dir, `shellPush.${extension}`)).find(existsSync2);
5620
5645
  if (candidate)
@@ -5692,10 +5717,11 @@ var buildShellBootstrap = async (staging, auth, sync, storagePrefix, engine, dev
5692
5717
  const shellCapabilities = capacitor ? deviceCapabilities.capabilities : [];
5693
5718
  const modulePath = shellBootstrapModule();
5694
5719
  const authFactory = capacitor ? "createAbsoluteMobileShellAuth" : "createAbsoluteExpoShellAuth";
5720
+ const syncFactory = capacitor ? "installAbsoluteMobileShellSync" : "installAbsoluteExpoShellSync";
5695
5721
  const authImport = auth ? `import { ${authFactory} } from ${JSON.stringify(capacitor ? shellAuthModule() : shellExpoAuthModule())};
5696
5722
  ` : "";
5697
- const options = auth ? `{ createAuth: ${authFactory}${sync ? ", installSync: installAbsoluteMobileShellSync" : ""} }` : "";
5698
- const syncImport = sync ? `import { installAbsoluteMobileShellSync } from ${JSON.stringify(shellSyncModule())};
5723
+ const options = auth ? `{ createAuth: ${authFactory}${sync ? `, installSync: ${syncFactory}` : ""} }` : "";
5724
+ const syncImport = sync ? `import { ${syncFactory} } from ${JSON.stringify(capacitor ? shellSyncModule() : shellExpoSyncModule())};
5699
5725
  ` : "";
5700
5726
  const pushIndex = shellCapabilities.indexOf("pushNotifications");
5701
5727
  const push = pushIndex !== -1;
@@ -5715,12 +5741,12 @@ const absoluteMobilePushCapability = absoluteDeviceCapability${pushIndex}(absolu
5715
5741
  const entryPath = join9(staging, ".absolute-mobile-entry.ts");
5716
5742
  const baseAdapterModule = capacitor ? await resolveProjectImport(projectRoot, "@absolutejs/devices-capacitor") : shellExpoDevicesModule();
5717
5743
  const adapterImport = capacitor ? `import { installCapacitorDeviceAdapterIfNative } from ${JSON.stringify(baseAdapterModule)};` : `import { createAbsoluteExpoBridgeFetch, installAbsoluteExpoWebDeviceAdapter } from ${JSON.stringify(baseAdapterModule)};`;
5718
- const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : "installAbsoluteExpoWebDeviceAdapter();";
5744
+ const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : `installAbsoluteExpoWebDeviceAdapter(${JSON.stringify(deviceCapabilities.capabilities)});`;
5719
5745
  let shellOptions = auth ? options : "{ createFetch: createAbsoluteExpoBridgeFetch }";
5720
5746
  if (capacitor)
5721
5747
  shellOptions = options;
5722
5748
  if (push) {
5723
- shellOptions = `{ createAuth: (config, options) => createAbsoluteMobileShellAuth(config, options), beforeSignOut: absoluteMobilePush.beforeSignOut, connectPush: (auth) => absoluteMobilePush.connect(auth, absoluteMobilePushCapability)${sync ? ", installSync: installAbsoluteMobileShellSync" : ""} }`;
5749
+ shellOptions = `{ createAuth: (config, options) => createAbsoluteMobileShellAuth(config, options), beforeSignOut: absoluteMobilePush.beforeSignOut, connectPush: (auth) => absoluteMobilePush.connect(auth, absoluteMobilePushCapability)${sync ? `, installSync: ${syncFactory}` : ""} }`;
5724
5750
  }
5725
5751
  await writeFile10(entryPath, `import { startAbsoluteMobileShell } from ${JSON.stringify(modulePath)};
5726
5752
  ${adapterImport}
@@ -6087,6 +6113,8 @@ init_deviceCapabilities();
6087
6113
 
6088
6114
  // src/mobile/expoProject.ts
6089
6115
  init_nativeAuth();
6116
+ init_syncSchema();
6117
+ init_deviceCapabilities();
6090
6118
  import {
6091
6119
  access as access9,
6092
6120
  cp as cp2,
@@ -6125,12 +6153,22 @@ var routeSegments = (route) => route.split("/").filter(Boolean).map((segment) =>
6125
6153
  return segment;
6126
6154
  });
6127
6155
  var routeFile = (project, route) => join14(project, "app", ...routeSegments(route), "index.tsx");
6128
- var expoPackage = (auth) => ({
6156
+ var packageDependencies = (plan) => Object.fromEntries(plan.requiredPackages.map((spec) => {
6157
+ const separator = spec.lastIndexOf("@");
6158
+ return [spec.slice(0, separator), spec.slice(separator + 1)];
6159
+ }));
6160
+ var expoPackage = (auth, sync, devices) => ({
6129
6161
  dependencies: {
6162
+ "@absolutejs/devices": "0.7.0",
6163
+ "@absolutejs/devices-expo": "0.0.2",
6130
6164
  ...auth ? {
6131
6165
  "@absolutejs/auth": ABSOLUTE_EXPO_AUTH_CORE_VERSION,
6132
6166
  [ABSOLUTE_EXPO_AUTH_PACKAGE]: ABSOLUTE_EXPO_AUTH_VERSION
6133
6167
  } : {},
6168
+ ...sync ? {
6169
+ "@absolutejs/sync": ABSOLUTE_EXPO_SYNC_CORE_VERSION,
6170
+ [ABSOLUTE_EXPO_SYNC_PACKAGE]: ABSOLUTE_EXPO_SYNC_VERSION
6171
+ } : {},
6134
6172
  expo: "~57.0.9",
6135
6173
  "expo-asset": "~57.0.15",
6136
6174
  "expo-constants": "~57.0.16",
@@ -6138,6 +6176,13 @@ var expoPackage = (auth) => ({
6138
6176
  "expo-file-system": "~57.0.6",
6139
6177
  "expo-haptics": "~57.0.2",
6140
6178
  "expo-linking": "~57.0.8",
6179
+ ...sync ? {
6180
+ "expo-background-task": "~57.0.14",
6181
+ "expo-network": "~57.0.1",
6182
+ "expo-sqlite": "~57.0.2",
6183
+ "expo-task-manager": "~57.0.14",
6184
+ "expo-updates": "~57.0.19"
6185
+ } : {},
6141
6186
  "expo-router": "~57.0.17",
6142
6187
  ...auth ? {
6143
6188
  "expo-secure-store": "~57.0.2",
@@ -6147,7 +6192,8 @@ var expoPackage = (auth) => ({
6147
6192
  "react-native": "0.86.3",
6148
6193
  "react-native-safe-area-context": "~5.7.0",
6149
6194
  "react-native-screens": "4.26.0",
6150
- "react-native-webview": "13.16.1"
6195
+ "react-native-webview": "13.16.1",
6196
+ ...packageDependencies(devices)
6151
6197
  },
6152
6198
  devDependencies: {
6153
6199
  "@types/react": "~19.2.2",
@@ -6163,35 +6209,107 @@ var expoPackage = (auth) => ({
6163
6209
  },
6164
6210
  version: "0.0.0"
6165
6211
  });
6166
- var expoAppConfig = (config, auth) => ({
6167
- expo: {
6168
- android: {
6169
- intentFilters: config.deepLinkHosts.map((host2) => ({
6170
- action: "VIEW",
6171
- autoVerify: true,
6172
- category: ["BROWSABLE", "DEFAULT"],
6173
- data: [{ host: host2, pathPrefix: "/", scheme: "https" }]
6174
- })),
6175
- package: config.appId
6176
- },
6177
- experiments: { typedRoutes: true },
6178
- ios: {
6179
- associatedDomains: config.deepLinkHosts.map((host2) => `applinks:${host2}`),
6180
- bundleIdentifier: config.appId,
6181
- ...config.iosVersion ? { buildNumber: config.iosVersion } : {}
6182
- },
6183
- name: config.appName,
6184
- plugins: [
6185
- "expo-router",
6186
- ["expo-dev-client", { launchMode: "most-recent" }],
6187
- ...auth ? ["expo-secure-store"] : []
6188
- ],
6189
- runtimeVersion: { policy: "appVersion" },
6190
- scheme: config.deepLinkScheme,
6191
- slug: config.appId.toLowerCase().replaceAll(".", "-"),
6192
- version: config.iosVersion ?? "0.1.0"
6193
- }
6194
- });
6212
+ var expoAppConfig = (config, auth, sync, devices) => {
6213
+ const requirements = absoluteDeviceNativeRequirements(devices);
6214
+ const usageKey = (purpose) => {
6215
+ if (purpose === "camera")
6216
+ return "NSCameraUsageDescription";
6217
+ if (purpose === "photo-library")
6218
+ return "NSPhotoLibraryUsageDescription";
6219
+ if (purpose === "photo-library-add")
6220
+ return "NSPhotoLibraryAddUsageDescription";
6221
+ if (purpose === "location-always")
6222
+ return "NSLocationAlwaysAndWhenInUseUsageDescription";
6223
+ return "NSLocationWhenInUseUsageDescription";
6224
+ };
6225
+ const usageDescription = (purpose) => {
6226
+ if (purpose === "camera")
6227
+ return `${config.appName} uses your camera when you choose to take a photo.`;
6228
+ if (purpose === "photo-library")
6229
+ return `${config.appName} accesses your photo library only for photo actions you choose.`;
6230
+ if (purpose.startsWith("location-"))
6231
+ return `${config.appName} uses your location only while you use the app and request a location-based action.`;
6232
+ return `${config.appName} adds to your photo library only for photo actions you choose.`;
6233
+ };
6234
+ const descriptions = Object.fromEntries(requirements.iosUsageDescriptions.map((purpose) => [
6235
+ usageKey(purpose),
6236
+ usageDescription(purpose)
6237
+ ]));
6238
+ const devicePlugins = [
6239
+ ...new Set(devices.capabilities.flatMap((name) => devices.providers[name]?.plugins ?? []))
6240
+ ];
6241
+ const configuredPlugins = devicePlugins.map((plugin) => {
6242
+ if (plugin === "expo-image-picker")
6243
+ return [
6244
+ plugin,
6245
+ {
6246
+ cameraPermission: descriptions.NSCameraUsageDescription,
6247
+ microphonePermission: false,
6248
+ photosPermission: descriptions.NSPhotoLibraryUsageDescription
6249
+ }
6250
+ ];
6251
+ if (plugin === "expo-location")
6252
+ return [
6253
+ plugin,
6254
+ {
6255
+ locationWhenInUsePermission: descriptions.NSLocationWhenInUseUsageDescription
6256
+ }
6257
+ ];
6258
+ return plugin;
6259
+ });
6260
+ return {
6261
+ expo: {
6262
+ android: {
6263
+ ...devicePlugins.includes("expo-image-picker") ? {
6264
+ blockedPermissions: [
6265
+ "android.permission.READ_EXTERNAL_STORAGE",
6266
+ "android.permission.RECORD_AUDIO",
6267
+ "android.permission.WRITE_EXTERNAL_STORAGE"
6268
+ ]
6269
+ } : {},
6270
+ intentFilters: config.deepLinkHosts.map((host2) => ({
6271
+ action: "VIEW",
6272
+ autoVerify: true,
6273
+ category: ["BROWSABLE", "DEFAULT"],
6274
+ data: [{ host: host2, pathPrefix: "/", scheme: "https" }]
6275
+ })),
6276
+ package: config.appId,
6277
+ permissions: requirements.androidPermissions
6278
+ },
6279
+ experiments: { typedRoutes: true },
6280
+ ios: {
6281
+ associatedDomains: config.deepLinkHosts.map((host2) => `applinks:${host2}`),
6282
+ bundleIdentifier: config.appId,
6283
+ infoPlist: descriptions,
6284
+ ...requirements.iosPrivacyAccessedApis.length > 0 ? {
6285
+ privacyManifests: {
6286
+ NSPrivacyAccessedAPITypes: requirements.iosPrivacyAccessedApis.map(({ api, reasons }) => ({
6287
+ NSPrivacyAccessedAPIType: api,
6288
+ NSPrivacyAccessedAPITypeReasons: reasons
6289
+ }))
6290
+ }
6291
+ } : {},
6292
+ ...config.iosVersion ? { buildNumber: config.iosVersion } : {}
6293
+ },
6294
+ name: config.appName,
6295
+ plugins: [
6296
+ "expo-router",
6297
+ ["expo-dev-client", { launchMode: "most-recent" }],
6298
+ ...auth ? ["expo-secure-store"] : [],
6299
+ ...sync ? [
6300
+ "expo-sqlite",
6301
+ "expo-background-task",
6302
+ "expo-task-manager"
6303
+ ] : [],
6304
+ ...configuredPlugins
6305
+ ],
6306
+ runtimeVersion: { policy: "appVersion" },
6307
+ scheme: config.deepLinkScheme,
6308
+ slug: config.appId.toLowerCase().replaceAll(".", "-"),
6309
+ version: config.iosVersion ?? "0.1.0"
6310
+ }
6311
+ };
6312
+ };
6195
6313
  var expoDynamicAppConfig = `${EXPO_GENERATED_HEADER}const config = require('./app.json');
6196
6314
 
6197
6315
  if (process.env.ABSOLUTE_EXPO_DEVELOPMENT === '1' && process.env.ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH) {
@@ -6258,14 +6376,20 @@ config.watchFolders = [appRoot];
6258
6376
 
6259
6377
  module.exports = config;
6260
6378
  `;
6261
- var layoutSource = (auth) => `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
6262
- ${auth ? `import { useEffect } from 'react';
6379
+ var layoutSource = (auth, sync) => `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
6380
+ import '../src/generated/AbsoluteDevices';
6381
+ ${auth ? `import { useEffect, useState } from 'react';
6263
6382
  import { startAbsoluteExpoAuth } from '../src/generated/AbsoluteAuth';` : ""}
6383
+ ${sync ? "import { startAbsoluteExpoSync } from '../src/generated/AbsoluteSync';" : ""}
6264
6384
 
6265
6385
  export default function AbsoluteLayout() {
6266
- ${auth ? `useEffect(() => {
6267
- void startAbsoluteExpoAuth();
6268
- }, []);` : ""}
6386
+ ${auth ? `const [ready, setReady] = useState(false);
6387
+ useEffect(() => {
6388
+ let active = true;
6389
+ void ${sync ? "startAbsoluteExpoSync" : "startAbsoluteExpoAuth"}().then(() => { if (active) setReady(true); });
6390
+ return () => { active = false; };
6391
+ }, []);
6392
+ if (!ready) return null;` : ""}
6269
6393
  return <Stack screenOptions={{ headerShown: false }} />;
6270
6394
  }
6271
6395
  `;
@@ -6300,6 +6424,76 @@ const styles = StyleSheet.create({
6300
6424
  title: { color: '#f8fafc', fontSize: 32, fontWeight: '800' }
6301
6425
  });
6302
6426
  `;
6427
+ var devicesRuntimeSource = (config, plan, auth) => {
6428
+ const imports = plan.capabilities.map((name, index) => {
6429
+ const provider = plan.providers[name];
6430
+ if (!provider)
6431
+ throw new TypeError(`Missing Expo device capability provider ${name}.`);
6432
+ return `import { ${provider.factory} as absoluteExpoCapability${index} } from ${JSON.stringify(provider.module)};`;
6433
+ });
6434
+ const pushIndex = plan.capabilities.indexOf("pushNotifications");
6435
+ const push = pushIndex !== -1;
6436
+ if (push && !auth)
6437
+ throw new TypeError("Expo push notifications require the provisioned AbsoluteJS Auth runtime.");
6438
+ const entries = plan.capabilities.map((name, index) => `${JSON.stringify(name)}: absoluteExpoCapability${index}(${name === "pushNotifications" ? "absoluteExpoPushOptions" : ""})`).join(`,
6439
+ `);
6440
+ const pushSource = push ? `const INSTALLATION_KEY = 'absolutejs.push.installation-id';
6441
+ const requirePushResponse = async (response: Response, operation: string) => {
6442
+ if (!response.ok) throw new Error(\`AbsoluteJS native push \${operation} failed with HTTP \${response.status}.\`);
6443
+ return response.json() as Promise<Record<string, unknown>>;
6444
+ };
6445
+ const absoluteExpoPushOptions = {
6446
+ onRegistration: async (registration: { platform: 'apns' | 'fcm'; token: string }) => {
6447
+ const known = await absoluteExpoDevices.storage.get(INSTALLATION_KEY);
6448
+ const register = (installationId?: string | null) => absoluteExpoAuth.fetch('/auth/push', {
6449
+ body: JSON.stringify({ ...(installationId ? { installationId } : {}), platform: registration.platform, token: registration.token }),
6450
+ headers: { 'content-type': 'application/json' },
6451
+ method: 'POST'
6452
+ });
6453
+ let response = await register(known);
6454
+ const conflict = response.status === 409 && await response.clone().json().then(value => typeof value === 'object' && value !== null && Reflect.get(value, 'code') === 'installation-ownership').catch(() => false);
6455
+ if (known && conflict) {
6456
+ await absoluteExpoDevices.storage.remove(INSTALLATION_KEY);
6457
+ response = await register();
6458
+ }
6459
+ const result = await requirePushResponse(response, 'registration');
6460
+ if (typeof result.installationId !== 'string' || !result.installationId || result.installationId.length > 128) throw new Error('AbsoluteJS native push returned an invalid installation identity.');
6461
+ await absoluteExpoDevices.storage.set(INSTALLATION_KEY, result.installationId);
6462
+ },
6463
+ onUnregistration: async () => {
6464
+ const installationId = await absoluteExpoDevices.storage.get(INSTALLATION_KEY);
6465
+ if (!installationId) return;
6466
+ await requirePushResponse(await absoluteExpoAuth.fetch('/auth/push', {
6467
+ body: JSON.stringify({ installationId }),
6468
+ headers: { 'content-type': 'application/json' },
6469
+ method: 'DELETE'
6470
+ }), 'removal');
6471
+ await absoluteExpoDevices.storage.remove(INSTALLATION_KEY);
6472
+ }
6473
+ };` : "";
6474
+ return `${EXPO_GENERATED_HEADER}import { installDeviceAdapter } from '@absolutejs/devices/runtime';
6475
+ import { createExpoDeviceAdapter } from '@absolutejs/devices-expo';
6476
+ ${push ? "import { absoluteExpoAuth } from './AbsoluteAuth';" : ""}
6477
+ ${imports.join(`
6478
+ `)}
6479
+
6480
+ ${pushSource}
6481
+
6482
+ export const absoluteExpoDeviceCapabilities = ${JSON.stringify(plan.capabilities)} as const;
6483
+ export const absoluteExpoDevices = createExpoDeviceAdapter({
6484
+ storagePrefix: ${JSON.stringify(`absolutejs.${config.appId}.`)},
6485
+ ${entries}
6486
+ });
6487
+ installDeviceAdapter(absoluteExpoDevices);
6488
+ export const beforeAbsoluteExpoDeviceSignOut = async () => {
6489
+ ${push ? "await absoluteExpoDevices.pushNotifications?.disable();" : ""}
6490
+ };
6491
+ ${push ? `absoluteExpoAuth.onPrincipalChange(principal => {
6492
+ if (!principal) return;
6493
+ void absoluteExpoDevices.pushNotifications?.queryPermission().then(permission => permission.state === 'granted' ? absoluteExpoDevices.pushNotifications?.enable() : undefined).catch(() => undefined);
6494
+ });` : ""}
6495
+ `;
6496
+ };
6303
6497
  var authRuntimeSource = (auth, appId) => {
6304
6498
  const storageIdentity = createHash10("sha256").update(appId).digest("hex").slice(0, 24);
6305
6499
  return `${EXPO_GENERATED_HEADER}import { createAbsoluteExpoAuthClient } from '@absolutejs/auth-expo';
@@ -6325,28 +6519,128 @@ export const getAbsoluteExpoAuthPrincipal = () => currentPrincipal;
6325
6519
  let startPromise: Promise<void> | undefined;
6326
6520
  export const startAbsoluteExpoAuth = () => {
6327
6521
  startPromise ??= absoluteExpoAuth.start().then(async () => {
6328
- await absoluteExpoAuth.principal();
6522
+ currentPrincipal = await absoluteExpoAuth.principal();
6329
6523
  });
6330
6524
 
6331
6525
  return startPromise;
6332
6526
  };
6333
6527
  `;
6334
6528
  };
6335
- var webHostSource = (config, auth) => {
6529
+ var syncRuntimeSource = (config, schema) => {
6530
+ const storageIdentity = createHash10("sha256").update(config.appId).digest("hex").slice(0, 24);
6531
+ const backgroundEndpoint = new URL("/__absolute/sync/background", config.productionOrigin).href;
6532
+ const backgroundTask = `${config.appId}.absolutejs.sync`;
6533
+ return `${EXPO_GENERATED_HEADER}import { runHeadlessSync } from '@absolutejs/sync/client/headless';
6534
+ import type { SyncLocalStoreSchemaBundle } from '@absolutejs/sync/client';
6535
+ import { installSyncClientRuntimeTransport } from '@absolutejs/sync/client/runtime';
6536
+ import {
6537
+ createExpoSyncBridgeHost,
6538
+ createExpoSyncLocalStore,
6539
+ createExpoSyncProtection,
6540
+ createExpoSyncSocketBridgeHost,
6541
+ defineExpoSyncBackgroundTask,
6542
+ installExpoSyncLifecycle,
6543
+ registerExpoSyncBackgroundTask
6544
+ } from '@absolutejs/sync-expo';
6545
+ import * as Updates from 'expo-updates';
6546
+ import { absoluteExpoAuth, startAbsoluteExpoAuth } from './AbsoluteAuth';
6547
+
6548
+ const BACKGROUND_TASK = ${JSON.stringify(backgroundTask)};
6549
+ const BACKGROUND_ENDPOINT = ${JSON.stringify(backgroundEndpoint)};
6550
+ const PRODUCTION_ORIGIN = ${JSON.stringify(config.productionOrigin)};
6551
+ const STORAGE_SCHEMA: SyncLocalStoreSchemaBundle = ${JSON.stringify(schema)};
6552
+ const store = createExpoSyncLocalStore({
6553
+ databaseName: ${JSON.stringify(`absolutejs-sync-${storageIdentity}.db`)},
6554
+ protection: createExpoSyncProtection({ storagePrefix: ${JSON.stringify(`absolutejs.sync.${storageIdentity}`)} }),
6555
+ storageSchema: STORAGE_SCHEMA
6556
+ });
6557
+
6558
+ const runBackgroundSync = async () => {
6559
+ await startAbsoluteExpoAuth();
6560
+ const principal = await absoluteExpoAuth.principal();
6561
+ if (!principal) return;
6562
+ await runHeadlessSync({
6563
+ endpoint: BACKGROUND_ENDPOINT,
6564
+ fetch: (url, init) => absoluteExpoAuth.fetch(url, init),
6565
+ namespace: principal.namespace,
6566
+ store
6567
+ });
6568
+ };
6569
+ defineExpoSyncBackgroundTask(BACKGROUND_TASK, runBackgroundSync);
6570
+
6571
+ let activeNamespace: string | undefined;
6572
+ let started = false;
6573
+ let startPromise: Promise<void> | undefined;
6574
+ export const startAbsoluteExpoSync = () => {
6575
+ startPromise ??= (async () => {
6576
+ await startAbsoluteExpoAuth();
6577
+ const principal = await absoluteExpoAuth.principal();
6578
+ activeNamespace = principal?.namespace;
6579
+ installSyncClientRuntimeTransport({
6580
+ ...(principal ? { durable: { namespace: principal.namespace, store }, socketTicket: () => absoluteExpoAuth.socketTicket() } : {}),
6581
+ registerClient: client => installExpoSyncLifecycle({ client })
6582
+ });
6583
+ await registerExpoSyncBackgroundTask(BACKGROUND_TASK, { minimumInterval: 15 });
6584
+ started = true;
6585
+ })();
6586
+
6587
+ return startPromise;
6588
+ };
6589
+
6590
+ absoluteExpoAuth.onPrincipalChange(principal => {
6591
+ if (!started || principal?.namespace === activeNamespace) return;
6592
+ void Updates.reloadAsync();
6593
+ });
6594
+
6595
+ export const createAbsoluteExpoSyncBridge = async (
6596
+ emit: (event: 'sync.socket' | 'sync.wake', payload: Record<string, unknown>) => void
6597
+ ) => {
6598
+ await startAbsoluteExpoSync();
6599
+ const principal = await absoluteExpoAuth.principal();
6600
+ if (!principal) return {
6601
+ close: () => undefined,
6602
+ request: async () => { throw new Error('Expo Sync requires an authenticated principal.'); }
6603
+ };
6604
+ const transactions = createExpoSyncBridgeHost({ namespace: principal.namespace, store });
6605
+ const sockets = createExpoSyncSocketBridgeHost({
6606
+ allowedOrigin: PRODUCTION_ORIGIN,
6607
+ emit: payload => emit('sync.socket', payload),
6608
+ socketTicket: audience => absoluteExpoAuth.socketTicket(audience)
6609
+ });
6610
+ const removeLifecycle = installExpoSyncLifecycle({
6611
+ client: { reconnect: () => emit('sync.wake', {}) }
6612
+ });
6613
+
6614
+ return {
6615
+ close: async () => {
6616
+ removeLifecycle();
6617
+ await Promise.all([transactions.close(), sockets.close()]);
6618
+ },
6619
+ request: (method: string, params: Record<string, unknown>) =>
6620
+ method.startsWith('sync.socket.')
6621
+ ? sockets.request(method, params)
6622
+ : transactions.request(method, params)
6623
+ };
6624
+ };
6625
+ `;
6626
+ };
6627
+ var webHostSource = (config, auth, sync) => {
6336
6628
  const nativeRoutes = [
6337
6629
  "/__absolute/native",
6338
6630
  ...Object.keys(config.expoNativeRoutes)
6339
6631
  ];
6340
- return `${EXPO_GENERATED_HEADER}import * as Haptics from 'expo-haptics';
6341
- import * as Linking from 'expo-linking';
6632
+ return `${EXPO_GENERATED_HEADER}import * as Linking from 'expo-linking';
6342
6633
  import { router, usePathname } from 'expo-router';
6343
6634
  import { useEffect, useRef, useState } from 'react';
6344
6635
  import { ActivityIndicator, BackHandler, Platform, StyleSheet, View } from 'react-native';
6345
6636
  import { WebView, type WebViewMessageEvent } from 'react-native-webview';
6346
6637
  import { materializeAbsoluteWebBundle } from './webAssets';
6638
+ import { createExpoDevicesBridgeHost } from '@absolutejs/devices-expo/bridge';
6639
+ import { absoluteExpoDevices, beforeAbsoluteExpoDeviceSignOut } from './AbsoluteDevices';
6347
6640
  ${auth ? "import { absoluteExpoAuth, getAbsoluteExpoAuthPrincipal, startAbsoluteExpoAuth } from './AbsoluteAuth';" : ""}
6641
+ ${sync ? "import { createAbsoluteExpoSyncBridge, startAbsoluteExpoSync } from './AbsoluteSync';" : ""}
6348
6642
 
6349
- const BRIDGE_FORMAT = 2;
6643
+ const BRIDGE_FORMAT = 3;
6350
6644
  const MAX_MESSAGE_BYTES = 64 * 1024;
6351
6645
  const MAX_HTTP_BODY_BYTES = 48 * 1024;
6352
6646
  const NATIVE_ROUTES = new Set(${JSON.stringify(nativeRoutes)});
@@ -6356,6 +6650,7 @@ const DEV_ORIGIN = Platform.OS === 'android'
6356
6650
  : process.env.EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN;
6357
6651
  const HMR_TARGET = Platform.OS === 'android' ? 'expo-android' : 'expo-ios';
6358
6652
  const AUTH_ENABLED = ${auth ? "true" : "false"};
6653
+ const SYNC_ENABLED = ${sync ? "true" : "false"};
6359
6654
 
6360
6655
  const bridgeBootstrap = (path: string) => {
6361
6656
  const initialPath = DEV_ORIGIN
@@ -6395,18 +6690,19 @@ const bridgeBootstrap = (path: string) => {
6395
6690
  },
6396
6691
  request(method, params) {
6397
6692
  const id = 'web_' + Date.now().toString(36) + '_' + (++sequence).toString(36);
6398
- send({ format: 2, id, kind: 'request', method, params, path: currentPath });
6693
+ send({ format: 3, id, kind: 'request', method, params, path: currentPath });
6399
6694
  return new Promise((resolve, reject) => {
6695
+ const interactive = method.startsWith('devices.camera.') || method.startsWith('devices.photos.') || method.startsWith('devices.documents.') || method.endsWith('.requestPermission');
6400
6696
  const timer = setTimeout(() => {
6401
6697
  pending.delete(id);
6402
6698
  reject(new Error('Expo bridge request timed out.'));
6403
- }, 10000);
6699
+ }, interactive ? 5 * 60 * 1000 : 30 * 1000);
6404
6700
  pending.set(id, { reject, resolve, timer });
6405
6701
  });
6406
6702
  },
6407
6703
  setPath(path) {
6408
6704
  currentPath = path;
6409
- send({ format: 2, kind: 'event', event: 'navigation', path });
6705
+ send({ format: 3, kind: 'event', event: 'navigation', path });
6410
6706
  }
6411
6707
  };
6412
6708
  if (\${DEV_ORIGIN ? 'true' : 'false'}) {
@@ -6414,7 +6710,7 @@ const bridgeBootstrap = (path: string) => {
6414
6710
  const path = location.pathname + location.search + location.hash;
6415
6711
  if (path === currentPath) return;
6416
6712
  currentPath = path;
6417
- send({ format: 2, kind: 'event', event: 'navigation', path });
6713
+ send({ format: 3, kind: 'event', event: 'navigation', path });
6418
6714
  };
6419
6715
  for (const method of ['pushState', 'replaceState']) {
6420
6716
  const original = history[method];
@@ -6433,23 +6729,12 @@ const bridgeBootstrap = (path: string) => {
6433
6729
  const url = new URL(anchor.href, location.href);
6434
6730
  if (!${JSON.stringify(nativeRoutes)}.includes(url.pathname)) return;
6435
6731
  event.preventDefault();
6436
- send({ format: 2, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
6732
+ send({ format: 3, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
6437
6733
  }, true);
6438
- send({ format: 2, kind: 'event', event: 'ready', path: currentPath });
6734
+ send({ format: 3, kind: 'event', event: 'ready', path: currentPath });
6439
6735
  })(); true;\`;
6440
6736
  };
6441
6737
 
6442
- const impact = async (params: Record<string, unknown>) => {
6443
- const style = params.style;
6444
- if (style === 'selection') return Haptics.selectionAsync();
6445
- if (style === 'success' || style === 'warning' || style === 'error') {
6446
- const value = style === 'success' ? Haptics.NotificationFeedbackType.Success : style === 'warning' ? Haptics.NotificationFeedbackType.Warning : Haptics.NotificationFeedbackType.Error;
6447
- return Haptics.notificationAsync(value);
6448
- }
6449
- const value = style === 'light' ? Haptics.ImpactFeedbackStyle.Light : style === 'heavy' ? Haptics.ImpactFeedbackStyle.Heavy : Haptics.ImpactFeedbackStyle.Medium;
6450
- return Haptics.impactAsync(value);
6451
- };
6452
-
6453
6738
  const bridgeFetch = async (params: Record<string, unknown>) => {
6454
6739
  if (typeof params.method !== 'string' || !['DELETE', 'GET', 'PATCH', 'POST', 'PUT'].includes(params.method) || typeof params.url !== 'string' || typeof params.headers !== 'object' || params.headers === null || Array.isArray(params.headers) || params.body !== undefined && typeof params.body !== 'string') throw new Error('Expo bridge HTTP request is invalid.');
6455
6740
  const url = new URL(params.url);
@@ -6481,21 +6766,36 @@ const authStatus = async () => {
6481
6766
  export function AbsoluteWebHost() {
6482
6767
  const pathname = usePathname() || '/';
6483
6768
  const webView = useRef<WebView>(null);
6769
+ const devicesBridge = useRef<{ close(): void | Promise<void>; request(method: string, params: Record<string, unknown>): Promise<unknown> } | undefined>(undefined);
6770
+ const syncBridge = useRef<{ close(): void | Promise<void>; request(method: string, params: Record<string, unknown>): Promise<unknown> } | undefined>(undefined);
6484
6771
  const [indexUri, setIndexUri] = useState<string>();
6485
6772
  const [canGoBack, setCanGoBack] = useState(false);
6486
- const [authReady, setAuthReady] = useState(!AUTH_ENABLED);
6773
+ const [runtimeReady, setRuntimeReady] = useState(!AUTH_ENABLED && !SYNC_ENABLED);
6774
+ const [devicesReady, setDevicesReady] = useState(false);
6487
6775
  const activeWebPath = useRef(pathname);
6488
6776
 
6489
6777
  useEffect(() => {
6490
- if (!AUTH_ENABLED) return;
6778
+ if (!AUTH_ENABLED && !SYNC_ENABLED) return;
6491
6779
  let active = true;
6492
6780
  ${auth ? `const stop = absoluteExpoAuth.onPrincipalChange(principal => {
6493
6781
  if (!active) return;
6494
6782
  const source = JSON.stringify({ event: 'auth.principal', format: BRIDGE_FORMAT, kind: 'event', path: activeWebPath.current, payload: { principal: principal ? { namespace: principal.namespace } : null } });
6495
6783
  webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
6496
6784
  });
6497
- void startAbsoluteExpoAuth().then(() => { if (active) setAuthReady(true); });
6498
- return () => { active = false; stop(); };` : ""}
6785
+ void ${sync ? "startAbsoluteExpoSync" : "startAbsoluteExpoAuth"}().then(async () => {
6786
+ ${sync ? `syncBridge.current = await createAbsoluteExpoSyncBridge((event, payload) => {
6787
+ const source = JSON.stringify({ event, format: BRIDGE_FORMAT, kind: 'event', path: activeWebPath.current, payload });
6788
+ webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
6789
+ });` : ""}
6790
+ if (active) setRuntimeReady(true);
6791
+ });
6792
+ return () => {
6793
+ active = false;
6794
+ stop();
6795
+ const bridge = syncBridge.current;
6796
+ syncBridge.current = undefined;
6797
+ void bridge?.close();
6798
+ };` : ""}
6499
6799
  }, []);
6500
6800
 
6501
6801
  useEffect(() => {
@@ -6521,6 +6821,23 @@ export function AbsoluteWebHost() {
6521
6821
  if (new TextEncoder().encode(source).byteLength > MAX_MESSAGE_BYTES) throw new Error('Expo bridge response exceeds 64 KiB.');
6522
6822
  webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
6523
6823
  };
6824
+ useEffect(() => {
6825
+ let active = true;
6826
+ void createExpoDevicesBridgeHost(absoluteExpoDevices, (event, payload) => {
6827
+ if (!active) return;
6828
+ respond({ event, format: BRIDGE_FORMAT, kind: 'event', path: activeWebPath.current, payload });
6829
+ }).then(host => {
6830
+ if (!active) return void host.close();
6831
+ devicesBridge.current = host;
6832
+ setDevicesReady(true);
6833
+ });
6834
+ return () => {
6835
+ active = false;
6836
+ const host = devicesBridge.current;
6837
+ devicesBridge.current = undefined;
6838
+ void host?.close();
6839
+ };
6840
+ }, []);
6524
6841
  const hasOrigin = (source: string, origin: string) => {
6525
6842
  try { return new URL(source).origin === origin; } catch { return false; }
6526
6843
  };
@@ -6540,11 +6857,9 @@ export function AbsoluteWebHost() {
6540
6857
  if (message.kind !== 'request' || typeof message.id !== 'string' || message.path !== activeWebPath.current) return;
6541
6858
  try {
6542
6859
  if (typeof message.params !== 'object' || message.params === null || Array.isArray(message.params)) throw new Error('Expo bridge method params are invalid.');
6543
- if (message.method === 'devices.haptics.impact') {
6544
- const style = (message.params as Record<string, unknown>).style;
6545
- if (typeof style !== 'string' || !['error', 'heavy', 'light', 'medium', 'selection', 'success', 'vibrate', 'warning'].includes(style)) throw new Error('Expo bridge haptics style is invalid.');
6546
- await impact(message.params as Record<string, unknown>);
6547
- respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: null });
6860
+ if (typeof message.method === 'string' && message.method.startsWith('devices.')) {
6861
+ if (!devicesBridge.current) throw new Error('Expo devices bridge is unavailable.');
6862
+ respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await devicesBridge.current.request(message.method, message.params as Record<string, unknown>) });
6548
6863
  } else if (message.method === 'http.fetch') {
6549
6864
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await bridgeFetch(message.params as Record<string, unknown>) });
6550
6865
  } else if (message.method === 'auth.signIn') {
@@ -6553,10 +6868,14 @@ export function AbsoluteWebHost() {
6553
6868
  await absoluteExpoAuth.signIn({ authorizationParameters: { login_hint: params.email, ...(params.signup ? { screen_hint: 'signup' } : {}) } });
6554
6869
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });` : "throw new Error('Expo Auth is not configured.');"}
6555
6870
  } else if (message.method === 'auth.signOut') {
6556
- ${auth ? `await absoluteExpoAuth.signOut();
6871
+ ${auth ? `await beforeAbsoluteExpoDeviceSignOut();
6872
+ await absoluteExpoAuth.signOut();
6557
6873
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: null });` : "throw new Error('Expo Auth is not configured.');"}
6558
6874
  } else if (message.method === 'auth.status') {
6559
6875
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });
6876
+ } else if (typeof message.method === 'string' && message.method.startsWith('sync.')) {
6877
+ if (!syncBridge.current) throw new Error('Expo Sync bridge is unavailable.');
6878
+ respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await syncBridge.current.request(message.method, message.params as Record<string, unknown>) });
6560
6879
  } else {
6561
6880
  throw new Error('Expo bridge method is not allowed.');
6562
6881
  }
@@ -6565,7 +6884,7 @@ export function AbsoluteWebHost() {
6565
6884
  }
6566
6885
  };
6567
6886
 
6568
- if (!indexUri || !authReady) return <View style={styles.loading}><ActivityIndicator /></View>;
6887
+ if (!indexUri || !runtimeReady || !devicesReady) return <View style={styles.loading}><ActivityIndicator /></View>;
6569
6888
  return <WebView
6570
6889
  allowFileAccess
6571
6890
  allowFileAccessFromFileURLs
@@ -6598,7 +6917,7 @@ export default AbsoluteWebHost;
6598
6917
  `;
6599
6918
  var nativeWrapperSource = (wrapper, module) => `${EXPO_GENERATED_HEADER}export { default } from ${JSON.stringify(portableRelative2(dirname10(wrapper), module).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
6600
6919
  `;
6601
- var expoTsConfig = (projectRoot, project, auth) => ({
6920
+ var expoTsConfig = (projectRoot, project, auth, sync) => ({
6602
6921
  compilerOptions: {
6603
6922
  paths: {
6604
6923
  ...auth ? {
@@ -6609,6 +6928,20 @@ var expoTsConfig = (projectRoot, project, auth) => ({
6609
6928
  "./node_modules/@absolutejs/auth/dist/*"
6610
6929
  ]
6611
6930
  } : {},
6931
+ ...sync ? {
6932
+ "@absolutejs/sync": [
6933
+ "./node_modules/@absolutejs/sync/dist/index.d.ts"
6934
+ ],
6935
+ "@absolutejs/sync-expo": [
6936
+ "./node_modules/@absolutejs/sync-expo/dist/index.d.ts"
6937
+ ],
6938
+ "@absolutejs/sync-expo/*": [
6939
+ "./node_modules/@absolutejs/sync-expo/dist/*"
6940
+ ],
6941
+ "@absolutejs/sync/*": [
6942
+ "./node_modules/@absolutejs/sync/dist/*"
6943
+ ]
6944
+ } : {},
6612
6945
  "*": [
6613
6946
  "./node_modules/*",
6614
6947
  `${portableRelative2(project, projectRoot)}/node_modules/*`
@@ -6647,6 +6980,9 @@ var writeAbsoluteExpoProject = async (config, options) => {
6647
6980
  const projectRoot = resolve12(options.projectRoot);
6648
6981
  const project = config.nativeProjectDirectory;
6649
6982
  const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
6983
+ const devices = resolveAbsoluteDeviceCapabilityPlan(projectRoot, "expo");
6984
+ const syncEnabled = Boolean(auth && projectUsesAbsoluteSync(projectRoot));
6985
+ const syncSchema = syncEnabled ? { components: discoverAbsoluteSyncSchema(projectRoot).components } : undefined;
6650
6986
  const routeModules = Object.entries(config.expoNativeRoutes);
6651
6987
  const moduleChecks = await Promise.all(routeModules.map(async ([route, module]) => ({
6652
6988
  exists: await exists3(module),
@@ -6679,10 +7015,13 @@ node_modules/
6679
7015
  ],
6680
7016
  [
6681
7017
  join14(project, "app.json"),
6682
- jsonSource(expoAppConfig(config, authEnabled))
7018
+ jsonSource(expoAppConfig(config, authEnabled, syncEnabled, devices))
6683
7019
  ],
6684
7020
  [join14(project, "app.config.js"), expoDynamicAppConfig],
6685
- [join14(project, "package.json"), jsonSource(expoPackage(authEnabled))],
7021
+ [
7022
+ join14(project, "package.json"),
7023
+ jsonSource(expoPackage(authEnabled, syncEnabled, devices))
7024
+ ],
6686
7025
  [join14(project, "metro.config.js"), metroConfig(projectRoot)],
6687
7026
  [
6688
7027
  join14(project, "plugins", "withAbsoluteDevelopmentCa.js"),
@@ -6690,21 +7029,31 @@ node_modules/
6690
7029
  ],
6691
7030
  [
6692
7031
  join14(project, "tsconfig.json"),
6693
- jsonSource(expoTsConfig(projectRoot, project, authEnabled))
7032
+ jsonSource(expoTsConfig(projectRoot, project, authEnabled, syncEnabled))
7033
+ ],
7034
+ [
7035
+ join14(project, "app", "_layout.tsx"),
7036
+ layoutSource(authEnabled, syncEnabled)
6694
7037
  ],
6695
- [join14(project, "app", "_layout.tsx"), layoutSource(authEnabled)],
6696
7038
  [
6697
7039
  join14(project, "app", "__absolute", "native", "index.tsx"),
6698
7040
  nativeDiagnosticSource
6699
7041
  ],
7042
+ [
7043
+ join14(project, "src", "generated", "AbsoluteDevices.ts"),
7044
+ devicesRuntimeSource(config, devices, authEnabled)
7045
+ ],
6700
7046
  [
6701
7047
  join14(project, "src", "generated", "AbsoluteWebHost.tsx"),
6702
- webHostSource(config, auth)
7048
+ webHostSource(config, auth, syncEnabled)
6703
7049
  ]
6704
7050
  ]);
6705
7051
  if (auth) {
6706
7052
  files.set(join14(project, "src", "generated", "AbsoluteAuth.ts"), authRuntimeSource(auth, config.appId));
6707
7053
  }
7054
+ if (syncSchema) {
7055
+ files.set(join14(project, "src", "generated", "AbsoluteSync.ts"), syncRuntimeSource(config, syncSchema));
7056
+ }
6708
7057
  const webAssetsPath = join14(project, "src", "generated", "webAssets.ts");
6709
7058
  if (!await exists3(webAssetsPath)) {
6710
7059
  files.set(webAssetsPath, emptyWebAssetsSource);
@@ -6904,15 +7253,9 @@ var finalizeAbsoluteMobileCompatibilityBuild = async (options) => {
6904
7253
  });
6905
7254
  const auth = resolveAbsoluteMobileAuthManifest(options.projectRoot, mobile);
6906
7255
  const sync = auth !== undefined && projectUsesAbsoluteSync(options.projectRoot);
6907
- if (mobile.engine === "expo" && sync) {
6908
- throw new TypeError("Expo mobile Sync is not released yet. Auth and authenticated HTTP are available, but @absolutejs/sync requires @absolutejs/sync-expo for shared durable state.");
6909
- }
6910
7256
  const syncSchema = sync ? discoverAbsoluteSyncSchema(options.projectRoot) : undefined;
6911
- const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot);
7257
+ const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot, mobile.engine);
6912
7258
  const usesPush = deviceCapabilities.capabilities.includes("pushNotifications");
6913
- if (mobile.engine === "expo" && deviceCapabilities.capabilities.some((capability) => capability !== "haptics")) {
6914
- throw new TypeError("Experimental Expo builds currently bridge only @absolutejs/devices haptics. Other detected device capabilities require their Expo adapters.");
6915
- }
6916
7259
  if (usesPush && !auth)
6917
7260
  throw new TypeError("Portable push notifications require @absolutejs/auth so provider tokens can be registered without exposing identity controls to page code.");
6918
7261
  if (usesPush && !loaded.app.routes.some((route) => route.path === "/auth/push" || route.path === "/auth/mobile/push"))
@@ -7067,14 +7410,88 @@ var installAbsoluteMobileSyncRemediation = (bridge = {
7067
7410
  init_deviceCapabilities();
7068
7411
 
7069
7412
  // src/mobile/expoBridge.ts
7070
- var ABSOLUTE_EXPO_BRIDGE_FORMAT = 2;
7413
+ var ABSOLUTE_EXPO_BRIDGE_FORMAT = 3;
7071
7414
  var ABSOLUTE_EXPO_BRIDGE_MAX_BYTES = 64 * 1024;
7072
7415
  var ABSOLUTE_EXPO_BRIDGE_METHODS = [
7416
+ "devices.platform.getInfo",
7417
+ "devices.lifecycle.getState",
7418
+ "devices.links.getLaunchUrl",
7419
+ "devices.links.openExternal",
7420
+ "devices.network.getStatus",
7421
+ "devices.storage.clear",
7422
+ "devices.storage.get",
7423
+ "devices.storage.keys",
7424
+ "devices.storage.remove",
7425
+ "devices.storage.set",
7426
+ "devices.back.capability",
7427
+ "devices.clipboard.capability",
7428
+ "devices.clipboard.readText",
7429
+ "devices.clipboard.writeText",
7430
+ "devices.share.capability",
7431
+ "devices.share.share",
7432
+ "devices.haptics.capability",
7073
7433
  "devices.haptics.impact",
7434
+ "devices.haptics.notification",
7435
+ "devices.haptics.selectionChanged",
7436
+ "devices.haptics.vibrate",
7437
+ "devices.keyboard.capability",
7438
+ "devices.keyboard.dismiss",
7439
+ "devices.keyboard.getState",
7440
+ "devices.systemBars.capability",
7441
+ "devices.systemBars.setAppearance",
7442
+ "devices.systemBars.setVisible",
7443
+ "devices.camera.capability",
7444
+ "devices.camera.queryPermission",
7445
+ "devices.camera.requestPermission",
7446
+ "devices.camera.takePhoto",
7447
+ "devices.photos.capability",
7448
+ "devices.photos.pick",
7449
+ "devices.location.capability",
7450
+ "devices.location.current",
7451
+ "devices.location.queryPermission",
7452
+ "devices.location.requestPermission",
7453
+ "devices.location.watch.start",
7454
+ "devices.location.watch.stop",
7455
+ "devices.localNotifications.capability",
7456
+ "devices.localNotifications.queryPermission",
7457
+ "devices.localNotifications.requestPermission",
7458
+ "devices.localNotifications.schedule",
7459
+ "devices.localNotifications.pending",
7460
+ "devices.localNotifications.cancel",
7461
+ "devices.pushNotifications.capability",
7462
+ "devices.pushNotifications.queryPermission",
7463
+ "devices.pushNotifications.requestPermission",
7464
+ "devices.pushNotifications.enable",
7465
+ "devices.pushNotifications.disable",
7466
+ "devices.documents.capability",
7467
+ "devices.documents.pick",
7468
+ "devices.transfer.read",
7469
+ "devices.transfer.close",
7470
+ "devices.upload.begin",
7471
+ "devices.upload.write",
7472
+ "devices.documents.export",
7473
+ "devices.documents.open",
7074
7474
  "http.fetch",
7075
7475
  "auth.signIn",
7076
7476
  "auth.signOut",
7077
- "auth.status"
7477
+ "auth.status",
7478
+ "sync.store.begin",
7479
+ "sync.store.deleteNamespace",
7480
+ "sync.store.end",
7481
+ "sync.store.schema",
7482
+ "sync.tx.deleteCollection",
7483
+ "sync.tx.deleteMutation",
7484
+ "sync.tx.getCollection",
7485
+ "sync.tx.getInstallationId",
7486
+ "sync.tx.getMutation",
7487
+ "sync.tx.listCollections",
7488
+ "sync.tx.listMutations",
7489
+ "sync.tx.putCollection",
7490
+ "sync.tx.putMutation",
7491
+ "sync.tx.setInstallationId",
7492
+ "sync.socket.close",
7493
+ "sync.socket.open",
7494
+ "sync.socket.sendChunk"
7078
7495
  ];
7079
7496
  var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
7080
7497
  var validId = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,80}$/u.test(value);
@@ -7173,7 +7590,7 @@ var parseResponse = (value) => {
7173
7590
  };
7174
7591
  };
7175
7592
  var parseEvent = (value) => {
7176
- if (value.event !== "ready" && value.event !== "navigation" && value.event !== "auth.principal") {
7593
+ if (value.event !== "ready" && value.event !== "navigation" && value.event !== "auth.principal" && value.event !== "sync.socket" && value.event !== "sync.wake") {
7177
7594
  throw new TypeError("Expo bridge event is not allowed.");
7178
7595
  }
7179
7596
  if (typeof value.path !== "string" || !validPath(value.path))
@@ -7568,7 +7985,7 @@ var startAbsoluteExpoDevSession = async (options) => {
7568
7985
  init_config();
7569
7986
 
7570
7987
  // src/mobile/ciWorkflow.ts
7571
- import { existsSync as existsSync3 } from "fs";
7988
+ import { existsSync as existsSync4 } from "fs";
7572
7989
  import { access as access11, mkdir as mkdir12, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
7573
7990
  import { dirname as dirname11, extname as extname4, relative as relative11, resolve as resolve14, sep as sep6 } from "path";
7574
7991
  var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1;
@@ -7607,7 +8024,7 @@ var projectPath = (projectRoot, value, field2, options = {}) => {
7607
8024
  }
7608
8025
  if (/\r|\n/u.test(portable) || portable.startsWith("-"))
7609
8026
  throw new TypeError(`${field2} contains an unsafe path.`);
7610
- if (!options.allowMissing && !existsSync3(path))
8027
+ if (!options.allowMissing && !existsSync4(path))
7611
8028
  throw new TypeError(`${field2} does not exist inside the project.`);
7612
8029
  return portable;
7613
8030
  };
@@ -8892,7 +9309,7 @@ var publishAbsoluteIosRelease = async (options) => {
8892
9309
  return publication;
8893
9310
  };
8894
9311
  // src/mobile/routeMetadataTransform.ts
8895
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
9312
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
8896
9313
  import { dirname as dirname13, extname as extname5, relative as relative13, resolve as resolve16 } from "path";
8897
9314
  import ts2 from "typescript";
8898
9315
  var ROUTE_METHODS = new Set(["get", "head"]);
@@ -8944,7 +9361,7 @@ var PAGE_HANDLERS = new Map([
8944
9361
  ]
8945
9362
  ]);
8946
9363
  var posixPath = (value) => value.replace(/\\/g, "/");
8947
- var findTsconfig = (entry, projectRoot) => ts2.findConfigFile(dirname13(entry), existsSync6, "tsconfig.json") ?? ts2.findConfigFile(projectRoot, existsSync6, "tsconfig.json");
9364
+ var findTsconfig = (entry, projectRoot) => ts2.findConfigFile(dirname13(entry), existsSync7, "tsconfig.json") ?? ts2.findConfigFile(projectRoot, existsSync7, "tsconfig.json");
8948
9365
  var createProgram = (entry, projectRoot) => {
8949
9366
  const configPath2 = findTsconfig(entry, projectRoot);
8950
9367
  if (!configPath2) {
@@ -9634,6 +10051,9 @@ export {
9634
10051
  ABSOLUTE_EXPO_BRIDGE_FORMAT,
9635
10052
  ABSOLUTE_EXPO_BRIDGE_MAX_BYTES,
9636
10053
  ABSOLUTE_EXPO_BRIDGE_METHODS,
10054
+ ABSOLUTE_EXPO_SYNC_CORE_VERSION,
10055
+ ABSOLUTE_EXPO_SYNC_PACKAGE,
10056
+ ABSOLUTE_EXPO_SYNC_VERSION,
9637
10057
  ABSOLUTE_IOS_RELEASE_FORMAT,
9638
10058
  ABSOLUTE_IOS_SIMULATOR_NAME,
9639
10059
  ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT,
@@ -9781,5 +10201,5 @@ export {
9781
10201
  writeAbsoluteMobileGithubWorkflow
9782
10202
  };
9783
10203
 
9784
- //# debugId=609EFE7CD27840A164756E2164756E21
10204
+ //# debugId=99CCB7E44742D46964756E2164756E21
9785
10205
  //# sourceMappingURL=index.js.map