@absolutejs/absolute 0.20.0-beta.45 → 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.
@@ -772,10 +772,11 @@ var init_syncSchema = __esm(() => {
772
772
  });
773
773
 
774
774
  // src/mobile/deviceCapabilities.ts
775
- import { readFileSync as readFileSync4 } from "fs";
775
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
776
776
  import { extname as extname3, join as join13, relative as relative9, resolve as resolve11 } from "path";
777
+ import { fileURLToPath } from "url";
777
778
  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) => {
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) => {
779
780
  const value = JSON.parse(readFileSync4(path, "utf8"));
780
781
  if (!object2(value))
781
782
  throw new TypeError(`${path} must contain an object.`);
@@ -835,7 +836,7 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
835
836
  ...systemBars === true ? { systemBars: true } : {},
836
837
  ...usageDescriptions === undefined ? {} : { usageDescriptions: [...usageDescriptions] }
837
838
  };
838
- }, parseProvider = (name, value) => {
839
+ }, parseProvider = (name, value, providerName) => {
839
840
  if (!IDENTIFIER_PATTERN.test(name))
840
841
  throw new TypeError("Device capability names must be identifiers.");
841
842
  if (!object2(value))
@@ -844,10 +845,13 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
844
845
  const module = text(value.module, `${name}.module`);
845
846
  if (!IDENTIFIER_PATTERN.test(factory))
846
847
  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.`);
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.`);
851
855
  let native;
852
856
  const { native: nativeMetadata } = value;
853
857
  if (nativeMetadata !== undefined) {
@@ -865,6 +869,7 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
865
869
  factory,
866
870
  module,
867
871
  ...native === undefined ? {} : { native },
872
+ ...plugins === undefined ? {} : { plugins: [...plugins] },
868
873
  packages: [...value.packages]
869
874
  };
870
875
  }, absoluteDeviceNativeRequirements = (plan) => {
@@ -894,18 +899,27 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
894
899
  ...new Set(plan.capabilities.flatMap((name) => plan.providers[name]?.native?.ios?.usageDescriptions ?? []))
895
900
  ].sort()
896
901
  };
897
- }, loadAbsoluteDeviceCapabilityProviders = (projectRoot) => {
898
- 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
+ }
899
910
  const manifest = readJson(path);
900
911
  const { absolutejs } = manifest;
901
912
  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]) => ({
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]) => ({
905
916
  name,
906
- provider: parseProvider(name, provider)
917
+ provider: parseProvider(name, capability, provider)
907
918
  }));
908
- 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
+ ]));
909
923
  }, isIgnored = (path) => path.split("/").some((segment) => IGNORED_DIRECTORIES.has(segment)), importedCapabilities = (source, file) => {
910
924
  const names = new Set;
911
925
  const namespaces = new Set;
@@ -962,6 +976,8 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
962
976
  return packages;
963
977
  }, discoverAbsoluteDeviceCapabilities = (projectRoot, providers = loadAbsoluteDeviceCapabilityProviders(projectRoot)) => {
964
978
  const root = resolve11(projectRoot);
979
+ if (!existsSync3(root))
980
+ return [];
965
981
  const known = new Set(Object.keys(providers));
966
982
  const capabilities = new Set;
967
983
  for (const path of SOURCE_GLOB.scanSync({ cwd: root })) {
@@ -988,14 +1004,14 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
988
1004
  return true;
989
1005
  }
990
1006
  return false;
991
- }, resolveAbsoluteDeviceCapabilityPlan = (projectRoot) => {
992
- const allProviders = loadAbsoluteDeviceCapabilityProviders(projectRoot);
1007
+ }, resolveAbsoluteDeviceCapabilityPlan = (projectRoot, provider = "capacitor") => {
1008
+ const allProviders = loadAbsoluteDeviceCapabilityProviders(projectRoot, provider);
993
1009
  const capabilities = discoverAbsoluteDeviceCapabilities(projectRoot, allProviders);
994
1010
  const providers = {};
995
1011
  for (const name of capabilities) {
996
- const provider = allProviders[name];
997
- if (provider)
998
- providers[name] = provider;
1012
+ const capabilityProvider = allProviders[name];
1013
+ if (capabilityProvider)
1014
+ providers[name] = capabilityProvider;
999
1015
  }
1000
1016
  return {
1001
1017
  capabilities,
@@ -1006,6 +1022,10 @@ var DEVICES_PACKAGE = "@absolutejs/devices", CAPACITOR_ADAPTER = "@absolutejs/de
1006
1022
  };
1007
1023
  };
1008
1024
  var init_deviceCapabilities = __esm(() => {
1025
+ ADAPTERS = {
1026
+ capacitor: "@absolutejs/devices-capacitor",
1027
+ expo: "@absolutejs/devices-expo"
1028
+ };
1009
1029
  SOURCE_GLOB = new Bun.Glob("**/*.{js,jsx,ts,tsx,svelte,vue}");
1010
1030
  IGNORED_DIRECTORIES = new Set([
1011
1031
  ".absolutejs",
@@ -1019,8 +1039,6 @@ var init_deviceCapabilities = __esm(() => {
1019
1039
  "tests"
1020
1040
  ]);
1021
1041
  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
1042
  ANDROID_PERMISSION_PATTERN = /^android\.permission\.[A-Z][A-Z0-9_]*$/u;
1025
1043
  IOS_USAGE_DESCRIPTIONS = new Set([
1026
1044
  "camera",
@@ -1038,12 +1056,12 @@ var init_deviceCapabilities = __esm(() => {
1038
1056
  });
1039
1057
 
1040
1058
  // src/cli/scripts/telemetry.ts
1041
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
1059
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
1042
1060
  import { homedir as homedir3 } from "os";
1043
1061
  import { join as join17 } from "path";
1044
1062
  var configDir, configPath, getTelemetryConfig = () => {
1045
1063
  try {
1046
- if (!existsSync4(configPath))
1064
+ if (!existsSync5(configPath))
1047
1065
  return null;
1048
1066
  const raw = readFileSync5(configPath, "utf-8");
1049
1067
  const config = JSON.parse(raw);
@@ -1058,11 +1076,11 @@ var init_telemetry = __esm(() => {
1058
1076
  });
1059
1077
 
1060
1078
  // src/cli/telemetryEvent.ts
1061
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
1079
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1062
1080
  import { arch, platform } from "os";
1063
1081
  import { dirname as dirname12, join as join18, parse } from "path";
1064
1082
  var checkCandidate = (candidate) => {
1065
- if (!existsSync5(candidate)) {
1083
+ if (!existsSync6(candidate)) {
1066
1084
  return null;
1067
1085
  }
1068
1086
  const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
@@ -5723,7 +5741,7 @@ const absoluteMobilePushCapability = absoluteDeviceCapability${pushIndex}(absolu
5723
5741
  const entryPath = join9(staging, ".absolute-mobile-entry.ts");
5724
5742
  const baseAdapterModule = capacitor ? await resolveProjectImport(projectRoot, "@absolutejs/devices-capacitor") : shellExpoDevicesModule();
5725
5743
  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();";
5744
+ const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : `installAbsoluteExpoWebDeviceAdapter(${JSON.stringify(deviceCapabilities.capabilities)});`;
5727
5745
  let shellOptions = auth ? options : "{ createFetch: createAbsoluteExpoBridgeFetch }";
5728
5746
  if (capacitor)
5729
5747
  shellOptions = options;
@@ -6096,6 +6114,7 @@ init_deviceCapabilities();
6096
6114
  // src/mobile/expoProject.ts
6097
6115
  init_nativeAuth();
6098
6116
  init_syncSchema();
6117
+ init_deviceCapabilities();
6099
6118
  import {
6100
6119
  access as access9,
6101
6120
  cp as cp2,
@@ -6134,8 +6153,14 @@ var routeSegments = (route) => route.split("/").filter(Boolean).map((segment) =>
6134
6153
  return segment;
6135
6154
  });
6136
6155
  var routeFile = (project, route) => join14(project, "app", ...routeSegments(route), "index.tsx");
6137
- var expoPackage = (auth, sync) => ({
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) => ({
6138
6161
  dependencies: {
6162
+ "@absolutejs/devices": "0.7.0",
6163
+ "@absolutejs/devices-expo": "0.0.2",
6139
6164
  ...auth ? {
6140
6165
  "@absolutejs/auth": ABSOLUTE_EXPO_AUTH_CORE_VERSION,
6141
6166
  [ABSOLUTE_EXPO_AUTH_PACKAGE]: ABSOLUTE_EXPO_AUTH_VERSION
@@ -6167,7 +6192,8 @@ var expoPackage = (auth, sync) => ({
6167
6192
  "react-native": "0.86.3",
6168
6193
  "react-native-safe-area-context": "~5.7.0",
6169
6194
  "react-native-screens": "4.26.0",
6170
- "react-native-webview": "13.16.1"
6195
+ "react-native-webview": "13.16.1",
6196
+ ...packageDependencies(devices)
6171
6197
  },
6172
6198
  devDependencies: {
6173
6199
  "@types/react": "~19.2.2",
@@ -6183,36 +6209,107 @@ var expoPackage = (auth, sync) => ({
6183
6209
  },
6184
6210
  version: "0.0.0"
6185
6211
  });
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
- });
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
+ };
6216
6313
  var expoDynamicAppConfig = `${EXPO_GENERATED_HEADER}const config = require('./app.json');
6217
6314
 
6218
6315
  if (process.env.ABSOLUTE_EXPO_DEVELOPMENT === '1' && process.env.ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH) {
@@ -6280,6 +6377,7 @@ config.watchFolders = [appRoot];
6280
6377
  module.exports = config;
6281
6378
  `;
6282
6379
  var layoutSource = (auth, sync) => `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
6380
+ import '../src/generated/AbsoluteDevices';
6283
6381
  ${auth ? `import { useEffect, useState } from 'react';
6284
6382
  import { startAbsoluteExpoAuth } from '../src/generated/AbsoluteAuth';` : ""}
6285
6383
  ${sync ? "import { startAbsoluteExpoSync } from '../src/generated/AbsoluteSync';" : ""}
@@ -6326,6 +6424,76 @@ const styles = StyleSheet.create({
6326
6424
  title: { color: '#f8fafc', fontSize: 32, fontWeight: '800' }
6327
6425
  });
6328
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
+ };
6329
6497
  var authRuntimeSource = (auth, appId) => {
6330
6498
  const storageIdentity = createHash10("sha256").update(appId).digest("hex").slice(0, 24);
6331
6499
  return `${EXPO_GENERATED_HEADER}import { createAbsoluteExpoAuthClient } from '@absolutejs/auth-expo';
@@ -6461,13 +6629,14 @@ var webHostSource = (config, auth, sync) => {
6461
6629
  "/__absolute/native",
6462
6630
  ...Object.keys(config.expoNativeRoutes)
6463
6631
  ];
6464
- return `${EXPO_GENERATED_HEADER}import * as Haptics from 'expo-haptics';
6465
- import * as Linking from 'expo-linking';
6632
+ return `${EXPO_GENERATED_HEADER}import * as Linking from 'expo-linking';
6466
6633
  import { router, usePathname } from 'expo-router';
6467
6634
  import { useEffect, useRef, useState } from 'react';
6468
6635
  import { ActivityIndicator, BackHandler, Platform, StyleSheet, View } from 'react-native';
6469
6636
  import { WebView, type WebViewMessageEvent } from 'react-native-webview';
6470
6637
  import { materializeAbsoluteWebBundle } from './webAssets';
6638
+ import { createExpoDevicesBridgeHost } from '@absolutejs/devices-expo/bridge';
6639
+ import { absoluteExpoDevices, beforeAbsoluteExpoDeviceSignOut } from './AbsoluteDevices';
6471
6640
  ${auth ? "import { absoluteExpoAuth, getAbsoluteExpoAuthPrincipal, startAbsoluteExpoAuth } from './AbsoluteAuth';" : ""}
6472
6641
  ${sync ? "import { createAbsoluteExpoSyncBridge, startAbsoluteExpoSync } from './AbsoluteSync';" : ""}
6473
6642
 
@@ -6523,10 +6692,11 @@ const bridgeBootstrap = (path: string) => {
6523
6692
  const id = 'web_' + Date.now().toString(36) + '_' + (++sequence).toString(36);
6524
6693
  send({ format: 3, id, kind: 'request', method, params, path: currentPath });
6525
6694
  return new Promise((resolve, reject) => {
6695
+ const interactive = method.startsWith('devices.camera.') || method.startsWith('devices.photos.') || method.startsWith('devices.documents.') || method.endsWith('.requestPermission');
6526
6696
  const timer = setTimeout(() => {
6527
6697
  pending.delete(id);
6528
6698
  reject(new Error('Expo bridge request timed out.'));
6529
- }, 10000);
6699
+ }, interactive ? 5 * 60 * 1000 : 30 * 1000);
6530
6700
  pending.set(id, { reject, resolve, timer });
6531
6701
  });
6532
6702
  },
@@ -6565,17 +6735,6 @@ const bridgeBootstrap = (path: string) => {
6565
6735
  })(); true;\`;
6566
6736
  };
6567
6737
 
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
6738
  const bridgeFetch = async (params: Record<string, unknown>) => {
6580
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.');
6581
6740
  const url = new URL(params.url);
@@ -6607,10 +6766,12 @@ const authStatus = async () => {
6607
6766
  export function AbsoluteWebHost() {
6608
6767
  const pathname = usePathname() || '/';
6609
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);
6610
6770
  const syncBridge = useRef<{ close(): void | Promise<void>; request(method: string, params: Record<string, unknown>): Promise<unknown> } | undefined>(undefined);
6611
6771
  const [indexUri, setIndexUri] = useState<string>();
6612
6772
  const [canGoBack, setCanGoBack] = useState(false);
6613
6773
  const [runtimeReady, setRuntimeReady] = useState(!AUTH_ENABLED && !SYNC_ENABLED);
6774
+ const [devicesReady, setDevicesReady] = useState(false);
6614
6775
  const activeWebPath = useRef(pathname);
6615
6776
 
6616
6777
  useEffect(() => {
@@ -6660,6 +6821,23 @@ export function AbsoluteWebHost() {
6660
6821
  if (new TextEncoder().encode(source).byteLength > MAX_MESSAGE_BYTES) throw new Error('Expo bridge response exceeds 64 KiB.');
6661
6822
  webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
6662
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
+ }, []);
6663
6841
  const hasOrigin = (source: string, origin: string) => {
6664
6842
  try { return new URL(source).origin === origin; } catch { return false; }
6665
6843
  };
@@ -6679,11 +6857,9 @@ export function AbsoluteWebHost() {
6679
6857
  if (message.kind !== 'request' || typeof message.id !== 'string' || message.path !== activeWebPath.current) return;
6680
6858
  try {
6681
6859
  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 });
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>) });
6687
6863
  } else if (message.method === 'http.fetch') {
6688
6864
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await bridgeFetch(message.params as Record<string, unknown>) });
6689
6865
  } else if (message.method === 'auth.signIn') {
@@ -6692,7 +6868,8 @@ export function AbsoluteWebHost() {
6692
6868
  await absoluteExpoAuth.signIn({ authorizationParameters: { login_hint: params.email, ...(params.signup ? { screen_hint: 'signup' } : {}) } });
6693
6869
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });` : "throw new Error('Expo Auth is not configured.');"}
6694
6870
  } else if (message.method === 'auth.signOut') {
6695
- ${auth ? `await absoluteExpoAuth.signOut();
6871
+ ${auth ? `await beforeAbsoluteExpoDeviceSignOut();
6872
+ await absoluteExpoAuth.signOut();
6696
6873
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: null });` : "throw new Error('Expo Auth is not configured.');"}
6697
6874
  } else if (message.method === 'auth.status') {
6698
6875
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });
@@ -6707,7 +6884,7 @@ export function AbsoluteWebHost() {
6707
6884
  }
6708
6885
  };
6709
6886
 
6710
- if (!indexUri || !runtimeReady) return <View style={styles.loading}><ActivityIndicator /></View>;
6887
+ if (!indexUri || !runtimeReady || !devicesReady) return <View style={styles.loading}><ActivityIndicator /></View>;
6711
6888
  return <WebView
6712
6889
  allowFileAccess
6713
6890
  allowFileAccessFromFileURLs
@@ -6803,6 +6980,7 @@ var writeAbsoluteExpoProject = async (config, options) => {
6803
6980
  const projectRoot = resolve12(options.projectRoot);
6804
6981
  const project = config.nativeProjectDirectory;
6805
6982
  const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
6983
+ const devices = resolveAbsoluteDeviceCapabilityPlan(projectRoot, "expo");
6806
6984
  const syncEnabled = Boolean(auth && projectUsesAbsoluteSync(projectRoot));
6807
6985
  const syncSchema = syncEnabled ? { components: discoverAbsoluteSyncSchema(projectRoot).components } : undefined;
6808
6986
  const routeModules = Object.entries(config.expoNativeRoutes);
@@ -6837,12 +7015,12 @@ node_modules/
6837
7015
  ],
6838
7016
  [
6839
7017
  join14(project, "app.json"),
6840
- jsonSource(expoAppConfig(config, authEnabled, syncEnabled))
7018
+ jsonSource(expoAppConfig(config, authEnabled, syncEnabled, devices))
6841
7019
  ],
6842
7020
  [join14(project, "app.config.js"), expoDynamicAppConfig],
6843
7021
  [
6844
7022
  join14(project, "package.json"),
6845
- jsonSource(expoPackage(authEnabled, syncEnabled))
7023
+ jsonSource(expoPackage(authEnabled, syncEnabled, devices))
6846
7024
  ],
6847
7025
  [join14(project, "metro.config.js"), metroConfig(projectRoot)],
6848
7026
  [
@@ -6861,6 +7039,10 @@ node_modules/
6861
7039
  join14(project, "app", "__absolute", "native", "index.tsx"),
6862
7040
  nativeDiagnosticSource
6863
7041
  ],
7042
+ [
7043
+ join14(project, "src", "generated", "AbsoluteDevices.ts"),
7044
+ devicesRuntimeSource(config, devices, authEnabled)
7045
+ ],
6864
7046
  [
6865
7047
  join14(project, "src", "generated", "AbsoluteWebHost.tsx"),
6866
7048
  webHostSource(config, auth, syncEnabled)
@@ -7072,11 +7254,8 @@ var finalizeAbsoluteMobileCompatibilityBuild = async (options) => {
7072
7254
  const auth = resolveAbsoluteMobileAuthManifest(options.projectRoot, mobile);
7073
7255
  const sync = auth !== undefined && projectUsesAbsoluteSync(options.projectRoot);
7074
7256
  const syncSchema = sync ? discoverAbsoluteSyncSchema(options.projectRoot) : undefined;
7075
- const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot);
7257
+ const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot, mobile.engine);
7076
7258
  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
7259
  if (usesPush && !auth)
7081
7260
  throw new TypeError("Portable push notifications require @absolutejs/auth so provider tokens can be registered without exposing identity controls to page code.");
7082
7261
  if (usesPush && !loaded.app.routes.some((route) => route.path === "/auth/push" || route.path === "/auth/mobile/push"))
@@ -7234,7 +7413,64 @@ init_deviceCapabilities();
7234
7413
  var ABSOLUTE_EXPO_BRIDGE_FORMAT = 3;
7235
7414
  var ABSOLUTE_EXPO_BRIDGE_MAX_BYTES = 64 * 1024;
7236
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",
7237
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",
7238
7474
  "http.fetch",
7239
7475
  "auth.signIn",
7240
7476
  "auth.signOut",
@@ -7749,7 +7985,7 @@ var startAbsoluteExpoDevSession = async (options) => {
7749
7985
  init_config();
7750
7986
 
7751
7987
  // src/mobile/ciWorkflow.ts
7752
- import { existsSync as existsSync3 } from "fs";
7988
+ import { existsSync as existsSync4 } from "fs";
7753
7989
  import { access as access11, mkdir as mkdir12, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
7754
7990
  import { dirname as dirname11, extname as extname4, relative as relative11, resolve as resolve14, sep as sep6 } from "path";
7755
7991
  var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1;
@@ -7788,7 +8024,7 @@ var projectPath = (projectRoot, value, field2, options = {}) => {
7788
8024
  }
7789
8025
  if (/\r|\n/u.test(portable) || portable.startsWith("-"))
7790
8026
  throw new TypeError(`${field2} contains an unsafe path.`);
7791
- if (!options.allowMissing && !existsSync3(path))
8027
+ if (!options.allowMissing && !existsSync4(path))
7792
8028
  throw new TypeError(`${field2} does not exist inside the project.`);
7793
8029
  return portable;
7794
8030
  };
@@ -9073,7 +9309,7 @@ var publishAbsoluteIosRelease = async (options) => {
9073
9309
  return publication;
9074
9310
  };
9075
9311
  // src/mobile/routeMetadataTransform.ts
9076
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
9312
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
9077
9313
  import { dirname as dirname13, extname as extname5, relative as relative13, resolve as resolve16 } from "path";
9078
9314
  import ts2 from "typescript";
9079
9315
  var ROUTE_METHODS = new Set(["get", "head"]);
@@ -9125,7 +9361,7 @@ var PAGE_HANDLERS = new Map([
9125
9361
  ]
9126
9362
  ]);
9127
9363
  var posixPath = (value) => value.replace(/\\/g, "/");
9128
- 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");
9129
9365
  var createProgram = (entry, projectRoot) => {
9130
9366
  const configPath2 = findTsconfig(entry, projectRoot);
9131
9367
  if (!configPath2) {
@@ -9965,5 +10201,5 @@ export {
9965
10201
  writeAbsoluteMobileGithubWorkflow
9966
10202
  };
9967
10203
 
9968
- //# debugId=735330FB398C20EB64756E2164756E21
10204
+ //# debugId=99CCB7E44742D46964756E2164756E21
9969
10205
  //# sourceMappingURL=index.js.map