@absolutejs/absolute 0.20.0-beta.45 → 0.20.0-beta.47

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.
@@ -167,7 +167,7 @@ var init_startupBanner = __esm(() => {
167
167
 
168
168
  // src/mobile/config.ts
169
169
  import { resolve as resolve6 } from "path";
170
- var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, HOSTNAME_PATTERN, resolveProjectPath = (projectRoot, value, field2) => {
170
+ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
171
171
  const root = resolve6(projectRoot);
172
172
  const path = resolve6(root, value);
173
173
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -240,11 +240,31 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
240
240
  }
241
241
  return value.match(/.{2}/g)?.join(":") ?? value;
242
242
  }))
243
- ].sort(), normalizeExpoNativeRoutes = (config, projectRoot) => {
243
+ ].sort(), validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
244
+ if (segment === "*" && (index !== count - 1 || count === 1)) {
245
+ throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
246
+ }
247
+ if (segment === "*")
248
+ return;
249
+ if (!segment.startsWith(":") && (segment.includes("*") || segment.includes(":"))) {
250
+ throw new TypeError(`mobile.routes.native route ${path} contains invalid segment ${segment}.`);
251
+ }
252
+ if (!segment.startsWith(":"))
253
+ return;
254
+ const name = segment.slice(1);
255
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(name)) {
256
+ throw new TypeError(`mobile.routes.native route ${path} has invalid parameter ${segment}.`);
257
+ }
258
+ if (parameters.has(name)) {
259
+ throw new TypeError(`mobile.routes.native route ${path} repeats parameter ${segment}.`);
260
+ }
261
+ parameters.add(name);
262
+ }, normalizeExpoNativeRoutes = (config, projectRoot) => {
244
263
  if (config.engine !== "expo")
245
264
  return {};
246
265
  const routes = config.routes?.native ?? {};
247
266
  const normalized = {};
267
+ const ownership = new Map;
248
268
  for (const [route, module] of Object.entries(routes)) {
249
269
  const path = normalizeEntry(route);
250
270
  if (path.includes("?") || path.includes("#") || path !== "/" && path.endsWith("/")) {
@@ -253,9 +273,18 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
253
273
  if (path === "/__absolute/native") {
254
274
  throw new TypeError("mobile.routes.native reserves /__absolute/native for the Expo diagnostic screen.");
255
275
  }
256
- if (path.includes("*") || path.includes(":")) {
257
- throw new TypeError(`mobile.routes.native route ${path} must be static during the Expo experiment; parameters and wildcards are not supported yet.`);
276
+ const segments = path.split("/").filter(Boolean);
277
+ if (segments[0] && EXPO_RESERVED_ROUTE_PREFIXES.has(segments[0])) {
278
+ throw new TypeError(`mobile.routes.native route ${path} conflicts with an Expo Router or Metro reserved path.`);
279
+ }
280
+ const parameters = new Set;
281
+ segments.forEach((segment, index) => validateExpoNativeRouteSegment(path, segment, index, segments.length, parameters));
282
+ const signature = segments.map((segment) => segment.startsWith(":") ? ":" : segment).join("/");
283
+ const existing = ownership.get(signature);
284
+ if (existing) {
285
+ throw new TypeError(`mobile.routes.native routes ${existing} and ${path} claim the same Expo route pattern.`);
258
286
  }
287
+ ownership.set(signature, path);
259
288
  normalized[path] = resolveProjectPath(projectRoot, requireText(module, `mobile.routes.native[${path}]`), `mobile.routes.native[${path}]`);
260
289
  }
261
290
  return Object.fromEntries(Object.entries(normalized).sort(([left], [right]) => left.localeCompare(right)));
@@ -294,6 +323,16 @@ var init_config = __esm(() => {
294
323
  APPLE_APP_ID_PREFIX_PATTERN = /^[A-Z0-9]{10}$/;
295
324
  CERTIFICATE_FINGERPRINT_PATTERN = /^[0-9A-F]{64}$/;
296
325
  HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/;
326
+ EXPO_RESERVED_ROUTE_PREFIXES = new Set([
327
+ "_expo",
328
+ "_flight",
329
+ "_sitemap",
330
+ "assets",
331
+ "expo-dev-plugins",
332
+ "inspector",
333
+ "manifest",
334
+ "public"
335
+ ]);
297
336
  });
298
337
 
299
338
  // src/utils/stringModifiers.ts
@@ -772,10 +811,11 @@ var init_syncSchema = __esm(() => {
772
811
  });
773
812
 
774
813
  // src/mobile/deviceCapabilities.ts
775
- import { readFileSync as readFileSync4 } from "fs";
814
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
776
815
  import { extname as extname3, join as join13, relative as relative9, resolve as resolve11 } from "path";
816
+ import { fileURLToPath } from "url";
777
817
  import ts from "typescript";
778
- 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) => {
818
+ 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) => {
779
819
  const value = JSON.parse(readFileSync4(path, "utf8"));
780
820
  if (!object2(value))
781
821
  throw new TypeError(`${path} must contain an object.`);
@@ -835,7 +875,7 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
835
875
  ...systemBars === true ? { systemBars: true } : {},
836
876
  ...usageDescriptions === undefined ? {} : { usageDescriptions: [...usageDescriptions] }
837
877
  };
838
- }, parseProvider = (name, value) => {
878
+ }, parseProvider = (name, value, providerName) => {
839
879
  if (!IDENTIFIER_PATTERN.test(name))
840
880
  throw new TypeError("Device capability names must be identifiers.");
841
881
  if (!object2(value))
@@ -844,10 +884,13 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
844
884
  const module = text(value.module, `${name}.module`);
845
885
  if (!IDENTIFIER_PATTERN.test(factory))
846
886
  throw new TypeError(`${name}.factory must be a JavaScript identifier.`);
847
- if (!CAPACITOR_MODULE_PATTERN.test(module))
848
- throw new TypeError(`${name}.module must be an official devices-capacitor subpath.`);
849
- if (!Array.isArray(value.packages) || !value.packages.every((spec) => typeof spec === "string" && CAPACITOR_PACKAGE_PATTERN.test(spec)))
850
- throw new TypeError(`${name}.packages must contain exact official Capacitor package versions.`);
887
+ if (!providerModulePattern(providerName).test(module))
888
+ throw new TypeError(`${name}.module must be an official devices-${providerName} subpath.`);
889
+ if (!Array.isArray(value.packages) || !value.packages.every((spec) => typeof spec === "string" && providerPackagePattern(providerName).test(spec)))
890
+ throw new TypeError(`${name}.packages must contain exact official ${providerLabel(providerName)} package versions.`);
891
+ const { plugins } = value;
892
+ if (plugins !== undefined && (!Array.isArray(plugins) || !plugins.every((plugin) => typeof plugin === "string" && /^expo-[a-z][a-z0-9-]*$/u.test(plugin))))
893
+ throw new TypeError(`${name}.plugins must contain Expo config plugin names.`);
851
894
  let native;
852
895
  const { native: nativeMetadata } = value;
853
896
  if (nativeMetadata !== undefined) {
@@ -865,6 +908,7 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
865
908
  factory,
866
909
  module,
867
910
  ...native === undefined ? {} : { native },
911
+ ...plugins === undefined ? {} : { plugins: [...plugins] },
868
912
  packages: [...value.packages]
869
913
  };
870
914
  }, absoluteDeviceNativeRequirements = (plan) => {
@@ -894,18 +938,27 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
894
938
  ...new Set(plan.capabilities.flatMap((name) => plan.providers[name]?.native?.ios?.usageDescriptions ?? []))
895
939
  ].sort()
896
940
  };
897
- }, loadAbsoluteDeviceCapabilityProviders = (projectRoot) => {
898
- const path = join13(resolve11(projectRoot), "node_modules", CAPACITOR_ADAPTER, "package.json");
941
+ }, loadAbsoluteDeviceCapabilityProviders = (projectRoot, provider = "capacitor") => {
942
+ const adapter = ADAPTERS[provider];
943
+ let path = join13(resolve11(projectRoot), "node_modules", adapter, "package.json");
944
+ try {
945
+ readFileSync4(path, "utf8");
946
+ } catch {
947
+ path = fileURLToPath(import.meta.resolve(`${adapter}/package.json`));
948
+ }
899
949
  const manifest = readJson(path);
900
950
  const { absolutejs } = manifest;
901
951
  const devices = object2(absolutejs) ? absolutejs.devices : undefined;
902
- if (!object2(devices) || devices.format !== 1 || devices.provider !== "capacitor" || !object2(devices.capabilities))
903
- throw new TypeError(`${CAPACITOR_ADAPTER} does not publish supported capability metadata.`);
904
- const entries = Object.entries(devices.capabilities).map(([name, provider]) => ({
952
+ if (!object2(devices) || devices.format !== 1 || devices.provider !== provider || !object2(devices.capabilities))
953
+ throw new TypeError(`${adapter} does not publish supported capability metadata.`);
954
+ const entries = Object.entries(devices.capabilities).map(([name, capability]) => ({
905
955
  name,
906
- provider: parseProvider(name, provider)
956
+ provider: parseProvider(name, capability, provider)
907
957
  }));
908
- return Object.fromEntries(entries.sort((left, right) => left.name.localeCompare(right.name)).map(({ name, provider }) => [name, provider]));
958
+ return Object.fromEntries(entries.sort((left, right) => left.name.localeCompare(right.name)).map(({ name, provider: capabilityProvider }) => [
959
+ name,
960
+ capabilityProvider
961
+ ]));
909
962
  }, isIgnored = (path) => path.split("/").some((segment) => IGNORED_DIRECTORIES.has(segment)), importedCapabilities = (source, file) => {
910
963
  const names = new Set;
911
964
  const namespaces = new Set;
@@ -962,6 +1015,8 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
962
1015
  return packages;
963
1016
  }, discoverAbsoluteDeviceCapabilities = (projectRoot, providers = loadAbsoluteDeviceCapabilityProviders(projectRoot)) => {
964
1017
  const root = resolve11(projectRoot);
1018
+ if (!existsSync3(root))
1019
+ return [];
965
1020
  const known = new Set(Object.keys(providers));
966
1021
  const capabilities = new Set;
967
1022
  for (const path of SOURCE_GLOB.scanSync({ cwd: root })) {
@@ -988,14 +1043,14 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
988
1043
  return true;
989
1044
  }
990
1045
  return false;
991
- }, resolveAbsoluteDeviceCapabilityPlan = (projectRoot) => {
992
- const allProviders = loadAbsoluteDeviceCapabilityProviders(projectRoot);
1046
+ }, resolveAbsoluteDeviceCapabilityPlan = (projectRoot, provider = "capacitor") => {
1047
+ const allProviders = loadAbsoluteDeviceCapabilityProviders(projectRoot, provider);
993
1048
  const capabilities = discoverAbsoluteDeviceCapabilities(projectRoot, allProviders);
994
1049
  const providers = {};
995
1050
  for (const name of capabilities) {
996
- const provider = allProviders[name];
997
- if (provider)
998
- providers[name] = provider;
1051
+ const capabilityProvider = allProviders[name];
1052
+ if (capabilityProvider)
1053
+ providers[name] = capabilityProvider;
999
1054
  }
1000
1055
  return {
1001
1056
  capabilities,
@@ -1006,6 +1061,10 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
1006
1061
  };
1007
1062
  };
1008
1063
  var init_deviceCapabilities = __esm(() => {
1064
+ ADAPTERS = {
1065
+ capacitor: "@absolutejs/devices-capacitor",
1066
+ expo: "@absolutejs/devices-expo"
1067
+ };
1009
1068
  SOURCE_GLOB = new Bun.Glob("**/*.{js,jsx,ts,tsx,svelte,vue}");
1010
1069
  IGNORED_DIRECTORIES = new Set([
1011
1070
  ".absolutejs",
@@ -1019,8 +1078,6 @@ var init_deviceCapabilities = __esm(() => {
1019
1078
  "tests"
1020
1079
  ]);
1021
1080
  IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/u;
1022
- CAPACITOR_MODULE_PATTERN = /^@absolutejs\/devices-capacitor\/[a-z][a-z0-9-]*$/u;
1023
- CAPACITOR_PACKAGE_PATTERN = /^@capacitor\/[a-z][a-z0-9-]*@\d+\.\d+\.\d+$/u;
1024
1081
  ANDROID_PERMISSION_PATTERN = /^android\.permission\.[A-Z][A-Z0-9_]*$/u;
1025
1082
  IOS_USAGE_DESCRIPTIONS = new Set([
1026
1083
  "camera",
@@ -1038,12 +1095,12 @@ var init_deviceCapabilities = __esm(() => {
1038
1095
  });
1039
1096
 
1040
1097
  // src/cli/scripts/telemetry.ts
1041
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
1098
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
1042
1099
  import { homedir as homedir3 } from "os";
1043
1100
  import { join as join17 } from "path";
1044
1101
  var configDir, configPath, getTelemetryConfig = () => {
1045
1102
  try {
1046
- if (!existsSync4(configPath))
1103
+ if (!existsSync5(configPath))
1047
1104
  return null;
1048
1105
  const raw = readFileSync5(configPath, "utf-8");
1049
1106
  const config = JSON.parse(raw);
@@ -1058,11 +1115,11 @@ var init_telemetry = __esm(() => {
1058
1115
  });
1059
1116
 
1060
1117
  // src/cli/telemetryEvent.ts
1061
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
1118
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1062
1119
  import { arch, platform } from "os";
1063
1120
  import { dirname as dirname12, join as join18, parse } from "path";
1064
1121
  var checkCandidate = (candidate) => {
1065
- if (!existsSync5(candidate)) {
1122
+ if (!existsSync6(candidate)) {
1066
1123
  return null;
1067
1124
  }
1068
1125
  const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
@@ -5723,7 +5780,7 @@ const absoluteMobilePushCapability = absoluteDeviceCapability${pushIndex}(absolu
5723
5780
  const entryPath = join9(staging, ".absolute-mobile-entry.ts");
5724
5781
  const baseAdapterModule = capacitor ? await resolveProjectImport(projectRoot, "@absolutejs/devices-capacitor") : shellExpoDevicesModule();
5725
5782
  const adapterImport = capacitor ? `import { installCapacitorDeviceAdapterIfNative } from ${JSON.stringify(baseAdapterModule)};` : `import { createAbsoluteExpoBridgeFetch, installAbsoluteExpoWebDeviceAdapter } from ${JSON.stringify(baseAdapterModule)};`;
5726
- const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : "installAbsoluteExpoWebDeviceAdapter();";
5783
+ const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : `installAbsoluteExpoWebDeviceAdapter(${JSON.stringify(deviceCapabilities.capabilities)});`;
5727
5784
  let shellOptions = auth ? options : "{ createFetch: createAbsoluteExpoBridgeFetch }";
5728
5785
  if (capacitor)
5729
5786
  shellOptions = options;
@@ -6096,6 +6153,7 @@ init_deviceCapabilities();
6096
6153
  // src/mobile/expoProject.ts
6097
6154
  init_nativeAuth();
6098
6155
  init_syncSchema();
6156
+ init_deviceCapabilities();
6099
6157
  import {
6100
6158
  access as access9,
6101
6159
  cp as cp2,
@@ -6108,7 +6166,7 @@ import {
6108
6166
  writeFile as writeFile12
6109
6167
  } from "fs/promises";
6110
6168
  import { createHash as createHash10 } from "crypto";
6111
- import { basename as basename4, dirname as dirname10, join as join14, relative as relative10, resolve as resolve12 } from "path";
6169
+ import { basename as basename4, dirname as dirname10, join as join14, relative as relative10, resolve as resolve12, sep as sep6 } from "path";
6112
6170
  var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
6113
6171
  `;
6114
6172
  var EXPO_ASSET_EXTENSION = ".absasset";
@@ -6125,17 +6183,28 @@ var portableRelative2 = (from, destination) => {
6125
6183
  const value = relative10(from, destination).replaceAll("\\", "/");
6126
6184
  return value.startsWith(".") ? value : `./${value}`;
6127
6185
  };
6128
- var routeSegments = (route) => route.split("/").filter(Boolean).map((segment) => {
6129
- if (segment.startsWith(":"))
6130
- return `[${segment.slice(1)}]`;
6131
- if (segment === "." || segment === ".." || !/^[A-Za-z0-9._~-]+$/u.test(segment)) {
6132
- throw new TypeError(`Expo native route ${route} contains an unsupported segment ${segment}.`);
6133
- }
6134
- return segment;
6135
- });
6186
+ var routeSegments = (route) => {
6187
+ const segments = route.split("/").filter(Boolean);
6188
+ return segments.map((segment, index) => {
6189
+ if (segment.startsWith(":"))
6190
+ return `[${segment.slice(1)}]`;
6191
+ if (segment === "*" && index === segments.length - 1)
6192
+ return "[...absoluteWildcard]";
6193
+ if (segment === "." || segment === ".." || !/^[A-Za-z0-9._~-]+$/u.test(segment)) {
6194
+ throw new TypeError(`Expo native route ${route} contains an unsupported segment ${segment}.`);
6195
+ }
6196
+ return segment;
6197
+ });
6198
+ };
6136
6199
  var routeFile = (project, route) => join14(project, "app", ...routeSegments(route), "index.tsx");
6137
- var expoPackage = (auth, sync) => ({
6200
+ var packageDependencies = (plan) => Object.fromEntries(plan.requiredPackages.map((spec) => {
6201
+ const separator = spec.lastIndexOf("@");
6202
+ return [spec.slice(0, separator), spec.slice(separator + 1)];
6203
+ }));
6204
+ var expoPackage = (auth, sync, devices) => ({
6138
6205
  dependencies: {
6206
+ "@absolutejs/devices": "0.7.0",
6207
+ "@absolutejs/devices-expo": "0.0.2",
6139
6208
  ...auth ? {
6140
6209
  "@absolutejs/auth": ABSOLUTE_EXPO_AUTH_CORE_VERSION,
6141
6210
  [ABSOLUTE_EXPO_AUTH_PACKAGE]: ABSOLUTE_EXPO_AUTH_VERSION
@@ -6167,7 +6236,8 @@ var expoPackage = (auth, sync) => ({
6167
6236
  "react-native": "0.86.3",
6168
6237
  "react-native-safe-area-context": "~5.7.0",
6169
6238
  "react-native-screens": "4.26.0",
6170
- "react-native-webview": "13.16.1"
6239
+ "react-native-webview": "13.16.1",
6240
+ ...packageDependencies(devices)
6171
6241
  },
6172
6242
  devDependencies: {
6173
6243
  "@types/react": "~19.2.2",
@@ -6183,36 +6253,107 @@ var expoPackage = (auth, sync) => ({
6183
6253
  },
6184
6254
  version: "0.0.0"
6185
6255
  });
6186
- var expoAppConfig = (config, auth, sync) => ({
6187
- expo: {
6188
- android: {
6189
- intentFilters: config.deepLinkHosts.map((host2) => ({
6190
- action: "VIEW",
6191
- autoVerify: true,
6192
- category: ["BROWSABLE", "DEFAULT"],
6193
- data: [{ host: host2, pathPrefix: "/", scheme: "https" }]
6194
- })),
6195
- package: config.appId
6196
- },
6197
- experiments: { typedRoutes: true },
6198
- ios: {
6199
- associatedDomains: config.deepLinkHosts.map((host2) => `applinks:${host2}`),
6200
- bundleIdentifier: config.appId,
6201
- ...config.iosVersion ? { buildNumber: config.iosVersion } : {}
6202
- },
6203
- name: config.appName,
6204
- plugins: [
6205
- "expo-router",
6206
- ["expo-dev-client", { launchMode: "most-recent" }],
6207
- ...auth ? ["expo-secure-store"] : [],
6208
- ...sync ? ["expo-sqlite", "expo-background-task", "expo-task-manager"] : []
6209
- ],
6210
- runtimeVersion: { policy: "appVersion" },
6211
- scheme: config.deepLinkScheme,
6212
- slug: config.appId.toLowerCase().replaceAll(".", "-"),
6213
- version: config.iosVersion ?? "0.1.0"
6214
- }
6215
- });
6256
+ var expoAppConfig = (config, auth, sync, devices) => {
6257
+ const requirements = absoluteDeviceNativeRequirements(devices);
6258
+ const usageKey = (purpose) => {
6259
+ if (purpose === "camera")
6260
+ return "NSCameraUsageDescription";
6261
+ if (purpose === "photo-library")
6262
+ return "NSPhotoLibraryUsageDescription";
6263
+ if (purpose === "photo-library-add")
6264
+ return "NSPhotoLibraryAddUsageDescription";
6265
+ if (purpose === "location-always")
6266
+ return "NSLocationAlwaysAndWhenInUseUsageDescription";
6267
+ return "NSLocationWhenInUseUsageDescription";
6268
+ };
6269
+ const usageDescription = (purpose) => {
6270
+ if (purpose === "camera")
6271
+ return `${config.appName} uses your camera when you choose to take a photo.`;
6272
+ if (purpose === "photo-library")
6273
+ return `${config.appName} accesses your photo library only for photo actions you choose.`;
6274
+ if (purpose.startsWith("location-"))
6275
+ return `${config.appName} uses your location only while you use the app and request a location-based action.`;
6276
+ return `${config.appName} adds to your photo library only for photo actions you choose.`;
6277
+ };
6278
+ const descriptions = Object.fromEntries(requirements.iosUsageDescriptions.map((purpose) => [
6279
+ usageKey(purpose),
6280
+ usageDescription(purpose)
6281
+ ]));
6282
+ const devicePlugins = [
6283
+ ...new Set(devices.capabilities.flatMap((name) => devices.providers[name]?.plugins ?? []))
6284
+ ];
6285
+ const configuredPlugins = devicePlugins.map((plugin) => {
6286
+ if (plugin === "expo-image-picker")
6287
+ return [
6288
+ plugin,
6289
+ {
6290
+ cameraPermission: descriptions.NSCameraUsageDescription,
6291
+ microphonePermission: false,
6292
+ photosPermission: descriptions.NSPhotoLibraryUsageDescription
6293
+ }
6294
+ ];
6295
+ if (plugin === "expo-location")
6296
+ return [
6297
+ plugin,
6298
+ {
6299
+ locationWhenInUsePermission: descriptions.NSLocationWhenInUseUsageDescription
6300
+ }
6301
+ ];
6302
+ return plugin;
6303
+ });
6304
+ return {
6305
+ expo: {
6306
+ android: {
6307
+ ...devicePlugins.includes("expo-image-picker") ? {
6308
+ blockedPermissions: [
6309
+ "android.permission.READ_EXTERNAL_STORAGE",
6310
+ "android.permission.RECORD_AUDIO",
6311
+ "android.permission.WRITE_EXTERNAL_STORAGE"
6312
+ ]
6313
+ } : {},
6314
+ intentFilters: config.deepLinkHosts.map((host2) => ({
6315
+ action: "VIEW",
6316
+ autoVerify: true,
6317
+ category: ["BROWSABLE", "DEFAULT"],
6318
+ data: [{ host: host2, pathPrefix: "/", scheme: "https" }]
6319
+ })),
6320
+ package: config.appId,
6321
+ permissions: requirements.androidPermissions
6322
+ },
6323
+ experiments: { typedRoutes: true },
6324
+ ios: {
6325
+ associatedDomains: config.deepLinkHosts.map((host2) => `applinks:${host2}`),
6326
+ bundleIdentifier: config.appId,
6327
+ infoPlist: descriptions,
6328
+ ...requirements.iosPrivacyAccessedApis.length > 0 ? {
6329
+ privacyManifests: {
6330
+ NSPrivacyAccessedAPITypes: requirements.iosPrivacyAccessedApis.map(({ api, reasons }) => ({
6331
+ NSPrivacyAccessedAPIType: api,
6332
+ NSPrivacyAccessedAPITypeReasons: reasons
6333
+ }))
6334
+ }
6335
+ } : {},
6336
+ ...config.iosVersion ? { buildNumber: config.iosVersion } : {}
6337
+ },
6338
+ name: config.appName,
6339
+ plugins: [
6340
+ "expo-router",
6341
+ ["expo-dev-client", { launchMode: "most-recent" }],
6342
+ ...auth ? ["expo-secure-store"] : [],
6343
+ ...sync ? [
6344
+ "expo-sqlite",
6345
+ "expo-background-task",
6346
+ "expo-task-manager"
6347
+ ] : [],
6348
+ ...configuredPlugins
6349
+ ],
6350
+ runtimeVersion: { policy: "appVersion" },
6351
+ scheme: config.deepLinkScheme,
6352
+ slug: config.appId.toLowerCase().replaceAll(".", "-"),
6353
+ version: config.iosVersion ?? "0.1.0"
6354
+ }
6355
+ };
6356
+ };
6216
6357
  var expoDynamicAppConfig = `${EXPO_GENERATED_HEADER}const config = require('./app.json');
6217
6358
 
6218
6359
  if (process.env.ABSOLUTE_EXPO_DEVELOPMENT === '1' && process.env.ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH) {
@@ -6280,6 +6421,7 @@ config.watchFolders = [appRoot];
6280
6421
  module.exports = config;
6281
6422
  `;
6282
6423
  var layoutSource = (auth, sync) => `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
6424
+ import '../src/generated/AbsoluteDevices';
6283
6425
  ${auth ? `import { useEffect, useState } from 'react';
6284
6426
  import { startAbsoluteExpoAuth } from '../src/generated/AbsoluteAuth';` : ""}
6285
6427
  ${sync ? "import { startAbsoluteExpoSync } from '../src/generated/AbsoluteSync';" : ""}
@@ -6326,6 +6468,76 @@ const styles = StyleSheet.create({
6326
6468
  title: { color: '#f8fafc', fontSize: 32, fontWeight: '800' }
6327
6469
  });
6328
6470
  `;
6471
+ var devicesRuntimeSource = (config, plan, auth) => {
6472
+ const imports = plan.capabilities.map((name, index) => {
6473
+ const provider = plan.providers[name];
6474
+ if (!provider)
6475
+ throw new TypeError(`Missing Expo device capability provider ${name}.`);
6476
+ return `import { ${provider.factory} as absoluteExpoCapability${index} } from ${JSON.stringify(provider.module)};`;
6477
+ });
6478
+ const pushIndex = plan.capabilities.indexOf("pushNotifications");
6479
+ const push = pushIndex !== -1;
6480
+ if (push && !auth)
6481
+ throw new TypeError("Expo push notifications require the provisioned AbsoluteJS Auth runtime.");
6482
+ const entries = plan.capabilities.map((name, index) => `${JSON.stringify(name)}: absoluteExpoCapability${index}(${name === "pushNotifications" ? "absoluteExpoPushOptions" : ""})`).join(`,
6483
+ `);
6484
+ const pushSource = push ? `const INSTALLATION_KEY = 'absolutejs.push.installation-id';
6485
+ const requirePushResponse = async (response: Response, operation: string) => {
6486
+ if (!response.ok) throw new Error(\`AbsoluteJS native push \${operation} failed with HTTP \${response.status}.\`);
6487
+ return response.json() as Promise<Record<string, unknown>>;
6488
+ };
6489
+ const absoluteExpoPushOptions = {
6490
+ onRegistration: async (registration: { platform: 'apns' | 'fcm'; token: string }) => {
6491
+ const known = await absoluteExpoDevices.storage.get(INSTALLATION_KEY);
6492
+ const register = (installationId?: string | null) => absoluteExpoAuth.fetch('/auth/push', {
6493
+ body: JSON.stringify({ ...(installationId ? { installationId } : {}), platform: registration.platform, token: registration.token }),
6494
+ headers: { 'content-type': 'application/json' },
6495
+ method: 'POST'
6496
+ });
6497
+ let response = await register(known);
6498
+ const conflict = response.status === 409 && await response.clone().json().then(value => typeof value === 'object' && value !== null && Reflect.get(value, 'code') === 'installation-ownership').catch(() => false);
6499
+ if (known && conflict) {
6500
+ await absoluteExpoDevices.storage.remove(INSTALLATION_KEY);
6501
+ response = await register();
6502
+ }
6503
+ const result = await requirePushResponse(response, 'registration');
6504
+ if (typeof result.installationId !== 'string' || !result.installationId || result.installationId.length > 128) throw new Error('AbsoluteJS native push returned an invalid installation identity.');
6505
+ await absoluteExpoDevices.storage.set(INSTALLATION_KEY, result.installationId);
6506
+ },
6507
+ onUnregistration: async () => {
6508
+ const installationId = await absoluteExpoDevices.storage.get(INSTALLATION_KEY);
6509
+ if (!installationId) return;
6510
+ await requirePushResponse(await absoluteExpoAuth.fetch('/auth/push', {
6511
+ body: JSON.stringify({ installationId }),
6512
+ headers: { 'content-type': 'application/json' },
6513
+ method: 'DELETE'
6514
+ }), 'removal');
6515
+ await absoluteExpoDevices.storage.remove(INSTALLATION_KEY);
6516
+ }
6517
+ };` : "";
6518
+ return `${EXPO_GENERATED_HEADER}import { installDeviceAdapter } from '@absolutejs/devices/runtime';
6519
+ import { createExpoDeviceAdapter } from '@absolutejs/devices-expo';
6520
+ ${push ? "import { absoluteExpoAuth } from './AbsoluteAuth';" : ""}
6521
+ ${imports.join(`
6522
+ `)}
6523
+
6524
+ ${pushSource}
6525
+
6526
+ export const absoluteExpoDeviceCapabilities = ${JSON.stringify(plan.capabilities)} as const;
6527
+ export const absoluteExpoDevices = createExpoDeviceAdapter({
6528
+ storagePrefix: ${JSON.stringify(`absolutejs.${config.appId}.`)},
6529
+ ${entries}
6530
+ });
6531
+ installDeviceAdapter(absoluteExpoDevices);
6532
+ export const beforeAbsoluteExpoDeviceSignOut = async () => {
6533
+ ${push ? "await absoluteExpoDevices.pushNotifications?.disable();" : ""}
6534
+ };
6535
+ ${push ? `absoluteExpoAuth.onPrincipalChange(principal => {
6536
+ if (!principal) return;
6537
+ void absoluteExpoDevices.pushNotifications?.queryPermission().then(permission => permission.state === 'granted' ? absoluteExpoDevices.pushNotifications?.enable() : undefined).catch(() => undefined);
6538
+ });` : ""}
6539
+ `;
6540
+ };
6329
6541
  var authRuntimeSource = (auth, appId) => {
6330
6542
  const storageIdentity = createHash10("sha256").update(appId).digest("hex").slice(0, 24);
6331
6543
  return `${EXPO_GENERATED_HEADER}import { createAbsoluteExpoAuthClient } from '@absolutejs/auth-expo';
@@ -6461,20 +6673,22 @@ var webHostSource = (config, auth, sync) => {
6461
6673
  "/__absolute/native",
6462
6674
  ...Object.keys(config.expoNativeRoutes)
6463
6675
  ];
6464
- return `${EXPO_GENERATED_HEADER}import * as Haptics from 'expo-haptics';
6465
- import * as Linking from 'expo-linking';
6676
+ const nativeRoutePatterns = nativeRoutes.map((route) => route.split("/").filter(Boolean));
6677
+ return `${EXPO_GENERATED_HEADER}import * as Linking from 'expo-linking';
6466
6678
  import { router, usePathname } from 'expo-router';
6467
6679
  import { useEffect, useRef, useState } from 'react';
6468
6680
  import { ActivityIndicator, BackHandler, Platform, StyleSheet, View } from 'react-native';
6469
6681
  import { WebView, type WebViewMessageEvent } from 'react-native-webview';
6470
6682
  import { materializeAbsoluteWebBundle } from './webAssets';
6683
+ import { createExpoDevicesBridgeHost } from '@absolutejs/devices-expo/bridge';
6684
+ import { absoluteExpoDevices, beforeAbsoluteExpoDeviceSignOut } from './AbsoluteDevices';
6471
6685
  ${auth ? "import { absoluteExpoAuth, getAbsoluteExpoAuthPrincipal, startAbsoluteExpoAuth } from './AbsoluteAuth';" : ""}
6472
6686
  ${sync ? "import { createAbsoluteExpoSyncBridge, startAbsoluteExpoSync } from './AbsoluteSync';" : ""}
6473
6687
 
6474
6688
  const BRIDGE_FORMAT = 3;
6475
6689
  const MAX_MESSAGE_BYTES = 64 * 1024;
6476
6690
  const MAX_HTTP_BODY_BYTES = 48 * 1024;
6477
- const NATIVE_ROUTES = new Set(${JSON.stringify(nativeRoutes)});
6691
+ const NATIVE_ROUTE_PATTERNS = ${JSON.stringify(nativeRoutePatterns)};
6478
6692
  const PRODUCTION_ORIGIN = ${JSON.stringify(config.productionOrigin)};
6479
6693
  const DEV_ORIGIN = Platform.OS === 'android'
6480
6694
  ? process.env.EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN
@@ -6483,6 +6697,19 @@ const HMR_TARGET = Platform.OS === 'android' ? 'expo-android' : 'expo-ios';
6483
6697
  const AUTH_ENABLED = ${auth ? "true" : "false"};
6484
6698
  const SYNC_ENABLED = ${sync ? "true" : "false"};
6485
6699
 
6700
+ const isNativeRoute = (pathname: string) => {
6701
+ const segments = pathname.split('/').filter(Boolean);
6702
+ return NATIVE_ROUTE_PATTERNS.some(pattern => {
6703
+ for (let index = 0; index < pattern.length; index += 1) {
6704
+ const expected = pattern[index]!;
6705
+ if (expected === '*') return segments.length > index;
6706
+ if (segments[index] === undefined) return false;
6707
+ if (!expected.startsWith(':') && expected !== segments[index]) return false;
6708
+ }
6709
+ return segments.length === pattern.length;
6710
+ });
6711
+ };
6712
+
6486
6713
  const bridgeBootstrap = (path: string) => {
6487
6714
  const initialPath = DEV_ORIGIN
6488
6715
  ? 'location.pathname + location.search + location.hash'
@@ -6523,10 +6750,11 @@ const bridgeBootstrap = (path: string) => {
6523
6750
  const id = 'web_' + Date.now().toString(36) + '_' + (++sequence).toString(36);
6524
6751
  send({ format: 3, id, kind: 'request', method, params, path: currentPath });
6525
6752
  return new Promise((resolve, reject) => {
6753
+ const interactive = method.startsWith('devices.camera.') || method.startsWith('devices.photos.') || method.startsWith('devices.documents.') || method.endsWith('.requestPermission');
6526
6754
  const timer = setTimeout(() => {
6527
6755
  pending.delete(id);
6528
6756
  reject(new Error('Expo bridge request timed out.'));
6529
- }, 10000);
6757
+ }, interactive ? 5 * 60 * 1000 : 30 * 1000);
6530
6758
  pending.set(id, { reject, resolve, timer });
6531
6759
  });
6532
6760
  },
@@ -6557,7 +6785,7 @@ const bridgeBootstrap = (path: string) => {
6557
6785
  const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null;
6558
6786
  if (!anchor) return;
6559
6787
  const url = new URL(anchor.href, location.href);
6560
- if (!${JSON.stringify(nativeRoutes)}.includes(url.pathname)) return;
6788
+ if (!isNativeRoute(url.pathname)) return;
6561
6789
  event.preventDefault();
6562
6790
  send({ format: 3, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
6563
6791
  }, true);
@@ -6565,17 +6793,6 @@ const bridgeBootstrap = (path: string) => {
6565
6793
  })(); true;\`;
6566
6794
  };
6567
6795
 
6568
- const impact = async (params: Record<string, unknown>) => {
6569
- const style = params.style;
6570
- if (style === 'selection') return Haptics.selectionAsync();
6571
- if (style === 'success' || style === 'warning' || style === 'error') {
6572
- const value = style === 'success' ? Haptics.NotificationFeedbackType.Success : style === 'warning' ? Haptics.NotificationFeedbackType.Warning : Haptics.NotificationFeedbackType.Error;
6573
- return Haptics.notificationAsync(value);
6574
- }
6575
- const value = style === 'light' ? Haptics.ImpactFeedbackStyle.Light : style === 'heavy' ? Haptics.ImpactFeedbackStyle.Heavy : Haptics.ImpactFeedbackStyle.Medium;
6576
- return Haptics.impactAsync(value);
6577
- };
6578
-
6579
6796
  const bridgeFetch = async (params: Record<string, unknown>) => {
6580
6797
  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.');
6581
6798
  const url = new URL(params.url);
@@ -6607,10 +6824,12 @@ const authStatus = async () => {
6607
6824
  export function AbsoluteWebHost() {
6608
6825
  const pathname = usePathname() || '/';
6609
6826
  const webView = useRef<WebView>(null);
6827
+ const devicesBridge = useRef<{ close(): void | Promise<void>; request(method: string, params: Record<string, unknown>): Promise<unknown> } | undefined>(undefined);
6610
6828
  const syncBridge = useRef<{ close(): void | Promise<void>; request(method: string, params: Record<string, unknown>): Promise<unknown> } | undefined>(undefined);
6611
6829
  const [indexUri, setIndexUri] = useState<string>();
6612
6830
  const [canGoBack, setCanGoBack] = useState(false);
6613
6831
  const [runtimeReady, setRuntimeReady] = useState(!AUTH_ENABLED && !SYNC_ENABLED);
6832
+ const [devicesReady, setDevicesReady] = useState(false);
6614
6833
  const activeWebPath = useRef(pathname);
6615
6834
 
6616
6835
  useEffect(() => {
@@ -6660,6 +6879,23 @@ export function AbsoluteWebHost() {
6660
6879
  if (new TextEncoder().encode(source).byteLength > MAX_MESSAGE_BYTES) throw new Error('Expo bridge response exceeds 64 KiB.');
6661
6880
  webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
6662
6881
  };
6882
+ useEffect(() => {
6883
+ let active = true;
6884
+ void createExpoDevicesBridgeHost(absoluteExpoDevices, (event, payload) => {
6885
+ if (!active) return;
6886
+ respond({ event, format: BRIDGE_FORMAT, kind: 'event', path: activeWebPath.current, payload });
6887
+ }).then(host => {
6888
+ if (!active) return void host.close();
6889
+ devicesBridge.current = host;
6890
+ setDevicesReady(true);
6891
+ });
6892
+ return () => {
6893
+ active = false;
6894
+ const host = devicesBridge.current;
6895
+ devicesBridge.current = undefined;
6896
+ void host?.close();
6897
+ };
6898
+ }, []);
6663
6899
  const hasOrigin = (source: string, origin: string) => {
6664
6900
  try { return new URL(source).origin === origin; } catch { return false; }
6665
6901
  };
@@ -6672,18 +6908,16 @@ export function AbsoluteWebHost() {
6672
6908
  if (message.kind === 'event' && (message.event === 'navigation' || message.event === 'ready')) {
6673
6909
  const target = new URL(message.path, PRODUCTION_ORIGIN);
6674
6910
  if (target.origin !== PRODUCTION_ORIGIN) return;
6675
- if (NATIVE_ROUTES.has(target.pathname)) router.push(message.path as never);
6911
+ if (isNativeRoute(target.pathname)) router.push(message.path as never);
6676
6912
  else activeWebPath.current = message.path;
6677
6913
  return;
6678
6914
  }
6679
6915
  if (message.kind !== 'request' || typeof message.id !== 'string' || message.path !== activeWebPath.current) return;
6680
6916
  try {
6681
6917
  if (typeof message.params !== 'object' || message.params === null || Array.isArray(message.params)) throw new Error('Expo bridge method params are invalid.');
6682
- if (message.method === 'devices.haptics.impact') {
6683
- const style = (message.params as Record<string, unknown>).style;
6684
- if (typeof style !== 'string' || !['error', 'heavy', 'light', 'medium', 'selection', 'success', 'vibrate', 'warning'].includes(style)) throw new Error('Expo bridge haptics style is invalid.');
6685
- await impact(message.params as Record<string, unknown>);
6686
- respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: null });
6918
+ if (typeof message.method === 'string' && message.method.startsWith('devices.')) {
6919
+ if (!devicesBridge.current) throw new Error('Expo devices bridge is unavailable.');
6920
+ respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await devicesBridge.current.request(message.method, message.params as Record<string, unknown>) });
6687
6921
  } else if (message.method === 'http.fetch') {
6688
6922
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await bridgeFetch(message.params as Record<string, unknown>) });
6689
6923
  } else if (message.method === 'auth.signIn') {
@@ -6692,7 +6926,8 @@ export function AbsoluteWebHost() {
6692
6926
  await absoluteExpoAuth.signIn({ authorizationParameters: { login_hint: params.email, ...(params.signup ? { screen_hint: 'signup' } : {}) } });
6693
6927
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });` : "throw new Error('Expo Auth is not configured.');"}
6694
6928
  } else if (message.method === 'auth.signOut') {
6695
- ${auth ? `await absoluteExpoAuth.signOut();
6929
+ ${auth ? `await beforeAbsoluteExpoDeviceSignOut();
6930
+ await absoluteExpoAuth.signOut();
6696
6931
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: null });` : "throw new Error('Expo Auth is not configured.');"}
6697
6932
  } else if (message.method === 'auth.status') {
6698
6933
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });
@@ -6707,7 +6942,7 @@ export function AbsoluteWebHost() {
6707
6942
  }
6708
6943
  };
6709
6944
 
6710
- if (!indexUri || !runtimeReady) return <View style={styles.loading}><ActivityIndicator /></View>;
6945
+ if (!indexUri || !runtimeReady || !devicesReady) return <View style={styles.loading}><ActivityIndicator /></View>;
6711
6946
  return <WebView
6712
6947
  allowFileAccess
6713
6948
  allowFileAccessFromFileURLs
@@ -6791,6 +7026,18 @@ var writeManagedFile = async (path, source, force) => {
6791
7026
  await rename11(temporary, path);
6792
7027
  return true;
6793
7028
  };
7029
+ var pruneStaleManagedExpoRoutes = async (project, expected) => {
7030
+ const appDirectory = join14(project, "app");
7031
+ if (!await exists3(appDirectory))
7032
+ return 0;
7033
+ const files = await walkFiles(appDirectory);
7034
+ const stale = (await Promise.all(files.map(async (path) => ({
7035
+ managed: path.endsWith(".tsx") && (await readFile14(path, "utf8")).startsWith(EXPO_GENERATED_HEADER),
7036
+ path
7037
+ })))).filter(({ managed, path }) => managed && !expected.has(path));
7038
+ await Promise.all(stale.map(({ path }) => rm9(path, { force: true })));
7039
+ return stale.length;
7040
+ };
6794
7041
  var jsonSource = (value) => `${JSON.stringify(value, null, "\t")}
6795
7042
  `;
6796
7043
  var emptyWebAssetsSource = `${EXPO_GENERATED_HEADER}export const materializeAbsoluteWebBundle = async () => {
@@ -6803,6 +7050,7 @@ var writeAbsoluteExpoProject = async (config, options) => {
6803
7050
  const projectRoot = resolve12(options.projectRoot);
6804
7051
  const project = config.nativeProjectDirectory;
6805
7052
  const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
7053
+ const devices = resolveAbsoluteDeviceCapabilityPlan(projectRoot, "expo");
6806
7054
  const syncEnabled = Boolean(auth && projectUsesAbsoluteSync(projectRoot));
6807
7055
  const syncSchema = syncEnabled ? { components: discoverAbsoluteSyncSchema(projectRoot).components } : undefined;
6808
7056
  const routeModules = Object.entries(config.expoNativeRoutes);
@@ -6837,12 +7085,12 @@ node_modules/
6837
7085
  ],
6838
7086
  [
6839
7087
  join14(project, "app.json"),
6840
- jsonSource(expoAppConfig(config, authEnabled, syncEnabled))
7088
+ jsonSource(expoAppConfig(config, authEnabled, syncEnabled, devices))
6841
7089
  ],
6842
7090
  [join14(project, "app.config.js"), expoDynamicAppConfig],
6843
7091
  [
6844
7092
  join14(project, "package.json"),
6845
- jsonSource(expoPackage(authEnabled, syncEnabled))
7093
+ jsonSource(expoPackage(authEnabled, syncEnabled, devices))
6846
7094
  ],
6847
7095
  [join14(project, "metro.config.js"), metroConfig(projectRoot)],
6848
7096
  [
@@ -6861,6 +7109,10 @@ node_modules/
6861
7109
  join14(project, "app", "__absolute", "native", "index.tsx"),
6862
7110
  nativeDiagnosticSource
6863
7111
  ],
7112
+ [
7113
+ join14(project, "src", "generated", "AbsoluteDevices.ts"),
7114
+ devicesRuntimeSource(config, devices, authEnabled)
7115
+ ],
6864
7116
  [
6865
7117
  join14(project, "src", "generated", "AbsoluteWebHost.tsx"),
6866
7118
  webHostSource(config, auth, syncEnabled)
@@ -6884,8 +7136,9 @@ node_modules/
6884
7136
  const wrapper = route === "/" ? join14(project, "app", "index.tsx") : routeFile(project, route);
6885
7137
  files.set(wrapper, nativeWrapperSource(wrapper, module));
6886
7138
  }
7139
+ const removed = await pruneStaleManagedExpoRoutes(project, new Set([...files.keys()].filter((path) => path.startsWith(`${join14(project, "app")}${sep6}`))));
6887
7140
  const changes = await Promise.all([...files].map(([path, source]) => writeManagedFile(path, source, true)));
6888
- const changed = changes.filter(Boolean).length;
7141
+ const changed = removed + changes.filter(Boolean).length;
6889
7142
  return { changed, path: project, written: [...files.keys()] };
6890
7143
  };
6891
7144
  var walkFiles = async (root, directory = root) => {
@@ -7072,11 +7325,8 @@ var finalizeAbsoluteMobileCompatibilityBuild = async (options) => {
7072
7325
  const auth = resolveAbsoluteMobileAuthManifest(options.projectRoot, mobile);
7073
7326
  const sync = auth !== undefined && projectUsesAbsoluteSync(options.projectRoot);
7074
7327
  const syncSchema = sync ? discoverAbsoluteSyncSchema(options.projectRoot) : undefined;
7075
- const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot);
7328
+ const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot, mobile.engine);
7076
7329
  const usesPush = deviceCapabilities.capabilities.includes("pushNotifications");
7077
- if (mobile.engine === "expo" && deviceCapabilities.capabilities.some((capability) => capability !== "haptics")) {
7078
- throw new TypeError("Experimental Expo builds currently bridge only @absolutejs/devices haptics. Other detected device capabilities require their Expo adapters.");
7079
- }
7080
7330
  if (usesPush && !auth)
7081
7331
  throw new TypeError("Portable push notifications require @absolutejs/auth so provider tokens can be registered without exposing identity controls to page code.");
7082
7332
  if (usesPush && !loaded.app.routes.some((route) => route.path === "/auth/push" || route.path === "/auth/mobile/push"))
@@ -7234,7 +7484,64 @@ init_deviceCapabilities();
7234
7484
  var ABSOLUTE_EXPO_BRIDGE_FORMAT = 3;
7235
7485
  var ABSOLUTE_EXPO_BRIDGE_MAX_BYTES = 64 * 1024;
7236
7486
  var ABSOLUTE_EXPO_BRIDGE_METHODS = [
7487
+ "devices.platform.getInfo",
7488
+ "devices.lifecycle.getState",
7489
+ "devices.links.getLaunchUrl",
7490
+ "devices.links.openExternal",
7491
+ "devices.network.getStatus",
7492
+ "devices.storage.clear",
7493
+ "devices.storage.get",
7494
+ "devices.storage.keys",
7495
+ "devices.storage.remove",
7496
+ "devices.storage.set",
7497
+ "devices.back.capability",
7498
+ "devices.clipboard.capability",
7499
+ "devices.clipboard.readText",
7500
+ "devices.clipboard.writeText",
7501
+ "devices.share.capability",
7502
+ "devices.share.share",
7503
+ "devices.haptics.capability",
7237
7504
  "devices.haptics.impact",
7505
+ "devices.haptics.notification",
7506
+ "devices.haptics.selectionChanged",
7507
+ "devices.haptics.vibrate",
7508
+ "devices.keyboard.capability",
7509
+ "devices.keyboard.dismiss",
7510
+ "devices.keyboard.getState",
7511
+ "devices.systemBars.capability",
7512
+ "devices.systemBars.setAppearance",
7513
+ "devices.systemBars.setVisible",
7514
+ "devices.camera.capability",
7515
+ "devices.camera.queryPermission",
7516
+ "devices.camera.requestPermission",
7517
+ "devices.camera.takePhoto",
7518
+ "devices.photos.capability",
7519
+ "devices.photos.pick",
7520
+ "devices.location.capability",
7521
+ "devices.location.current",
7522
+ "devices.location.queryPermission",
7523
+ "devices.location.requestPermission",
7524
+ "devices.location.watch.start",
7525
+ "devices.location.watch.stop",
7526
+ "devices.localNotifications.capability",
7527
+ "devices.localNotifications.queryPermission",
7528
+ "devices.localNotifications.requestPermission",
7529
+ "devices.localNotifications.schedule",
7530
+ "devices.localNotifications.pending",
7531
+ "devices.localNotifications.cancel",
7532
+ "devices.pushNotifications.capability",
7533
+ "devices.pushNotifications.queryPermission",
7534
+ "devices.pushNotifications.requestPermission",
7535
+ "devices.pushNotifications.enable",
7536
+ "devices.pushNotifications.disable",
7537
+ "devices.documents.capability",
7538
+ "devices.documents.pick",
7539
+ "devices.transfer.read",
7540
+ "devices.transfer.close",
7541
+ "devices.upload.begin",
7542
+ "devices.upload.write",
7543
+ "devices.documents.export",
7544
+ "devices.documents.open",
7238
7545
  "http.fetch",
7239
7546
  "auth.signIn",
7240
7547
  "auth.signOut",
@@ -7749,9 +8056,9 @@ var startAbsoluteExpoDevSession = async (options) => {
7749
8056
  init_config();
7750
8057
 
7751
8058
  // src/mobile/ciWorkflow.ts
7752
- import { existsSync as existsSync3 } from "fs";
8059
+ import { existsSync as existsSync4 } from "fs";
7753
8060
  import { access as access11, mkdir as mkdir12, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
7754
- import { dirname as dirname11, extname as extname4, relative as relative11, resolve as resolve14, sep as sep6 } from "path";
8061
+ import { dirname as dirname11, extname as extname4, relative as relative11, resolve as resolve14, sep as sep7 } from "path";
7755
8062
  var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1;
7756
8063
  var SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
7757
8064
  var CI_ENV_INDENTATION = 6;
@@ -7783,12 +8090,12 @@ var projectPath = (projectRoot, value, field2, options = {}) => {
7783
8090
  const root = resolve14(projectRoot);
7784
8091
  const path = resolve14(root, value);
7785
8092
  const portable = relative11(root, path).replaceAll("\\", "/");
7786
- if (portable === ".." || portable.startsWith(`..${sep6}`) || portable.startsWith("../") || portable === "") {
8093
+ if (portable === ".." || portable.startsWith(`..${sep7}`) || portable.startsWith("../") || portable === "") {
7787
8094
  throw new TypeError(`${field2} must remain inside the project root.`);
7788
8095
  }
7789
8096
  if (/\r|\n/u.test(portable) || portable.startsWith("-"))
7790
8097
  throw new TypeError(`${field2} contains an unsafe path.`);
7791
- if (!options.allowMissing && !existsSync3(path))
8098
+ if (!options.allowMissing && !existsSync4(path))
7792
8099
  throw new TypeError(`${field2} does not exist inside the project.`);
7793
8100
  return portable;
7794
8101
  };
@@ -7797,7 +8104,7 @@ var workflowOutputPath = (projectRoot, value) => {
7797
8104
  const workflows = resolve14(root, ".github/workflows");
7798
8105
  const path = resolve14(root, value ?? ".github/workflows/absolute-mobile.yml");
7799
8106
  const portable = relative11(workflows, path);
7800
- if (portable === ".." || portable.startsWith(`..${sep6}`) || extname4(path) !== ".yml" && extname4(path) !== ".yaml") {
8107
+ if (portable === ".." || portable.startsWith(`..${sep7}`) || extname4(path) !== ".yml" && extname4(path) !== ".yaml") {
7801
8108
  throw new TypeError("mobile ci github --output must be a .yml or .yaml file inside .github/workflows.");
7802
8109
  }
7803
8110
  return path;
@@ -8976,7 +9283,7 @@ init_nativeAuth();
8976
9283
 
8977
9284
  // src/mobile/releasePublisher.ts
8978
9285
  import { access as access12 } from "fs/promises";
8979
- import { isAbsolute as isAbsolute6, relative as relative12, resolve as resolve15, sep as sep7 } from "path";
9286
+ import { isAbsolute as isAbsolute6, relative as relative12, resolve as resolve15, sep as sep8 } from "path";
8980
9287
  import { pathToFileURL as pathToFileURL3 } from "url";
8981
9288
  var prepareAbsoluteIosRelease = async (publisher, options) => {
8982
9289
  if (typeof publisher.prepareIosRelease !== "function") {
@@ -9005,7 +9312,7 @@ var publisherModulePath = (projectRoot, requested) => {
9005
9312
  const root = resolve15(projectRoot);
9006
9313
  const path = resolve15(root, requested);
9007
9314
  const projectRelative = relative12(root, path);
9008
- if (projectRelative === ".." || projectRelative.startsWith(`..${sep7}`) || isAbsolute6(projectRelative)) {
9315
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep8}`) || isAbsolute6(projectRelative)) {
9009
9316
  throw new TypeError("mobile publish --registry must remain inside the project.");
9010
9317
  }
9011
9318
  return path;
@@ -9073,7 +9380,7 @@ var publishAbsoluteIosRelease = async (options) => {
9073
9380
  return publication;
9074
9381
  };
9075
9382
  // src/mobile/routeMetadataTransform.ts
9076
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
9383
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
9077
9384
  import { dirname as dirname13, extname as extname5, relative as relative13, resolve as resolve16 } from "path";
9078
9385
  import ts2 from "typescript";
9079
9386
  var ROUTE_METHODS = new Set(["get", "head"]);
@@ -9125,7 +9432,7 @@ var PAGE_HANDLERS = new Map([
9125
9432
  ]
9126
9433
  ]);
9127
9434
  var posixPath = (value) => value.replace(/\\/g, "/");
9128
- var findTsconfig = (entry, projectRoot) => ts2.findConfigFile(dirname13(entry), existsSync6, "tsconfig.json") ?? ts2.findConfigFile(projectRoot, existsSync6, "tsconfig.json");
9435
+ var findTsconfig = (entry, projectRoot) => ts2.findConfigFile(dirname13(entry), existsSync7, "tsconfig.json") ?? ts2.findConfigFile(projectRoot, existsSync7, "tsconfig.json");
9129
9436
  var createProgram = (entry, projectRoot) => {
9130
9437
  const configPath2 = findTsconfig(entry, projectRoot);
9131
9438
  if (!configPath2) {
@@ -9965,5 +10272,5 @@ export {
9965
10272
  writeAbsoluteMobileGithubWorkflow
9966
10273
  };
9967
10274
 
9968
- //# debugId=735330FB398C20EB64756E2164756E21
10275
+ //# debugId=CFD74BC8ED7A634C64756E2164756E21
9969
10276
  //# sourceMappingURL=index.js.map