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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/angular/browser.js +378 -361
  2. package/dist/angular/browser.js.map +2 -2
  3. package/dist/angular/components/core/streamingSlotRegistrar.js +7 -7
  4. package/dist/angular/components/core/streamingSlotRegistry.js +4 -4
  5. package/dist/angular/index.js +300 -294
  6. package/dist/angular/index.js.map +2 -2
  7. package/dist/angular/server.js +54 -50
  8. package/dist/angular/server.js.map +2 -2
  9. package/dist/build.js +199 -203
  10. package/dist/build.js.map +2 -2
  11. package/dist/cli/config/client.js +12 -12
  12. package/dist/cli/config/server.js +1096 -1090
  13. package/dist/cli/index.js +595 -256
  14. package/dist/client/index.js +29 -25
  15. package/dist/client/index.js.map +2 -2
  16. package/dist/core/streamingSlotRegistrar.js +15 -13
  17. package/dist/core/streamingSlotRegistrar.js.map +2 -2
  18. package/dist/core/streamingSlotRegistry.js +11 -9
  19. package/dist/core/streamingSlotRegistry.js.map +2 -2
  20. package/dist/image-client/imageClient.js +7 -7
  21. package/dist/index.js +782 -732
  22. package/dist/index.js.map +6 -5
  23. package/dist/islands/browser.js +11 -9
  24. package/dist/islands/browser.js.map +2 -2
  25. package/dist/islands/index.js +39 -35
  26. package/dist/islands/index.js.map +2 -2
  27. package/dist/mobile/browser.js +23 -22
  28. package/dist/mobile/browser.js.map +2 -2
  29. package/dist/mobile/index.js +724 -488
  30. package/dist/mobile/index.js.map +12 -12
  31. package/dist/mobile/remoteMacAgentEntry.js +413 -14
  32. package/dist/mobile/shellBootstrap.js +0 -1
  33. package/dist/mobile/shellExpoAuth.js +184 -0
  34. package/dist/mobile/shellExpoDevices.js +4 -5
  35. package/dist/mobile/shellSync.js +4 -4
  36. package/dist/react/browser.js +17 -15
  37. package/dist/react/browser.js.map +2 -2
  38. package/dist/react/components/browser/index.js +107 -107
  39. package/dist/react/components/index.js +118 -116
  40. package/dist/react/components/index.js.map +2 -2
  41. package/dist/react/hooks/index.js +11 -9
  42. package/dist/react/hooks/index.js.map +2 -2
  43. package/dist/react/index.js +53 -49
  44. package/dist/react/index.js.map +2 -2
  45. package/dist/react/jsxDevRuntimeCompat.js +11 -9
  46. package/dist/react/jsxDevRuntimeCompat.js.map +2 -2
  47. package/dist/react/jsxRuntimeCompat.js +11 -9
  48. package/dist/react/jsxRuntimeCompat.js.map +2 -2
  49. package/dist/react/router/browser.js +9 -7
  50. package/dist/react/router/browser.js.map +2 -2
  51. package/dist/react/router/index.js +9 -7
  52. package/dist/react/router/index.js.map +2 -2
  53. package/dist/react/server.js +12 -10
  54. package/dist/react/server.js.map +2 -2
  55. package/dist/src/mobile/devDeviceAdapter.d.ts +1 -1
  56. package/dist/src/mobile/expoBridge.d.ts +12 -12
  57. package/dist/src/mobile/expoDevController.d.ts +8 -4
  58. package/dist/src/mobile/nativeAuth.d.ts +3 -0
  59. package/dist/src/mobile/remoteMacProtocol.d.ts +27 -0
  60. package/dist/src/mobile/remoteMacWire.d.ts +1 -1
  61. package/dist/src/mobile/shellExpoAuth.d.ts +12 -0
  62. package/dist/svelte/browser.js +12 -10
  63. package/dist/svelte/browser.js.map +2 -2
  64. package/dist/svelte/index.js +47 -43
  65. package/dist/svelte/index.js.map +2 -2
  66. package/dist/svelte/server.js +29 -27
  67. package/dist/svelte/server.js.map +2 -2
  68. package/dist/vue/browser.js +14 -12
  69. package/dist/vue/browser.js.map +2 -2
  70. package/dist/vue/components/Image.js +9 -7
  71. package/dist/vue/components/Image.js.map +2 -2
  72. package/dist/vue/components/index.js +11 -9
  73. package/dist/vue/components/index.js.map +2 -2
  74. package/dist/vue/index.js +52 -48
  75. package/dist/vue/index.js.map +2 -2
  76. package/dist/vue/server.js +14 -12
  77. package/dist/vue/server.js.map +2 -2
  78. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -847,6 +847,50 @@ var init_config = __esm(() => {
847
847
  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])?))*$/;
848
848
  });
849
849
 
850
+ // src/mobile/nativeAuth.ts
851
+ import { readFileSync as readFileSync5 } from "fs";
852
+ import { join as join5 } from "path";
853
+ var ABSOLUTE_AUTH_PACKAGE = "@absolutejs/auth", ABSOLUTE_EXPO_AUTH_CORE_VERSION = "0.75.6", ABSOLUTE_EXPO_AUTH_PACKAGE = "@absolutejs/auth-expo", ABSOLUTE_EXPO_AUTH_VERSION = "0.0.2", ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV = "ABSOLUTE_AUTH_NATIVE_CLIENTS", ABSOLUTE_NATIVE_AUTH_SCOPES, ABSOLUTE_SYNC_PACKAGE = "@absolutejs/sync", readPackageManifest = (projectRoot) => {
854
+ try {
855
+ return JSON.parse(readFileSync5(join5(projectRoot, "package.json"), "utf8"));
856
+ } catch {
857
+ return;
858
+ }
859
+ }, packageManifestHas = (manifest, packageName) => {
860
+ if (typeof manifest !== "object" || manifest === null)
861
+ return false;
862
+ return [
863
+ Reflect.get(manifest, "dependencies"),
864
+ Reflect.get(manifest, "devDependencies"),
865
+ Reflect.get(manifest, "optionalDependencies"),
866
+ Reflect.get(manifest, "peerDependencies")
867
+ ].some((dependencies) => typeof dependencies === "object" && dependencies !== null && Object.hasOwn(dependencies, packageName));
868
+ }, createAbsoluteMobileAuthManifest = (config) => {
869
+ const scheme = config.deepLinkScheme ?? config.appId.toLowerCase();
870
+ return {
871
+ clientId: `absolutejs-native:${config.appId}`,
872
+ issuer: config.productionOrigin,
873
+ redirectUri: `${scheme}://auth/callback`,
874
+ scopes: [...ABSOLUTE_NATIVE_AUTH_SCOPES]
875
+ };
876
+ }, installAbsoluteMobileAuthEnvironment = (projectRoot, config) => {
877
+ const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
878
+ const serialized = serializeAbsoluteMobileAuthEnvironment(config, auth);
879
+ if (serialized === undefined)
880
+ delete process.env[ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV];
881
+ else
882
+ process.env[ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV] = serialized;
883
+ return auth;
884
+ }, projectUsesAbsoluteAuth = (projectRoot) => packageManifestHas(readPackageManifest(projectRoot), ABSOLUTE_AUTH_PACKAGE), projectUsesAbsoluteSync = (projectRoot) => packageManifestHas(readPackageManifest(projectRoot), ABSOLUTE_SYNC_PACKAGE), resolveAbsoluteMobileAuthManifest = (projectRoot, config) => projectUsesAbsoluteAuth(projectRoot) ? createAbsoluteMobileAuthManifest(config) : undefined, serializeAbsoluteMobileAuthEnvironment = (config, auth) => auth === undefined ? undefined : JSON.stringify([
885
+ {
886
+ ...auth,
887
+ name: `${config.appName} native app`
888
+ }
889
+ ]);
890
+ var init_nativeAuth = __esm(() => {
891
+ ABSOLUTE_NATIVE_AUTH_SCOPES = ["openid", "profile"];
892
+ });
893
+
850
894
  // src/mobile/expoProject.ts
851
895
  import {
852
896
  access,
@@ -860,7 +904,7 @@ import {
860
904
  writeFile
861
905
  } from "fs/promises";
862
906
  import { createHash } from "crypto";
863
- import { basename as basename2, dirname as dirname3, join as join5, relative, resolve as resolve3 } from "path";
907
+ import { basename as basename2, dirname as dirname3, join as join6, relative, resolve as resolve3 } from "path";
864
908
  var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
865
909
  `, EXPO_ASSET_EXTENSION = ".absasset", EXPO_PROJECT_MARKER = ".absolutejs-expo-project", exists = async (path) => {
866
910
  try {
@@ -879,8 +923,12 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
879
923
  throw new TypeError(`Expo native route ${route} contains an unsupported segment ${segment}.`);
880
924
  }
881
925
  return segment;
882
- }), routeFile = (project, route) => join5(project, "app", ...routeSegments(route), "index.tsx"), expoPackage = () => ({
926
+ }), routeFile = (project, route) => join6(project, "app", ...routeSegments(route), "index.tsx"), expoPackage = (auth) => ({
883
927
  dependencies: {
928
+ ...auth ? {
929
+ "@absolutejs/auth": ABSOLUTE_EXPO_AUTH_CORE_VERSION,
930
+ [ABSOLUTE_EXPO_AUTH_PACKAGE]: ABSOLUTE_EXPO_AUTH_VERSION
931
+ } : {},
884
932
  expo: "~57.0.9",
885
933
  "expo-asset": "~57.0.15",
886
934
  "expo-constants": "~57.0.16",
@@ -889,6 +937,10 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
889
937
  "expo-haptics": "~57.0.2",
890
938
  "expo-linking": "~57.0.8",
891
939
  "expo-router": "~57.0.17",
940
+ ...auth ? {
941
+ "expo-secure-store": "~57.0.2",
942
+ "expo-web-browser": "~57.0.2"
943
+ } : {},
892
944
  react: "19.2.3",
893
945
  "react-native": "0.86.3",
894
946
  "react-native-safe-area-context": "~5.7.0",
@@ -908,7 +960,7 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
908
960
  start: "expo start --dev-client"
909
961
  },
910
962
  version: "0.0.0"
911
- }), expoAppConfig = (config) => ({
963
+ }), expoAppConfig = (config, auth) => ({
912
964
  expo: {
913
965
  android: {
914
966
  intentFilters: config.deepLinkHosts.map((host) => ({
@@ -928,7 +980,8 @@ var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts
928
980
  name: config.appName,
929
981
  plugins: [
930
982
  "expo-router",
931
- ["expo-dev-client", { launchMode: "most-recent" }]
983
+ ["expo-dev-client", { launchMode: "most-recent" }],
984
+ ...auth ? ["expo-secure-store"] : []
932
985
  ],
933
986
  runtimeVersion: { policy: "appVersion" },
934
987
  scheme: config.deepLinkScheme,
@@ -949,7 +1002,48 @@ config.resolver.nodeModulesPaths = [
949
1002
  config.watchFolders = [appRoot];
950
1003
 
951
1004
  module.exports = config;
952
- `, layoutSource, nativeDiagnosticSource, webHostSource = (config) => {
1005
+ `, layoutSource = (auth) => `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
1006
+ ${auth ? `import { useEffect } from 'react';
1007
+ import { startAbsoluteExpoAuth } from '../src/generated/AbsoluteAuth';` : ""}
1008
+
1009
+ export default function AbsoluteLayout() {
1010
+ ${auth ? `useEffect(() => {
1011
+ void startAbsoluteExpoAuth();
1012
+ }, []);` : ""}
1013
+ return <Stack screenOptions={{ headerShown: false }} />;
1014
+ }
1015
+ `, nativeDiagnosticSource, authRuntimeSource = (auth, appId) => {
1016
+ const storageIdentity = createHash("sha256").update(appId).digest("hex").slice(0, 24);
1017
+ return `${EXPO_GENERATED_HEADER}import { createAbsoluteExpoAuthClient } from '@absolutejs/auth-expo';
1018
+ import { createMobileAuthTransport, installAuthClientRuntimeTransport } from '@absolutejs/auth/client/mobile';
1019
+
1020
+ export const absoluteExpoAuth = createAbsoluteExpoAuthClient({
1021
+ allowedOrigins: [${JSON.stringify(auth.issuer)}],
1022
+ clientId: ${JSON.stringify(auth.clientId)},
1023
+ issuer: ${JSON.stringify(auth.issuer)},
1024
+ redirectUri: ${JSON.stringify(auth.redirectUri)},
1025
+ resource: ${JSON.stringify(auth.issuer)},
1026
+ scopes: ${JSON.stringify(auth.scopes)},
1027
+ storagePrefix: ${JSON.stringify(`absolutejs.auth.${storageIdentity}`)}
1028
+ });
1029
+ installAuthClientRuntimeTransport(createMobileAuthTransport(absoluteExpoAuth, { baseUrl: ${JSON.stringify(auth.issuer)} }));
1030
+
1031
+ let currentPrincipal: Awaited<ReturnType<typeof absoluteExpoAuth.principal>> = null;
1032
+ absoluteExpoAuth.onPrincipalChange(principal => {
1033
+ currentPrincipal = principal;
1034
+ });
1035
+ export const getAbsoluteExpoAuthPrincipal = () => currentPrincipal;
1036
+
1037
+ let startPromise: Promise<void> | undefined;
1038
+ export const startAbsoluteExpoAuth = () => {
1039
+ startPromise ??= absoluteExpoAuth.start().then(async () => {
1040
+ await absoluteExpoAuth.principal();
1041
+ });
1042
+
1043
+ return startPromise;
1044
+ };
1045
+ `;
1046
+ }, webHostSource = (config, auth) => {
953
1047
  const nativeRoutes = [
954
1048
  "/__absolute/native",
955
1049
  ...Object.keys(config.expoNativeRoutes)
@@ -961,8 +1055,9 @@ import { useEffect, useRef, useState } from 'react';
961
1055
  import { ActivityIndicator, BackHandler, Platform, StyleSheet, View } from 'react-native';
962
1056
  import { WebView, type WebViewMessageEvent } from 'react-native-webview';
963
1057
  import { materializeAbsoluteWebBundle } from './webAssets';
1058
+ ${auth ? "import { absoluteExpoAuth, getAbsoluteExpoAuthPrincipal, startAbsoluteExpoAuth } from './AbsoluteAuth';" : ""}
964
1059
 
965
- const BRIDGE_FORMAT = 1;
1060
+ const BRIDGE_FORMAT = 2;
966
1061
  const MAX_MESSAGE_BYTES = 64 * 1024;
967
1062
  const MAX_HTTP_BODY_BYTES = 48 * 1024;
968
1063
  const NATIVE_ROUTES = new Set(${JSON.stringify(nativeRoutes)});
@@ -971,6 +1066,7 @@ const DEV_ORIGIN = Platform.OS === 'android'
971
1066
  ? process.env.EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN
972
1067
  : process.env.EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN;
973
1068
  const HMR_TARGET = Platform.OS === 'android' ? 'expo-android' : 'expo-ios';
1069
+ const AUTH_ENABLED = ${auth ? "true" : "false"};
974
1070
 
975
1071
  const bridgeBootstrap = (path: string) => {
976
1072
  const initialPath = DEV_ORIGIN
@@ -978,6 +1074,7 @@ const bridgeBootstrap = (path: string) => {
978
1074
  : JSON.stringify(path);
979
1075
  return \`(() => {
980
1076
  const pending = new Map();
1077
+ const listeners = new Map();
981
1078
  let sequence = 0;
982
1079
  let currentPath = \${initialPath};
983
1080
  const send = value => {
@@ -987,6 +1084,10 @@ const bridgeBootstrap = (path: string) => {
987
1084
  };
988
1085
  globalThis.__absoluteExpoReceive = source => {
989
1086
  const message = JSON.parse(source);
1087
+ if (message.kind === 'event') {
1088
+ for (const listener of listeners.get(message.event) || []) listener(message.payload || {});
1089
+ return;
1090
+ }
990
1091
  const operation = pending.get(message.id);
991
1092
  if (!operation) return;
992
1093
  pending.delete(message.id);
@@ -994,9 +1095,18 @@ const bridgeBootstrap = (path: string) => {
994
1095
  message.error ? operation.reject(new Error(message.error.message)) : operation.resolve(message.result);
995
1096
  };
996
1097
  globalThis.__absoluteExpoBridge = {
1098
+ on(event, listener) {
1099
+ const values = listeners.get(event) || new Set();
1100
+ values.add(listener);
1101
+ listeners.set(event, values);
1102
+ return () => {
1103
+ values.delete(listener);
1104
+ if (values.size === 0) listeners.delete(event);
1105
+ };
1106
+ },
997
1107
  request(method, params) {
998
1108
  const id = 'web_' + Date.now().toString(36) + '_' + (++sequence).toString(36);
999
- send({ format: 1, id, kind: 'request', method, params, path: currentPath });
1109
+ send({ format: 2, id, kind: 'request', method, params, path: currentPath });
1000
1110
  return new Promise((resolve, reject) => {
1001
1111
  const timer = setTimeout(() => {
1002
1112
  pending.delete(id);
@@ -1007,7 +1117,7 @@ const bridgeBootstrap = (path: string) => {
1007
1117
  },
1008
1118
  setPath(path) {
1009
1119
  currentPath = path;
1010
- send({ format: 1, kind: 'event', event: 'navigation', path });
1120
+ send({ format: 2, kind: 'event', event: 'navigation', path });
1011
1121
  }
1012
1122
  };
1013
1123
  if (\${DEV_ORIGIN ? 'true' : 'false'}) {
@@ -1015,7 +1125,7 @@ const bridgeBootstrap = (path: string) => {
1015
1125
  const path = location.pathname + location.search + location.hash;
1016
1126
  if (path === currentPath) return;
1017
1127
  currentPath = path;
1018
- send({ format: 1, kind: 'event', event: 'navigation', path });
1128
+ send({ format: 2, kind: 'event', event: 'navigation', path });
1019
1129
  };
1020
1130
  for (const method of ['pushState', 'replaceState']) {
1021
1131
  const original = history[method];
@@ -1034,9 +1144,9 @@ const bridgeBootstrap = (path: string) => {
1034
1144
  const url = new URL(anchor.href, location.href);
1035
1145
  if (!${JSON.stringify(nativeRoutes)}.includes(url.pathname)) return;
1036
1146
  event.preventDefault();
1037
- send({ format: 1, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
1147
+ send({ format: 2, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
1038
1148
  }, true);
1039
- send({ format: 1, kind: 'event', event: 'ready', path: currentPath });
1149
+ send({ format: 2, kind: 'event', event: 'ready', path: currentPath });
1040
1150
  })(); true;\`;
1041
1151
  };
1042
1152
 
@@ -1052,16 +1162,16 @@ const impact = async (params: Record<string, unknown>) => {
1052
1162
  };
1053
1163
 
1054
1164
  const bridgeFetch = async (params: Record<string, unknown>) => {
1055
- if (params.method !== 'GET' || typeof params.url !== 'string' || typeof params.headers !== 'object' || params.headers === null || Array.isArray(params.headers)) throw new Error('Expo bridge HTTP request is invalid.');
1165
+ 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.');
1056
1166
  const url = new URL(params.url);
1057
1167
  if (url.origin !== PRODUCTION_ORIGIN || url.username || url.password) throw new Error('Expo bridge HTTP request left productionOrigin.');
1058
1168
  const headers = new Headers();
1059
1169
  for (const [name, value] of Object.entries(params.headers as Record<string, unknown>)) {
1060
1170
  const normalized = name.toLowerCase();
1061
- if (typeof value !== 'string' || (normalized !== 'accept' && !normalized.startsWith('x-absolute-mobile-'))) throw new Error('Expo bridge HTTP header is not allowed.');
1171
+ if (typeof value !== 'string' || (normalized !== 'accept' && normalized !== 'content-type' && !normalized.startsWith('x-absolute-mobile-'))) throw new Error('Expo bridge HTTP header is not allowed.');
1062
1172
  headers.set(normalized, value);
1063
1173
  }
1064
- const response = await fetch(url, { headers, method: 'GET', redirect: 'manual' });
1174
+ const response = await ${auth ? "absoluteExpoAuth.fetchOptional" : "fetch"}(url, { ...(typeof params.body === 'string' ? { body: params.body } : {}), headers, method: params.method, redirect: 'manual' });
1065
1175
  if (new URL(response.url || url.href).origin !== PRODUCTION_ORIGIN || response.status >= 300 && response.status < 400) throw new Error('Expo bridge HTTP redirects are not allowed.');
1066
1176
  const body = await response.text();
1067
1177
  if (new TextEncoder().encode(body).byteLength > MAX_HTTP_BODY_BYTES) throw new Error('Expo bridge HTTP response exceeds 48 KiB.');
@@ -1073,13 +1183,32 @@ const bridgeFetch = async (params: Record<string, unknown>) => {
1073
1183
  return { body, headers: responseHeaders, status: response.status };
1074
1184
  };
1075
1185
 
1186
+ const authStatus = async () => {
1187
+ ${auth ? `const user = await absoluteExpoAuth.status();
1188
+ const principal = getAbsoluteExpoAuthPrincipal();
1189
+ return { principal: principal ? { namespace: principal.namespace } : null, user };` : "throw new Error('Expo Auth is not configured.');"}
1190
+ };
1191
+
1076
1192
  export function AbsoluteWebHost() {
1077
1193
  const pathname = usePathname() || '/';
1078
1194
  const webView = useRef<WebView>(null);
1079
1195
  const [indexUri, setIndexUri] = useState<string>();
1080
1196
  const [canGoBack, setCanGoBack] = useState(false);
1197
+ const [authReady, setAuthReady] = useState(!AUTH_ENABLED);
1081
1198
  const activeWebPath = useRef(pathname);
1082
1199
 
1200
+ useEffect(() => {
1201
+ if (!AUTH_ENABLED) return;
1202
+ let active = true;
1203
+ ${auth ? `const stop = absoluteExpoAuth.onPrincipalChange(principal => {
1204
+ if (!active) return;
1205
+ const source = JSON.stringify({ event: 'auth.principal', format: BRIDGE_FORMAT, kind: 'event', path: activeWebPath.current, payload: { principal: principal ? { namespace: principal.namespace } : null } });
1206
+ webView.current?.injectJavaScript(\`globalThis.__absoluteExpoReceive(\${JSON.stringify(source)}); true;\`);
1207
+ });
1208
+ void startAbsoluteExpoAuth().then(() => { if (active) setAuthReady(true); });
1209
+ return () => { active = false; stop(); };` : ""}
1210
+ }, []);
1211
+
1083
1212
  useEffect(() => {
1084
1213
  if (DEV_ORIGIN) {
1085
1214
  const target = new URL(pathname, DEV_ORIGIN);
@@ -1129,6 +1258,16 @@ export function AbsoluteWebHost() {
1129
1258
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: null });
1130
1259
  } else if (message.method === 'http.fetch') {
1131
1260
  respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await bridgeFetch(message.params as Record<string, unknown>) });
1261
+ } else if (message.method === 'auth.signIn') {
1262
+ ${auth ? `const params = message.params as Record<string, unknown>;
1263
+ if (typeof params.email !== 'string' || typeof params.signup !== 'boolean') throw new Error('Expo Auth sign-in params are invalid.');
1264
+ await absoluteExpoAuth.signIn({ authorizationParameters: { login_hint: params.email, ...(params.signup ? { screen_hint: 'signup' } : {}) } });
1265
+ respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });` : "throw new Error('Expo Auth is not configured.');"}
1266
+ } else if (message.method === 'auth.signOut') {
1267
+ ${auth ? `await absoluteExpoAuth.signOut();
1268
+ respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: null });` : "throw new Error('Expo Auth is not configured.');"}
1269
+ } else if (message.method === 'auth.status') {
1270
+ respond({ format: BRIDGE_FORMAT, id: message.id, kind: 'response', result: await authStatus() });
1132
1271
  } else {
1133
1272
  throw new Error('Expo bridge method is not allowed.');
1134
1273
  }
@@ -1137,7 +1276,7 @@ export function AbsoluteWebHost() {
1137
1276
  }
1138
1277
  };
1139
1278
 
1140
- if (!indexUri) return <View style={styles.loading}><ActivityIndicator /></View>;
1279
+ if (!indexUri || !authReady) return <View style={styles.loading}><ActivityIndicator /></View>;
1141
1280
  return <WebView
1142
1281
  allowFileAccess
1143
1282
  allowFileAccessFromFileURLs
@@ -1160,9 +1299,17 @@ export function AbsoluteWebHost() {
1160
1299
  const styles = StyleSheet.create({ loading: { alignItems: 'center', flex: 1, justifyContent: 'center' }, web: { flex: 1 } });
1161
1300
  `;
1162
1301
  }, webRouteSource, catchAllRouteSource, nativeWrapperSource = (wrapper, module) => `${EXPO_GENERATED_HEADER}export { default } from ${JSON.stringify(portableRelative(dirname3(wrapper), module).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
1163
- `, expoTsConfig = (projectRoot, project) => ({
1302
+ `, expoTsConfig = (projectRoot, project, auth) => ({
1164
1303
  compilerOptions: {
1165
1304
  paths: {
1305
+ ...auth ? {
1306
+ "@absolutejs/auth": [
1307
+ "./node_modules/@absolutejs/auth/dist/index.d.ts"
1308
+ ],
1309
+ "@absolutejs/auth/*": [
1310
+ "./node_modules/@absolutejs/auth/dist/*"
1311
+ ]
1312
+ } : {},
1166
1313
  "*": [
1167
1314
  "./node_modules/*",
1168
1315
  `${portableRelative(project, projectRoot)}/node_modules/*`
@@ -1193,6 +1340,7 @@ const styles = StyleSheet.create({ loading: { alignItems: 'center', flex: 1, jus
1193
1340
  throw new TypeError("Expo project generation requires mobile.engine: expo.");
1194
1341
  const projectRoot = resolve3(options.projectRoot);
1195
1342
  const project = config.nativeProjectDirectory;
1343
+ const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
1196
1344
  const routeModules = Object.entries(config.expoNativeRoutes);
1197
1345
  const moduleChecks = await Promise.all(routeModules.map(async ([route, module]) => ({
1198
1346
  exists: await exists(module),
@@ -1203,7 +1351,7 @@ const styles = StyleSheet.create({ loading: { alignItems: 'center', flex: 1, jus
1203
1351
  if (missing) {
1204
1352
  throw new TypeError(`Expo native route ${missing.route} references missing module ${missing.module}.`);
1205
1353
  }
1206
- const marker = join5(project, EXPO_PROJECT_MARKER);
1354
+ const marker = join6(project, EXPO_PROJECT_MARKER);
1207
1355
  if (await exists(project) && !await exists(marker)) {
1208
1356
  const entries = await readdir(project);
1209
1357
  if (entries.length > 0 && !options.force) {
@@ -1213,47 +1361,54 @@ const styles = StyleSheet.create({ loading: { alignItems: 'center', flex: 1, jus
1213
1361
  await mkdir(project, { recursive: true });
1214
1362
  await writeFile(marker, `format=1
1215
1363
  `);
1364
+ const authEnabled = Boolean(auth);
1216
1365
  const files = new Map([
1217
1366
  [
1218
- join5(project, ".gitignore"),
1367
+ join6(project, ".gitignore"),
1219
1368
  `.expo/
1220
1369
  android/
1221
1370
  ios/
1222
1371
  node_modules/
1223
1372
  `
1224
1373
  ],
1225
- [join5(project, "app.json"), jsonSource(expoAppConfig(config))],
1226
- [join5(project, "app.config.js"), expoDynamicAppConfig],
1227
- [join5(project, "package.json"), jsonSource(expoPackage())],
1228
- [join5(project, "metro.config.js"), metroConfig(projectRoot)],
1229
1374
  [
1230
- join5(project, "plugins", "withAbsoluteDevelopmentCa.js"),
1375
+ join6(project, "app.json"),
1376
+ jsonSource(expoAppConfig(config, authEnabled))
1377
+ ],
1378
+ [join6(project, "app.config.js"), expoDynamicAppConfig],
1379
+ [join6(project, "package.json"), jsonSource(expoPackage(authEnabled))],
1380
+ [join6(project, "metro.config.js"), metroConfig(projectRoot)],
1381
+ [
1382
+ join6(project, "plugins", "withAbsoluteDevelopmentCa.js"),
1231
1383
  expoDevelopmentCaPlugin
1232
1384
  ],
1233
1385
  [
1234
- join5(project, "tsconfig.json"),
1235
- jsonSource(expoTsConfig(projectRoot, project))
1386
+ join6(project, "tsconfig.json"),
1387
+ jsonSource(expoTsConfig(projectRoot, project, authEnabled))
1236
1388
  ],
1237
- [join5(project, "app", "_layout.tsx"), layoutSource],
1389
+ [join6(project, "app", "_layout.tsx"), layoutSource(authEnabled)],
1238
1390
  [
1239
- join5(project, "app", "__absolute", "native", "index.tsx"),
1391
+ join6(project, "app", "__absolute", "native", "index.tsx"),
1240
1392
  nativeDiagnosticSource
1241
1393
  ],
1242
1394
  [
1243
- join5(project, "src", "generated", "AbsoluteWebHost.tsx"),
1244
- webHostSource(config)
1395
+ join6(project, "src", "generated", "AbsoluteWebHost.tsx"),
1396
+ webHostSource(config, auth)
1245
1397
  ]
1246
1398
  ]);
1247
- const webAssetsPath = join5(project, "src", "generated", "webAssets.ts");
1399
+ if (auth) {
1400
+ files.set(join6(project, "src", "generated", "AbsoluteAuth.ts"), authRuntimeSource(auth, config.appId));
1401
+ }
1402
+ const webAssetsPath = join6(project, "src", "generated", "webAssets.ts");
1248
1403
  if (!await exists(webAssetsPath)) {
1249
1404
  files.set(webAssetsPath, emptyWebAssetsSource);
1250
1405
  }
1251
1406
  if (!config.expoNativeRoutes["/"]) {
1252
- files.set(join5(project, "app", "index.tsx"), webRouteSource);
1407
+ files.set(join6(project, "app", "index.tsx"), webRouteSource);
1253
1408
  }
1254
- files.set(join5(project, "app", "[...absolute].tsx"), catchAllRouteSource);
1409
+ files.set(join6(project, "app", "[...absolute].tsx"), catchAllRouteSource);
1255
1410
  for (const [route, module] of routeModules) {
1256
- const wrapper = route === "/" ? join5(project, "app", "index.tsx") : routeFile(project, route);
1411
+ const wrapper = route === "/" ? join6(project, "app", "index.tsx") : routeFile(project, route);
1257
1412
  files.set(wrapper, nativeWrapperSource(wrapper, module));
1258
1413
  }
1259
1414
  const changes = await Promise.all([...files].map(([path, source]) => writeManagedFile(path, source, true)));
@@ -1262,7 +1417,7 @@ node_modules/
1262
1417
  }, walkFiles = async (root, directory = root) => {
1263
1418
  const entries = await readdir(directory, { withFileTypes: true });
1264
1419
  const nested = await Promise.all(entries.map((entry) => {
1265
- const path = join5(directory, entry.name);
1420
+ const path = join6(directory, entry.name);
1266
1421
  if (entry.isDirectory())
1267
1422
  return walkFiles(root, path);
1268
1423
  if (entry.isFile())
@@ -1323,11 +1478,11 @@ export const materializeAbsoluteWebBundle = async () => {
1323
1478
  `, syncAbsoluteExpoWebAssets = async (config) => {
1324
1479
  if (config.engine !== "expo")
1325
1480
  throw new TypeError("Expo asset sync requires mobile.engine: expo.");
1326
- const marker = join5(config.nativeProjectDirectory, EXPO_PROJECT_MARKER);
1481
+ const marker = join6(config.nativeProjectDirectory, EXPO_PROJECT_MARKER);
1327
1482
  if (!await exists(marker)) {
1328
1483
  throw new TypeError("Expo asset sync requires an AbsoluteJS-managed Expo project. Run mobile init first.");
1329
1484
  }
1330
- const manifestPath = join5(config.bundleDirectory, "absolute-mobile-manifest.json");
1485
+ const manifestPath = join6(config.bundleDirectory, "absolute-mobile-manifest.json");
1331
1486
  const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1332
1487
  const appBuild = typeof manifest === "object" && manifest !== null && typeof Reflect.get(manifest, "appBuild") === "string" ? String(Reflect.get(manifest, "appBuild")) : undefined;
1333
1488
  if (!appBuild)
@@ -1342,16 +1497,16 @@ export const materializeAbsoluteWebBundle = async () => {
1342
1497
  bundleHash.update("\x00");
1343
1498
  });
1344
1499
  const bundleId = `amexpo_${bundleHash.digest("hex")}`;
1345
- const destination = join5(config.nativeProjectDirectory, "assets", "absolute");
1500
+ const destination = join6(config.nativeProjectDirectory, "assets", "absolute");
1346
1501
  await mkdir(dirname3(destination), { recursive: true });
1347
- const staging = await mkdtemp(join5(dirname3(destination), `.${basename2(destination)}.stage-`));
1502
+ const staging = await mkdtemp(join6(dirname3(destination), `.${basename2(destination)}.stage-`));
1348
1503
  let assets;
1349
1504
  try {
1350
1505
  assets = await Promise.all(files.map(async (source, index) => {
1351
1506
  const name = `${String(index).padStart(6, "0")}${EXPO_ASSET_EXTENSION}`;
1352
- await cp(source, join5(staging, name));
1507
+ await cp(source, join6(staging, name));
1353
1508
  return {
1354
- asset: portableRelative(join5(config.nativeProjectDirectory, "src", "generated"), join5(destination, name)),
1509
+ asset: portableRelative(join6(config.nativeProjectDirectory, "src", "generated"), join6(destination, name)),
1355
1510
  path: relative(config.bundleDirectory, source).replaceAll("\\", "/")
1356
1511
  };
1357
1512
  }));
@@ -1360,11 +1515,12 @@ export const materializeAbsoluteWebBundle = async () => {
1360
1515
  await rm(staging, { force: true, recursive: true });
1361
1516
  throw error;
1362
1517
  }
1363
- const generated = join5(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
1518
+ const generated = join6(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
1364
1519
  await writeManagedFile(generated, assetModuleSource(assets, bundleId), true);
1365
1520
  return { appBuild, assets: assets.length, bundleId, path: destination };
1366
1521
  };
1367
1522
  var init_expoProject = __esm(() => {
1523
+ init_nativeAuth();
1368
1524
  expoDynamicAppConfig = `${EXPO_GENERATED_HEADER}const config = require('./app.json');
1369
1525
 
1370
1526
  if (process.env.ABSOLUTE_EXPO_DEVELOPMENT === '1' && process.env.ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH) {
@@ -1415,12 +1571,6 @@ const withAbsoluteDevelopmentCa = config => {
1415
1571
  };
1416
1572
 
1417
1573
  module.exports = withAbsoluteDevelopmentCa;
1418
- `;
1419
- layoutSource = `${EXPO_GENERATED_HEADER}import { Stack } from 'expo-router';
1420
-
1421
- export default function AbsoluteLayout() {
1422
- return <Stack screenOptions={{ headerShown: false }} />;
1423
- }
1424
1574
  `;
1425
1575
  nativeDiagnosticSource = `${EXPO_GENERATED_HEADER}import * as Haptics from 'expo-haptics';
1426
1576
  import { Link } from 'expo-router';
@@ -1558,50 +1708,6 @@ var closeServer = (server) => new Promise((resolve4, reject) => {
1558
1708
  };
1559
1709
  var init_iosPhysicalDeviceTransport = () => {};
1560
1710
 
1561
- // src/mobile/nativeAuth.ts
1562
- import { readFileSync as readFileSync5 } from "fs";
1563
- import { join as join7 } from "path";
1564
- var ABSOLUTE_AUTH_PACKAGE = "@absolutejs/auth", ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV = "ABSOLUTE_AUTH_NATIVE_CLIENTS", ABSOLUTE_NATIVE_AUTH_SCOPES, ABSOLUTE_SYNC_PACKAGE = "@absolutejs/sync", readPackageManifest = (projectRoot) => {
1565
- try {
1566
- return JSON.parse(readFileSync5(join7(projectRoot, "package.json"), "utf8"));
1567
- } catch {
1568
- return;
1569
- }
1570
- }, packageManifestHas = (manifest, packageName) => {
1571
- if (typeof manifest !== "object" || manifest === null)
1572
- return false;
1573
- return [
1574
- Reflect.get(manifest, "dependencies"),
1575
- Reflect.get(manifest, "devDependencies"),
1576
- Reflect.get(manifest, "optionalDependencies"),
1577
- Reflect.get(manifest, "peerDependencies")
1578
- ].some((dependencies) => typeof dependencies === "object" && dependencies !== null && Object.hasOwn(dependencies, packageName));
1579
- }, createAbsoluteMobileAuthManifest = (config) => {
1580
- const scheme = config.deepLinkScheme ?? config.appId.toLowerCase();
1581
- return {
1582
- clientId: `absolutejs-native:${config.appId}`,
1583
- issuer: config.productionOrigin,
1584
- redirectUri: `${scheme}://auth/callback`,
1585
- scopes: [...ABSOLUTE_NATIVE_AUTH_SCOPES]
1586
- };
1587
- }, installAbsoluteMobileAuthEnvironment = (projectRoot, config) => {
1588
- const auth = resolveAbsoluteMobileAuthManifest(projectRoot, config);
1589
- const serialized = serializeAbsoluteMobileAuthEnvironment(config, auth);
1590
- if (serialized === undefined)
1591
- delete process.env[ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV];
1592
- else
1593
- process.env[ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV] = serialized;
1594
- return auth;
1595
- }, projectUsesAbsoluteAuth = (projectRoot) => packageManifestHas(readPackageManifest(projectRoot), ABSOLUTE_AUTH_PACKAGE), projectUsesAbsoluteSync = (projectRoot) => packageManifestHas(readPackageManifest(projectRoot), ABSOLUTE_SYNC_PACKAGE), resolveAbsoluteMobileAuthManifest = (projectRoot, config) => projectUsesAbsoluteAuth(projectRoot) ? createAbsoluteMobileAuthManifest(config) : undefined, serializeAbsoluteMobileAuthEnvironment = (config, auth) => auth === undefined ? undefined : JSON.stringify([
1596
- {
1597
- ...auth,
1598
- name: `${config.appName} native app`
1599
- }
1600
- ]);
1601
- var init_nativeAuth = __esm(() => {
1602
- ABSOLUTE_NATIVE_AUTH_SCOPES = ["openid", "profile"];
1603
- });
1604
-
1605
1711
  // src/cli/utils.ts
1606
1712
  var {$: $2 } = globalThis.Bun;
1607
1713
  import { execSync } from "child_process";
@@ -4488,7 +4594,7 @@ var init_iosSimulatorController = __esm(() => {
4488
4594
  });
4489
4595
 
4490
4596
  // src/mobile/remoteMacWire.ts
4491
- var ABSOLUTE_REMOTE_MAC_EVENT_PREFIX = "ABSOLUTE_REMOTE_MAC\t", ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION = 1;
4597
+ var ABSOLUTE_REMOTE_MAC_EVENT_PREFIX = "ABSOLUTE_REMOTE_MAC\t", ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION = 2;
4492
4598
 
4493
4599
  // src/mobile/remoteMacProtocol.ts
4494
4600
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
@@ -4504,7 +4610,7 @@ import {
4504
4610
  resolve as resolvePath,
4505
4611
  sep as sep4
4506
4612
  } from "path";
4507
- var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTINATION, defaultProfilePath = () => join13(homedir4(), ".absolutejs", "mobile", "remote-macs.json"), emptyStore = () => ({
4613
+ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, REMOTE_SESSION_CLOSE_TIMEOUT_MS = 2000, PROFILE_NAME, SSH_DESTINATION, defaultProfilePath = () => join13(homedir4(), ".absolutejs", "mobile", "remote-macs.json"), emptyStore = () => ({
4508
4614
  format: PROFILE_FORMAT,
4509
4615
  profiles: {}
4510
4616
  }), loadStore = async (path = defaultProfilePath()) => {
@@ -4675,7 +4781,11 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4675
4781
  }
4676
4782
  await saveStore(store, profilePath);
4677
4783
  return true;
4678
- }, projectIdentity = (projectRoot, appId) => createHash6("sha256").update(`${resolvePath(projectRoot)}\x00${appId}`).digest("hex").slice(0, 20), createAbsoluteRemoteIosDevProject = (config, projectRoot, profile) => ({
4784
+ }, projectIdentity = (projectRoot, appId) => createHash6("sha256").update(`${resolvePath(projectRoot)}\x00${appId}`).digest("hex").slice(0, 20), createAbsoluteRemoteExpoIosDevProject = (config, projectRoot, profile) => {
4785
+ if (config.engine !== "expo")
4786
+ throw new TypeError("Remote Expo execution requires mobile.engine: expo.");
4787
+ return createAbsoluteRemoteIosDevProject(config, projectRoot, profile);
4788
+ }, createAbsoluteRemoteIosDevProject = (config, projectRoot, profile) => ({
4679
4789
  cap: join13(resolvePath(projectRoot), "node_modules", ".bin", "cap"),
4680
4790
  config,
4681
4791
  nativeDirectory: join13(config.nativeProjectDirectory, "ios"),
@@ -4762,6 +4872,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4762
4872
  appId: project.config.appId,
4763
4873
  appName: project.config.appName,
4764
4874
  bundleDirectory: portableRelativePath(project.projectRoot, project.config.bundleDirectory),
4875
+ engine: project.config.engine,
4765
4876
  ...project.config.deepLinkScheme || project.config.deepLinkHosts.length > 1 || project.config.appleAppIdPrefix ? {
4766
4877
  deepLinks: {
4767
4878
  ...project.config.deepLinkScheme ? { scheme: project.config.deepLinkScheme } : {},
@@ -4774,6 +4885,16 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4774
4885
  }
4775
4886
  } : {},
4776
4887
  entry: project.config.entry,
4888
+ ...project.config.engine === "expo" ? {
4889
+ expo: { sdkVersion: project.config.expoSdkVersion ?? 57 },
4890
+ routes: {
4891
+ default: "web",
4892
+ native: Object.fromEntries(Object.entries(project.config.expoNativeRoutes).map(([route, module]) => [
4893
+ route,
4894
+ portableRelativePath(project.projectRoot, module)
4895
+ ]))
4896
+ }
4897
+ } : {},
4777
4898
  ...project.config.iosVersion ? { ios: { version: project.config.iosVersion } } : {},
4778
4899
  nativeProject: {
4779
4900
  directory: portableRelativePath(project.projectRoot, project.config.nativeProjectDirectory),
@@ -4861,7 +4982,30 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4861
4982
  } finally {
4862
4983
  reader.releaseLock();
4863
4984
  }
4864
- }, startAbsoluteRemoteIosDevSession = async (options) => {
4985
+ }, startAbsoluteRemoteExpoIosDevSession = async (options) => {
4986
+ const session = await startAbsoluteRemoteDevSession({
4987
+ certificateAuthorityPath: options.certificateAuthorityPath,
4988
+ deviceIdentifier: options.deviceIdentifier,
4989
+ https: options.https,
4990
+ installAgent: options.installAgent,
4991
+ log: options.log,
4992
+ metroPort: options.metroPort,
4993
+ onExpoPhaseTiming: options.onPhaseTiming,
4994
+ onExpoStateChange: options.onStateChange,
4995
+ port: options.port,
4996
+ project: options.project,
4997
+ serverHost: options.serverHost,
4998
+ signal: options.signal,
4999
+ syncProject: options.syncProject,
5000
+ transport: options.transport
5001
+ });
5002
+ return {
5003
+ close: session.close,
5004
+ metroPort: options.metroPort,
5005
+ platforms: ["ios"],
5006
+ timings: session.timings
5007
+ };
5008
+ }, startAbsoluteRemoteIosDevSession = (options) => startAbsoluteRemoteDevSession(options), startAbsoluteRemoteDevSession = async (options) => {
4865
5009
  const startedAt = performance.now();
4866
5010
  const transport = options.transport ?? defaultTransport;
4867
5011
  const installAgent = options.installAgent ?? installAbsoluteRemoteMacAgent;
@@ -4875,9 +5019,22 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4875
5019
  const encodedConfig = Buffer.from(JSON.stringify(portableMobileConfig(options.project))).toString("base64url");
4876
5020
  const encodedCertificateAuthority = options.certificateAuthorityPath ? (await readFile8(options.certificateAuthorityPath)).toString("base64url") : undefined;
4877
5021
  const physicalDevice = options.deviceIdentifier !== undefined;
4878
- let relayPort;
4879
- if (physicalDevice)
4880
- relayPort = options.port <= 49151 ? options.port + 16384 : options.port - 16384;
5022
+ const expo = options.project.config.engine === "expo";
5023
+ if (expo && options.metroPort === undefined)
5024
+ throw new TypeError("Remote Expo execution requires a Metro port.");
5025
+ if (options.metroPort === options.port)
5026
+ throw new TypeError("Expo Metro and AbsoluteJS must use different ports.");
5027
+ const translatedRelayPort = (sourcePort) => sourcePort <= 49151 ? sourcePort + 16384 : sourcePort - 16384;
5028
+ const occupiedRemotePorts = new Set([options.port, options.metroPort].filter((value) => value !== undefined));
5029
+ const reserveRelayPort = (sourcePort) => {
5030
+ let candidate = translatedRelayPort(sourcePort);
5031
+ while (occupiedRemotePorts.has(candidate))
5032
+ candidate = candidate === 65535 ? 1024 : candidate + 1;
5033
+ occupiedRemotePorts.add(candidate);
5034
+ return candidate;
5035
+ };
5036
+ const relayPort = physicalDevice ? reserveRelayPort(options.port) : undefined;
5037
+ const metroRelayPort = physicalDevice && options.metroPort !== undefined ? reserveRelayPort(options.metroPort) : undefined;
4881
5038
  if (physicalDevice && !options.serverHost)
4882
5039
  throw new Error("Remote physical iOS development requires the Remote Mac LAN host.");
4883
5040
  const remoteCommand = [
@@ -4890,6 +5047,7 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4890
5047
  String(options.port),
4891
5048
  "--mobile-config",
4892
5049
  shellQuote(encodedConfig),
5050
+ ...expo && options.metroPort !== undefined ? ["--metro-port", String(options.metroPort)] : [],
4893
5051
  ...encodedCertificateAuthority ? [
4894
5052
  "--certificate-authority",
4895
5053
  shellQuote(encodedCertificateAuthority)
@@ -4901,7 +5059,8 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4901
5059
  "--server-host",
4902
5060
  shellQuote(options.serverHost ?? ""),
4903
5061
  "--relay-port",
4904
- String(relayPort)
5062
+ String(relayPort),
5063
+ ...expo && metroRelayPort !== undefined ? ["--metro-relay-port", String(metroRelayPort)] : []
4905
5064
  ] : []
4906
5065
  ].join(" ");
4907
5066
  const command = [
@@ -4910,6 +5069,10 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4910
5069
  "ExitOnForwardFailure=yes",
4911
5070
  "-R",
4912
5071
  physicalDevice ? `127.0.0.1:${relayPort}:127.0.0.1:${options.port}` : `${options.port}:127.0.0.1:${options.port}`,
5072
+ ...expo && options.metroPort !== undefined ? [
5073
+ "-R",
5074
+ physicalDevice ? `127.0.0.1:${metroRelayPort}:127.0.0.1:${options.metroPort}` : `${options.metroPort}:127.0.0.1:${options.metroPort}`
5075
+ ] : [],
4913
5076
  "/bin/sh -lc",
4914
5077
  shellQuote(remoteCommand)
4915
5078
  ];
@@ -4935,11 +5098,19 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4935
5098
  if (event.type === "native-log")
4936
5099
  options.nativeLog?.(event.entry);
4937
5100
  if (event.type === "state") {
4938
- ({ state } = event);
4939
- options.onStateChange?.(state);
5101
+ if (expo)
5102
+ options.onExpoStateChange?.(event.state);
5103
+ else {
5104
+ state = event.state;
5105
+ options.onStateChange?.(state);
5106
+ }
5107
+ }
5108
+ if (event.type === "timing") {
5109
+ if (expo)
5110
+ options.onExpoPhaseTiming?.(event);
5111
+ else
5112
+ options.onPhaseTiming?.(event);
4940
5113
  }
4941
- if (event.type === "timing")
4942
- options.onPhaseTiming?.(event);
4943
5114
  if (event.type === "ready") {
4944
5115
  ready = event;
4945
5116
  resolveReady();
@@ -4981,9 +5152,26 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4981
5152
  pending.clear();
4982
5153
  return;
4983
5154
  });
4984
- await readyPromise;
5155
+ try {
5156
+ await readyPromise;
5157
+ } catch (error) {
5158
+ process2.stdin.end();
5159
+ process2.kill();
5160
+ await process2.exited.catch(() => {
5161
+ return;
5162
+ });
5163
+ throw error;
5164
+ }
4985
5165
  if (!ready)
4986
5166
  throw fatal ?? new Error("Remote Mac did not become ready.");
5167
+ if (ready.engine !== options.project.config.engine) {
5168
+ process2.stdin.end();
5169
+ process2.kill();
5170
+ await process2.exited.catch(() => {
5171
+ return;
5172
+ });
5173
+ throw new Error("Remote Mac executor returned the wrong mobile engine.");
5174
+ }
4987
5175
  const totalDuration = performance.now() - startedAt;
4988
5176
  let currentReady = {
4989
5177
  ...ready,
@@ -4995,11 +5183,11 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
4995
5183
  total: totalDuration
4996
5184
  }
4997
5185
  };
4998
- options.log?.(`Remote Mac connected (${options.project.profile.name}); agent ${agent.uploaded ? "uploaded" : "cache hit"}, project synced, and iOS ready in ${totalDuration.toFixed(2)}ms.`);
5186
+ options.log?.(`Remote Mac connected (${options.project.profile.name}); agent ${agent.uploaded ? "uploaded" : "cache hit"}, project synced, and ${expo ? "Expo " : ""}iOS ready in ${totalDuration.toFixed(2)}ms.`);
4999
5187
  const request = (commandName) => {
5000
5188
  const id = randomUUID4();
5001
5189
  const response = new Promise((resolve9, reject) => pending.set(id, { reject, resolve: resolve9 }));
5002
- process2.stdin.write(`${JSON.stringify({ command: commandName, id, v: 1 })}
5190
+ process2.stdin.write(`${JSON.stringify({ command: commandName, id, v: ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION })}
5003
5191
  `);
5004
5192
  const flush = async () => {
5005
5193
  for (let attempt = 0;attempt < REMOTE_STDIN_FLUSH_ATTEMPTS; attempt++) {
@@ -5018,13 +5206,24 @@ var PROFILE_FORMAT = 1, REMOTE_STDIN_FLUSH_ATTEMPTS = 3, PROFILE_NAME, SSH_DESTI
5018
5206
  if (closed)
5019
5207
  return;
5020
5208
  closed = true;
5021
- await request("close").catch(() => {
5022
- return;
5023
- });
5209
+ const timeout = () => new Promise((resolve9) => setTimeout(() => resolve9("timeout"), REMOTE_SESSION_CLOSE_TIMEOUT_MS));
5210
+ await Promise.race([
5211
+ request("close").catch(() => {
5212
+ return;
5213
+ }),
5214
+ timeout()
5215
+ ]);
5024
5216
  process2.stdin.end();
5025
- await process2.exited.catch(() => {
5026
- return;
5027
- });
5217
+ const exit = await Promise.race([
5218
+ process2.exited.then(() => "exited").catch(() => "exited"),
5219
+ timeout()
5220
+ ]);
5221
+ if (exit === "timeout") {
5222
+ process2.kill();
5223
+ await process2.exited.catch(() => {
5224
+ return;
5225
+ });
5226
+ }
5028
5227
  };
5029
5228
  const makeSession = () => ({
5030
5229
  close,
@@ -5085,13 +5284,13 @@ var init_remoteMacProtocol = __esm(() => {
5085
5284
  // src/dev/devCert.ts
5086
5285
  var exports_devCert = {};
5087
5286
  __export(exports_devCert, {
5088
- setupMkcert: () => setupMkcert,
5089
- normalizeDevCertificateHosts: () => normalizeDevCertificateHosts,
5090
- loadDevCert: () => loadDevCert,
5091
- hasMkcert: () => hasMkcert,
5092
- hasCert: () => hasCert,
5287
+ ensureDevCert: () => ensureDevCert,
5093
5288
  getDevCertificateAuthorityPath: () => getDevCertificateAuthorityPath,
5094
- ensureDevCert: () => ensureDevCert
5289
+ hasCert: () => hasCert,
5290
+ hasMkcert: () => hasMkcert,
5291
+ loadDevCert: () => loadDevCert,
5292
+ normalizeDevCertificateHosts: () => normalizeDevCertificateHosts,
5293
+ setupMkcert: () => setupMkcert
5095
5294
  });
5096
5295
  import {
5097
5296
  copyFileSync,
@@ -5399,13 +5598,13 @@ var init_devCert = __esm(() => {
5399
5598
  // src/cli/scripts/eslintChunked.ts
5400
5599
  var exports_eslintChunked = {};
5401
5600
  __export(exports_eslintChunked, {
5402
- upstreamRef: () => upstreamRef,
5403
- ruleSummary: () => ruleSummary,
5404
- resolveLintTargets: () => resolveLintTargets,
5405
- parseChunkedArgs: () => parseChunkedArgs,
5406
- gitVisibleFiles: () => gitVisibleFiles,
5601
+ eslintChunked: () => eslintChunked,
5407
5602
  gitChangedFiles: () => gitChangedFiles,
5408
- eslintChunked: () => eslintChunked
5603
+ gitVisibleFiles: () => gitVisibleFiles,
5604
+ parseChunkedArgs: () => parseChunkedArgs,
5605
+ resolveLintTargets: () => resolveLintTargets,
5606
+ ruleSummary: () => ruleSummary,
5607
+ upstreamRef: () => upstreamRef
5409
5608
  });
5410
5609
  import { existsSync as existsSync6 } from "fs";
5411
5610
  import { relative as relative7, resolve as resolve9 } from "path";
@@ -7051,6 +7250,11 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
7051
7250
  if (candidate)
7052
7251
  return candidate;
7053
7252
  throw new TypeError("AbsoluteJS mobile auth shell module is missing.");
7253
+ }, shellExpoAuthModule = () => {
7254
+ const candidate = ["js", "ts"].map((extension) => join19(import.meta.dir, `shellExpoAuth.${extension}`)).find(existsSync9);
7255
+ if (candidate)
7256
+ return candidate;
7257
+ throw new TypeError("AbsoluteJS Expo auth bridge module is missing.");
7054
7258
  }, shellSyncModule = () => {
7055
7259
  const candidate = ["js", "ts"].map((extension) => join19(import.meta.dir, `shellSync.${extension}`)).find(existsSync9);
7056
7260
  if (candidate)
@@ -7124,9 +7328,10 @@ var MANIFEST_FILE = "absolute-mobile-manifest.json", BOOTSTRAP_FILE = "absolute-
7124
7328
  const capacitor = engine !== "expo";
7125
7329
  const shellCapabilities = capacitor ? deviceCapabilities.capabilities : [];
7126
7330
  const modulePath = shellBootstrapModule();
7127
- const authImport = auth ? `import { createAbsoluteMobileShellAuth } from ${JSON.stringify(shellAuthModule())};
7331
+ const authFactory = capacitor ? "createAbsoluteMobileShellAuth" : "createAbsoluteExpoShellAuth";
7332
+ const authImport = auth ? `import { ${authFactory} } from ${JSON.stringify(capacitor ? shellAuthModule() : shellExpoAuthModule())};
7128
7333
  ` : "";
7129
- const options = auth ? `{ createAuth: createAbsoluteMobileShellAuth${sync ? ", installSync: installAbsoluteMobileShellSync" : ""} }` : "";
7334
+ const options = auth ? `{ createAuth: ${authFactory}${sync ? ", installSync: installAbsoluteMobileShellSync" : ""} }` : "";
7130
7335
  const syncImport = sync ? `import { installAbsoluteMobileShellSync } from ${JSON.stringify(shellSyncModule())};
7131
7336
  ` : "";
7132
7337
  const pushIndex = shellCapabilities.indexOf("pushNotifications");
@@ -7148,7 +7353,7 @@ const absoluteMobilePushCapability = absoluteDeviceCapability${pushIndex}(absolu
7148
7353
  const baseAdapterModule = capacitor ? await resolveProjectImport(projectRoot, "@absolutejs/devices-capacitor") : shellExpoDevicesModule();
7149
7354
  const adapterImport = capacitor ? `import { installCapacitorDeviceAdapterIfNative } from ${JSON.stringify(baseAdapterModule)};` : `import { createAbsoluteExpoBridgeFetch, installAbsoluteExpoWebDeviceAdapter } from ${JSON.stringify(baseAdapterModule)};`;
7150
7355
  const adapterInstall = capacitor ? `installCapacitorDeviceAdapterIfNative({ storagePrefix: ${JSON.stringify(storagePrefix)}${capabilityOptions ? `, ${capabilityOptions}` : ""} });` : "installAbsoluteExpoWebDeviceAdapter();";
7151
- let shellOptions = "{ createFetch: createAbsoluteExpoBridgeFetch }";
7356
+ let shellOptions = auth ? options : "{ createFetch: createAbsoluteExpoBridgeFetch }";
7152
7357
  if (capacitor)
7153
7358
  shellOptions = options;
7154
7359
  if (push) {
@@ -8234,10 +8439,10 @@ var isElysiaApp = (value) => typeof value === "object" && value !== null && type
8234
8439
  runtime: String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION)
8235
8440
  });
8236
8441
  const auth = resolveAbsoluteMobileAuthManifest(options.projectRoot, mobile);
8237
- if (mobile.engine === "expo" && auth) {
8238
- throw new TypeError("Expo mobile Auth is not released yet. The experimental Expo shell cannot safely substitute Capacitor credentials; remove engine: 'expo' or wait for @absolutejs/auth-expo.");
8239
- }
8240
8442
  const sync = auth !== undefined && projectUsesAbsoluteSync(options.projectRoot);
8443
+ if (mobile.engine === "expo" && sync) {
8444
+ throw new TypeError("Expo mobile Sync is not released yet. Auth and authenticated HTTP are available, but @absolutejs/sync requires @absolutejs/sync-expo for shared durable state.");
8445
+ }
8241
8446
  const syncSchema = sync ? discoverAbsoluteSyncSchema(options.projectRoot) : undefined;
8242
8447
  const deviceCapabilities = resolveAbsoluteDeviceCapabilityPlan(options.projectRoot);
8243
8448
  const usesPush = deviceCapabilities.capabilities.includes("pushNotifications");
@@ -8863,12 +9068,12 @@ var init_serverBundleExternals = __esm(() => {
8863
9068
  // src/core/prerender.ts
8864
9069
  var exports_prerender = {};
8865
9070
  __export(exports_prerender, {
8866
- routeToFilename: () => routeToFilename,
8867
- rerenderRoute: () => rerenderRoute,
8868
- readTimestamp: () => readTimestamp,
8869
- prerenderWithServer: () => prerenderWithServer,
9071
+ PRERENDER_BYPASS_HEADER: () => PRERENDER_BYPASS_HEADER,
8870
9072
  prerender: () => prerender,
8871
- PRERENDER_BYPASS_HEADER: () => PRERENDER_BYPASS_HEADER
9073
+ prerenderWithServer: () => prerenderWithServer,
9074
+ readTimestamp: () => readTimestamp,
9075
+ rerenderRoute: () => rerenderRoute,
9076
+ routeToFilename: () => routeToFilename
8872
9077
  });
8873
9078
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync14 } from "fs";
8874
9079
  import { join as join24 } from "path";
@@ -9528,8 +9733,8 @@ var init_rewriteImportsPlugin = __esm(() => {
9528
9733
  // src/build/rewriteImports.ts
9529
9734
  var exports_rewriteImports = {};
9530
9735
  __export(exports_rewriteImports, {
9531
- rewriteVendorDirectories: () => rewriteVendorDirectories2,
9532
- rewriteImports: () => rewriteImports
9736
+ rewriteImports: () => rewriteImports,
9737
+ rewriteVendorDirectories: () => rewriteVendorDirectories2
9533
9738
  });
9534
9739
  var rewriteImports = async (outputPaths, vendorPaths) => {
9535
9740
  const jsFiles = outputPaths.filter((path) => path.endsWith(".js"));
@@ -10192,11 +10397,11 @@ var init_build = __esm(() => {
10192
10397
  // src/cli/scripts/lintProof.ts
10193
10398
  var exports_lintProof = {};
10194
10399
  __export(exports_lintProof, {
10195
- writeLintProof: () => writeLintProof,
10196
- verifyLintProof: () => verifyLintProof,
10197
- runLintProof: () => runLintProof,
10400
+ createLintProof: () => createLintProof,
10198
10401
  createLintSourceTree: () => createLintSourceTree,
10199
- createLintProof: () => createLintProof
10402
+ runLintProof: () => runLintProof,
10403
+ verifyLintProof: () => verifyLintProof,
10404
+ writeLintProof: () => writeLintProof
10200
10405
  });
10201
10406
  import {
10202
10407
  createHash as createHash12,
@@ -14970,9 +15175,9 @@ var init_htmx = __esm(() => {
14970
15175
  // src/cli/scripts/env.ts
14971
15176
  var exports_env = {};
14972
15177
  __export(exports_env, {
14973
- scanEnvUsage: () => scanEnvUsage,
15178
+ collectEnvVars: () => collectEnvVars,
14974
15179
  runEnv: () => runEnv,
14975
- collectEnvVars: () => collectEnvVars
15180
+ scanEnvUsage: () => scanEnvUsage
14976
15181
  });
14977
15182
  import { existsSync as existsSync33, readFileSync as readFileSync33 } from "fs";
14978
15183
  import { join as join41 } from "path";
@@ -15036,12 +15241,12 @@ var init_env = __esm(() => {
15036
15241
  // src/cli/scripts/db.ts
15037
15242
  var exports_db = {};
15038
15243
  __export(exports_db, {
15039
- runDb: () => runDb,
15040
- quoteIdent: () => quoteIdent,
15041
- encodeValue: () => encodeValue,
15042
- dependencyOrder: () => dependencyOrder,
15244
+ chunkRows: () => chunkRows,
15043
15245
  conflictClause: () => conflictClause,
15044
- chunkRows: () => chunkRows
15246
+ dependencyOrder: () => dependencyOrder,
15247
+ encodeValue: () => encodeValue,
15248
+ quoteIdent: () => quoteIdent,
15249
+ runDb: () => runDb
15045
15250
  });
15046
15251
  import { existsSync as existsSync34, mkdirSync as mkdirSync15, readFileSync as readFileSync34, writeFileSync as writeFileSync18 } from "fs";
15047
15252
  import { join as join42 } from "path";
@@ -16381,8 +16586,8 @@ var init_externalAssetPlugin = () => {};
16381
16586
  // src/cli/scripts/compile.ts
16382
16587
  var exports_compile = {};
16383
16588
  __export(exports_compile, {
16384
- shouldEmbedCompiledAsset: () => shouldEmbedCompiledAsset,
16385
- compile: () => compile
16589
+ compile: () => compile,
16590
+ shouldEmbedCompiledAsset: () => shouldEmbedCompiledAsset
16386
16591
  });
16387
16592
  var {env: env5 } = globalThis.Bun;
16388
16593
  import {
@@ -18341,12 +18546,38 @@ import { mkdir as mkdir12, writeFile as writeFile14 } from "fs/promises";
18341
18546
  import { dirname as dirname29, resolve as resolve38 } from "path";
18342
18547
 
18343
18548
  class CdpConnection {
18344
- diagnostics = [];
18345
- socket;
18346
- #closed = false;
18347
- #nextId = 1;
18348
- #pending = new Map;
18549
+ #closed;
18550
+ #nextId;
18551
+ #pending;
18349
18552
  constructor(socket) {
18553
+ this.diagnostics = [];
18554
+ this.#closed = false;
18555
+ this.#nextId = 1;
18556
+ this.#pending = new Map;
18557
+ this.command = (method, params = {}, timeoutMs = CDP_COMMAND_TIMEOUT_MS) => {
18558
+ if (this.#closed) {
18559
+ return Promise.reject(new Error("Android WebView debugger is closed."));
18560
+ }
18561
+ const id = this.#nextId++;
18562
+ return new Promise((_resolve, reject) => {
18563
+ const timer = setTimeout(() => {
18564
+ this.#pending.delete(id);
18565
+ reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms.`));
18566
+ }, timeoutMs);
18567
+ this.#pending.set(id, {
18568
+ reject,
18569
+ resolve: _resolve,
18570
+ timer
18571
+ });
18572
+ this.socket.send(JSON.stringify({ id, method, params }));
18573
+ });
18574
+ };
18575
+ this.close = async () => {
18576
+ if (this.#closed)
18577
+ return;
18578
+ this.#closed = true;
18579
+ this.socket.close();
18580
+ };
18350
18581
  this.socket = socket;
18351
18582
  socket.addEventListener("message", (event) => {
18352
18583
  this.onMessage(event.data);
@@ -18425,30 +18656,6 @@ class CdpConnection {
18425
18656
  }
18426
18657
  pending.resolve(message.result);
18427
18658
  }
18428
- command = (method, params = {}, timeoutMs = CDP_COMMAND_TIMEOUT_MS) => {
18429
- if (this.#closed) {
18430
- return Promise.reject(new Error("Android WebView debugger is closed."));
18431
- }
18432
- const id = this.#nextId++;
18433
- return new Promise((_resolve, reject) => {
18434
- const timer = setTimeout(() => {
18435
- this.#pending.delete(id);
18436
- reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms.`));
18437
- }, timeoutMs);
18438
- this.#pending.set(id, {
18439
- reject,
18440
- resolve: _resolve,
18441
- timer
18442
- });
18443
- this.socket.send(JSON.stringify({ id, method, params }));
18444
- });
18445
- };
18446
- close = async () => {
18447
- if (this.#closed)
18448
- return;
18449
- this.#closed = true;
18450
- this.socket.close();
18451
- };
18452
18659
  }
18453
18660
  var CDP_COMMAND_TIMEOUT_MS = 1e4, WEBVIEW_ATTACH_TIMEOUT_MS = 30000, WEBVIEW_POLL_MS = 200, captureCommand3 = (command) => {
18454
18661
  const result = Bun.spawnSync(command, {
@@ -20926,7 +21133,7 @@ var NOT_FOUND4 = -1, isRecord15 = (value) => typeof value === "object" && value
20926
21133
  }, initialize = async (args) => {
20927
21134
  const { mobile, projectRoot } = await loadMobile(valueAfter(args, "--config"));
20928
21135
  if (mobile.engine === "expo") {
20929
- console.warn("Experimental: Expo Auth, Sync, release publishing, and physical-device acceptance are not complete.");
21136
+ console.warn("Experimental: Expo Auth and authenticated HTTP are available; Sync, release publishing, and physical-device acceptance are not complete.");
20930
21137
  const generated2 = await writeAbsoluteExpoProject(mobile, {
20931
21138
  force: args.includes("--force"),
20932
21139
  projectRoot
@@ -23362,7 +23569,7 @@ init_expoProject();
23362
23569
  init_iosPhysicalDeviceTransport();
23363
23570
  import { spawn } from "child_process";
23364
23571
  import { access as access2 } from "fs/promises";
23365
- import { join as join6 } from "path";
23572
+ import { join as join7 } from "path";
23366
23573
  var METRO_READY_TIMEOUT_MS = 60000;
23367
23574
  var PROCESS_CLOSE_TIMEOUT_MS = 2000;
23368
23575
  var commandEnvironment = (options) => ({
@@ -23371,10 +23578,11 @@ var commandEnvironment = (options) => ({
23371
23578
  ABSOLUTE_EXPO_DEVELOPMENT_CA_PATH: options.certificateAuthorityPath
23372
23579
  } : {},
23373
23580
  ...options.androidOrigin ? { EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN: options.androidOrigin } : {},
23374
- ...options.iosOrigin ? { EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN: options.iosOrigin } : {}
23581
+ ...options.iosOrigin ? { EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN: options.iosOrigin } : {},
23582
+ ...options.metroHost ? { REACT_NATIVE_PACKAGER_HOSTNAME: options.metroHost } : {}
23375
23583
  });
23376
23584
  var absoluteExpoExecutable = async (project) => {
23377
- const executable = join6(project, "node_modules", ".bin", "expo");
23585
+ const executable = join7(project, "node_modules", ".bin", "expo");
23378
23586
  try {
23379
23587
  await access2(executable);
23380
23588
  return executable;
@@ -23385,8 +23593,8 @@ var absoluteExpoExecutable = async (project) => {
23385
23593
  var planAbsoluteExpoDevSession = (config, options) => {
23386
23594
  if (config.engine !== "expo")
23387
23595
  throw new TypeError("The Expo development controller requires Expo.");
23388
- if (options.platforms.length === 0)
23389
- throw new TypeError("Expo development requires a local target platform.");
23596
+ if (options.platforms.length === 0 && options.metro === "external")
23597
+ throw new TypeError("External Expo execution requires a target platform.");
23390
23598
  const secureOrigin = [options.androidOrigin, options.iosOrigin].some((origin) => origin && new URL(origin).protocol === "https:");
23391
23599
  if (secureOrigin && !options.certificateAuthorityPath)
23392
23600
  throw new TypeError("Expo HTTPS development requires the AbsoluteJS development CA certificate.");
@@ -23403,7 +23611,7 @@ var planAbsoluteExpoDevSession = (config, options) => {
23403
23611
  env,
23404
23612
  role: "metro"
23405
23613
  };
23406
- const prepare = {
23614
+ const prepare = options.platforms.length === 0 ? undefined : {
23407
23615
  args: [
23408
23616
  "prebuild",
23409
23617
  "--clean",
@@ -23431,7 +23639,11 @@ var planAbsoluteExpoDevSession = (config, options) => {
23431
23639
  return command;
23432
23640
  });
23433
23641
  return {
23434
- commands: [prepare, metro, ...native],
23642
+ commands: [
23643
+ ...prepare ? [prepare] : [],
23644
+ ...options.metro === "external" ? [] : [metro],
23645
+ ...native
23646
+ ],
23435
23647
  metroPort: options.metroPort,
23436
23648
  project: config.nativeProjectDirectory
23437
23649
  };
@@ -23520,47 +23732,48 @@ var startAbsoluteExpoDevSession = async (options) => {
23520
23732
  const prepareCommand = plan.commands.find((command) => command.role === "native-prepare");
23521
23733
  const metroCommand = plan.commands.find((command) => command.role === "metro");
23522
23734
  const nativeCommands = plan.commands.filter((command) => command.role === "native-build");
23523
- if (!prepareCommand)
23524
- throw new TypeError("Expo native preparation command is missing.");
23525
- if (!metroCommand)
23526
- throw new TypeError("Expo Metro command is missing.");
23527
- setState("preparing-native");
23528
- const prepareStarted = performance.now();
23529
- const prepareProcess = run(executable, prepareCommand.args, {
23530
- cwd: plan.project,
23531
- env: { ...process.env, ...prepareCommand.env },
23532
- stdio: ["ignore", "pipe", "pipe"]
23533
- });
23534
- forwardLines(prepareProcess, (line) => {
23535
- if (line)
23536
- log(`[prebuild] ${line}`);
23537
- });
23538
- const abortPrepare = () => void stopProcess(prepareProcess);
23539
- options.signal?.addEventListener("abort", abortPrepare, { once: true });
23540
- const prepareExit = await waitForExit(prepareProcess);
23541
- options.signal?.removeEventListener("abort", abortPrepare);
23542
- if (options.signal?.aborted)
23543
- throw abortError();
23544
- if (prepareExit !== 0) {
23545
- setState("failed");
23546
- throw new Error(`Expo native preparation exited with status ${prepareExit}.`);
23547
- }
23548
- const prepareMs = performance.now() - prepareStarted;
23549
- timings["preparing-native"] = prepareMs;
23550
- options.onPhaseTiming?.({
23551
- durationMs: prepareMs,
23552
- phase: "preparing-native"
23553
- });
23554
- setState("starting-metro");
23735
+ const runPrepareCommand = async (command) => {
23736
+ setState("preparing-native");
23737
+ const prepareStarted = performance.now();
23738
+ const prepareProcess = run(executable, command.args, {
23739
+ cwd: plan.project,
23740
+ env: { ...process.env, ...command.env },
23741
+ stdio: ["ignore", "pipe", "pipe"]
23742
+ });
23743
+ forwardLines(prepareProcess, (line) => {
23744
+ if (line)
23745
+ log(`[prebuild] ${line}`);
23746
+ });
23747
+ const abortPrepare = () => void stopProcess(prepareProcess);
23748
+ options.signal?.addEventListener("abort", abortPrepare, { once: true });
23749
+ const prepareExit = await waitForExit(prepareProcess);
23750
+ options.signal?.removeEventListener("abort", abortPrepare);
23751
+ if (options.signal?.aborted)
23752
+ throw abortError();
23753
+ if (prepareExit !== 0) {
23754
+ setState("failed");
23755
+ throw new Error(`Expo native preparation exited with status ${prepareExit}.`);
23756
+ }
23757
+ publishTiming("preparing-native", performance.now() - prepareStarted);
23758
+ };
23759
+ if (prepareCommand)
23760
+ await runPrepareCommand(prepareCommand);
23761
+ const managedMetro = metroCommand !== undefined;
23762
+ if (managedMetro)
23763
+ setState("starting-metro");
23555
23764
  const metroStarted = performance.now();
23556
- const metro = run(executable, metroCommand.args, {
23765
+ const metro = metroCommand ? run(executable, metroCommand.args, {
23557
23766
  cwd: plan.project,
23558
23767
  env: { ...process.env, ...metroCommand.env },
23559
23768
  stdio: ["ignore", "pipe", "pipe"]
23560
- });
23769
+ }) : undefined;
23561
23770
  let metroReady = false;
23562
23771
  let resolveMetro;
23563
23772
  const metroPromise = new Promise((resolve4, reject) => {
23773
+ if (!metro) {
23774
+ resolve4();
23775
+ return;
23776
+ }
23564
23777
  const timeout = setTimeout(() => {
23565
23778
  reject(new Error("Expo Metro did not become ready within 60 seconds."));
23566
23779
  }, METRO_READY_TIMEOUT_MS);
@@ -23575,15 +23788,19 @@ var startAbsoluteExpoDevSession = async (options) => {
23575
23788
  }
23576
23789
  });
23577
23790
  });
23578
- forwardLines(metro, (line) => {
23579
- if (line)
23580
- log(`[metro] ${line}`);
23581
- if (!metroReady && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
23582
- metroReady = true;
23583
- resolveMetro?.();
23584
- }
23585
- });
23586
- const abort = () => void stopProcess(metro);
23791
+ if (metro)
23792
+ forwardLines(metro, (line) => {
23793
+ if (line)
23794
+ log(`[metro] ${line}`);
23795
+ if (!metroReady && /(?:Waiting on|Metro waiting on|Dev server ready)/iu.test(line)) {
23796
+ metroReady = true;
23797
+ resolveMetro?.();
23798
+ }
23799
+ });
23800
+ const abort = () => {
23801
+ if (metro)
23802
+ stopProcess(metro);
23803
+ };
23587
23804
  options.signal?.addEventListener("abort", abort, { once: true });
23588
23805
  const runNativeCommand = async (command) => {
23589
23806
  if (options.signal?.aborted)
@@ -23664,12 +23881,8 @@ var startAbsoluteExpoDevSession = async (options) => {
23664
23881
  try {
23665
23882
  await startPhysicalIosEnrollment();
23666
23883
  await metroPromise;
23667
- const metroMs = performance.now() - metroStarted;
23668
- timings["starting-metro"] = metroMs;
23669
- options.onPhaseTiming?.({
23670
- durationMs: metroMs,
23671
- phase: "starting-metro"
23672
- });
23884
+ if (managedMetro)
23885
+ publishTiming("starting-metro", performance.now() - metroStarted);
23673
23886
  await runNativeCommands(nativeCommands);
23674
23887
  setState("ready");
23675
23888
  return {
@@ -23678,14 +23891,16 @@ var startAbsoluteExpoDevSession = async (options) => {
23678
23891
  timings,
23679
23892
  close: async () => {
23680
23893
  options.signal?.removeEventListener("abort", abort);
23681
- await stopProcess(metro);
23894
+ if (metro)
23895
+ await stopProcess(metro);
23682
23896
  await closeEnrollmentServer();
23683
23897
  setState("closed");
23684
23898
  }
23685
23899
  };
23686
23900
  } catch (error) {
23687
23901
  options.signal?.removeEventListener("abort", abort);
23688
- await stopProcess(metro);
23902
+ if (metro)
23903
+ await stopProcess(metro);
23689
23904
  await closeEnrollmentServer();
23690
23905
  setState("failed");
23691
23906
  throw error;
@@ -24092,9 +24307,6 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24092
24307
  throw new TypeError("--ios-device requires ios in mobile.platforms.");
24093
24308
  }
24094
24309
  if (options.iosDevice) {
24095
- if (mobileConfig?.engine === "expo" && detectAbsoluteMobileHost() !== "macos") {
24096
- throw new Error("Expo physical iOS development currently requires local macOS; Expo Remote Mac execution is the next adapter milestone.");
24097
- }
24098
24310
  if (detectAbsoluteMobileHost() === "macos")
24099
24311
  iosPhysicalServerHost = mobileReachableHost(resolvedDev.host);
24100
24312
  else {
@@ -24138,6 +24350,7 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24138
24350
  }
24139
24351
  }
24140
24352
  const platforms = [];
24353
+ let remoteIos;
24141
24354
  if (normalized.platforms.includes("android")) {
24142
24355
  const target = options.androidDevice ? "device" : "emulator";
24143
24356
  let ready = androidToolchainReady(await inspectAbsoluteMobileToolchain(), target);
@@ -24153,7 +24366,14 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24153
24366
  }
24154
24367
  if (normalized.platforms.includes("ios")) {
24155
24368
  if (detectAbsoluteMobileHost() !== "macos") {
24156
- console.log(cliTag("\x1B[33m", "Expo iOS target skipped on this host; Expo Remote Mac execution is the next adapter milestone."));
24369
+ const remote = selectedRemoteMacProfile ?? await getAbsoluteRemoteMacProfile();
24370
+ if (!remote) {
24371
+ console.log(cliTag("\x1B[33m", "Expo iOS target skipped. Pair a Mac with `absolute mobile pair mac <name> <user@host>`."));
24372
+ } else {
24373
+ selectedRemoteMacProfile = remote;
24374
+ remoteIos = createAbsoluteRemoteExpoIosDevProject(normalized, process.cwd(), remote);
24375
+ console.log(cliTag("\x1B[35m", `Using remote Mac ${remote.name} for Expo iOS development.`));
24376
+ }
24157
24377
  } else {
24158
24378
  const target = options.iosDevice ? "device" : "simulator";
24159
24379
  let ready = iosToolchainReady(await inspectAbsoluteMobileToolchain(), target);
@@ -24168,13 +24388,17 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24168
24388
  platforms.push("ios");
24169
24389
  }
24170
24390
  }
24171
- if (platforms.length > 0) {
24391
+ if (platforms.length > 0 || remoteIos) {
24172
24392
  expoDevProject = {
24173
24393
  executable: await absoluteExpoExecutable(generated.path),
24174
24394
  mobile: normalized,
24175
- platforms
24395
+ platforms,
24396
+ ...remoteIos ? { remoteIos } : {}
24176
24397
  };
24177
- console.log(cliTag("\x1B[35m", `Expo development build queued for ${platforms.join(" + ")}; Metro and native launch will start with the Bun server.`));
24398
+ console.log(cliTag("\x1B[35m", `Expo development build queued for ${[
24399
+ ...platforms,
24400
+ ...remoteIos ? ["ios (Remote Mac)"] : []
24401
+ ].join(" + ")}; Metro and native launch will start with the Bun server.`));
24178
24402
  }
24179
24403
  } else if (normalized.platforms.includes("android")) {
24180
24404
  const androidTarget = options.androidDevice ? "device" : "emulator";
@@ -24601,54 +24825,166 @@ var dev = async (serverEntry, configPath2, options = {}) => {
24601
24825
  return;
24602
24826
  }
24603
24827
  const androidHost = options.androidDevice ? mobileReachableHost(resolvedDev.host) : "10.0.2.2";
24604
- const iosHost = options.iosDevice ? mobileReachableHost(resolvedDev.host) : "localhost";
24605
- expoDevStart = startAbsoluteExpoDevSession({
24606
- androidDevice: options.androidDevice,
24607
- androidOrigin: project.platforms.includes("android") ? absoluteDevOrigin(androidHost) : undefined,
24828
+ const iosHost = options.iosDevice ? iosPhysicalServerHost ?? mobileReachableHost(resolvedDev.host) : "localhost";
24829
+ const androidOrigin = project.platforms.includes("android") ? absoluteDevOrigin(androidHost) : undefined;
24830
+ const iosOrigin = project.platforms.includes("ios") || project.remoteIos ? absoluteDevOrigin(iosHost) : undefined;
24831
+ let metroSession;
24832
+ let localNativeSession;
24833
+ let remoteSession;
24834
+ const metroStart = startAbsoluteExpoDevSession({
24835
+ androidOrigin,
24608
24836
  certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
24609
24837
  config: project.mobile,
24610
24838
  executable: project.executable,
24611
- iosDevice: options.iosDevice,
24612
- iosOrigin: project.platforms.includes("ios") ? absoluteDevOrigin(iosHost) : undefined,
24839
+ iosOrigin,
24613
24840
  metroPort: expoMetroPort,
24614
- platforms: project.platforms,
24841
+ platforms: [],
24615
24842
  signal: expoDevAbort.signal,
24616
24843
  log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo ${message}`)),
24617
24844
  onPhaseTiming: ({ durationMs, phase }) => {
24618
24845
  sendTelemetryEvent("mobile:expo-dev-phase", {
24619
24846
  durationMs: Math.round(durationMs),
24847
+ host: detectAbsoluteMobileHost(),
24620
24848
  phase,
24621
24849
  platform: expoTelemetryPlatform(phase),
24622
24850
  provider: "expo"
24623
24851
  });
24624
24852
  },
24625
24853
  onStateChange: (state) => {
24626
- expoDevState = state;
24627
24854
  if (state === "ready" || state === "closed")
24628
24855
  return;
24856
+ expoDevState = state;
24629
24857
  printNativeOutput(cliTag("\x1B[35m", `Expo development: ${state}.`));
24630
24858
  }
24631
- }).then(async (session) => {
24859
+ }).then((session) => {
24860
+ metroSession = session;
24861
+ return session;
24862
+ });
24863
+ const localNativeStart = metroStart.then(async () => {
24864
+ if (project.platforms.length === 0)
24865
+ return;
24866
+ const session = await startAbsoluteExpoDevSession({
24867
+ androidDevice: options.androidDevice,
24868
+ androidOrigin,
24869
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
24870
+ config: project.mobile,
24871
+ executable: project.executable,
24872
+ iosDevice: options.iosDevice,
24873
+ iosOrigin,
24874
+ metro: "external",
24875
+ metroPort: expoMetroPort,
24876
+ platforms: project.platforms,
24877
+ signal: expoDevAbort.signal,
24878
+ log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo ${message}`)),
24879
+ onPhaseTiming: ({ durationMs, phase }) => {
24880
+ sendTelemetryEvent("mobile:expo-dev-phase", {
24881
+ durationMs: Math.round(durationMs),
24882
+ host: detectAbsoluteMobileHost(),
24883
+ phase,
24884
+ platform: expoTelemetryPlatform(phase),
24885
+ provider: "expo"
24886
+ });
24887
+ },
24888
+ onStateChange: (state) => {
24889
+ if (state === "ready" || state === "closed")
24890
+ return;
24891
+ expoDevState = state;
24892
+ printNativeOutput(cliTag("\x1B[35m", `Expo development: ${state}.`));
24893
+ }
24894
+ });
24895
+ localNativeSession = session;
24896
+ return session;
24897
+ });
24898
+ const remoteIosProject = project.remoteIos;
24899
+ const remoteStart = remoteIosProject ? metroStart.then(async () => {
24900
+ const session = await startAbsoluteRemoteExpoIosDevSession({
24901
+ certificateAuthorityPath: devCertificateAuthorityPath ?? undefined,
24902
+ deviceIdentifier: options.iosDevice,
24903
+ https: httpsEnabled,
24904
+ metroPort: expoMetroPort,
24905
+ port,
24906
+ project: remoteIosProject,
24907
+ serverHost: options.iosDevice ? iosPhysicalServerHost : undefined,
24908
+ signal: expoDevAbort.signal,
24909
+ log: (message) => printNativeOutput(cliTag("\x1B[35m", `Expo [remote-ios] ${message}`)),
24910
+ onPhaseTiming: ({ durationMs, phase }) => {
24911
+ sendTelemetryEvent("mobile:expo-dev-phase", {
24912
+ durationMs: Math.round(durationMs),
24913
+ host: "remote-macos",
24914
+ phase,
24915
+ platform: expoTelemetryPlatform(phase),
24916
+ provider: "expo"
24917
+ });
24918
+ },
24919
+ onStateChange: (state) => {
24920
+ if (state === "ready" || state === "closed")
24921
+ return;
24922
+ expoDevState = state;
24923
+ printNativeOutput(cliTag("\x1B[35m", `Expo Remote Mac iOS: ${state}.`));
24924
+ }
24925
+ });
24926
+ remoteSession = session;
24927
+ return session;
24928
+ }) : Promise.resolve(undefined);
24929
+ expoDevStart = Promise.all([metroStart, localNativeStart, remoteStart]).then(async ([metro, localNative, remote]) => {
24930
+ const session = {
24931
+ metroPort: metro.metroPort,
24932
+ platforms: [
24933
+ ...new Set([
24934
+ ...localNative?.platforms ?? [],
24935
+ ...remote?.platforms ?? []
24936
+ ])
24937
+ ],
24938
+ timings: {
24939
+ ...metro.timings,
24940
+ ...localNative?.timings,
24941
+ ...remote?.timings
24942
+ },
24943
+ close: async () => {
24944
+ await Promise.all([
24945
+ metro.close(),
24946
+ localNative?.close(),
24947
+ remote?.close()
24948
+ ]);
24949
+ }
24950
+ };
24632
24951
  if (cleaning) {
24633
24952
  await session.close();
24634
24953
  } else {
24635
24954
  expoDevSession = session;
24955
+ expoDevState = "ready";
24636
24956
  sendTelemetryEvent("mobile:expo-dev-ready", {
24637
24957
  metroPort: session.metroPort,
24638
24958
  platforms: session.platforms,
24639
24959
  provider: "expo",
24960
+ remoteIos: project.remoteIos !== undefined,
24640
24961
  timings: session.timings
24641
24962
  });
24642
24963
  printNativeOutput(cliTag("\x1B[35m", `Expo ${session.platforms.join(" + ")} connected; native Fast Refresh uses Metro on ${session.metroPort}, and AbsoluteJS pages use HMR on ${port}.`));
24643
24964
  }
24644
24965
  }).catch((error) => {
24966
+ expoDevAbort.abort();
24967
+ Promise.all([
24968
+ metroSession?.close().catch(() => {
24969
+ return;
24970
+ }),
24971
+ localNativeSession?.close().catch(() => {
24972
+ return;
24973
+ }),
24974
+ remoteSession?.close().catch(() => {
24975
+ return;
24976
+ })
24977
+ ]);
24645
24978
  expoDevState = "failed";
24646
24979
  if (cleaning && error instanceof Error && error.name === "AbortError") {
24647
24980
  return;
24648
24981
  }
24649
24982
  sendTelemetryEvent("mobile:expo-dev-failed", {
24650
24983
  phase: expoDevState,
24651
- platforms: project.platforms,
24984
+ platforms: [
24985
+ ...project.platforms,
24986
+ ...project.remoteIos ? ["ios"] : []
24987
+ ],
24652
24988
  provider: "expo"
24653
24989
  });
24654
24990
  console.error(cliTag("\x1B[31m", `Expo development failed: ${error instanceof Error ? error.message : String(error)}`));
@@ -25186,7 +25522,10 @@ var dev = async (serverEntry, configPath2, options = {}) => {
25186
25522
  console.log(cliTag("\x1B[35m", `iOS target: ${target}; HMR port ${port}.`));
25187
25523
  }
25188
25524
  if (expoDevProject) {
25189
- console.log(cliTag("\x1B[35m", `Expo targets: ${expoDevProject.platforms.join(", ")}; state ${expoDevState}; Metro ${expoMetroPort ?? "not allocated"}; AbsoluteJS HMR ${port}.`));
25525
+ console.log(cliTag("\x1B[35m", `Expo targets: ${[
25526
+ ...expoDevProject.platforms,
25527
+ ...expoDevProject.remoteIos ? ["ios (Remote Mac)"] : []
25528
+ ].join(", ")}; state ${expoDevState}; Metro ${expoMetroPort ?? "not allocated"}; AbsoluteJS HMR ${port}.`));
25190
25529
  }
25191
25530
  },
25192
25531
  relaunchDevice: async () => {