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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -167,7 +167,7 @@ var init_startupBanner = __esm(() => {
167
167
 
168
168
  // src/mobile/config.ts
169
169
  import { resolve as resolve6 } from "path";
170
- var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, HOSTNAME_PATTERN, resolveProjectPath = (projectRoot, value, field2) => {
170
+ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
171
171
  const root = resolve6(projectRoot);
172
172
  const path = resolve6(root, value);
173
173
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -240,11 +240,31 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
240
240
  }
241
241
  return value.match(/.{2}/g)?.join(":") ?? value;
242
242
  }))
243
- ].sort(), normalizeExpoNativeRoutes = (config, projectRoot) => {
243
+ ].sort(), validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
244
+ if (segment === "*" && (index !== count - 1 || count === 1)) {
245
+ throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
246
+ }
247
+ if (segment === "*")
248
+ return;
249
+ if (!segment.startsWith(":") && (segment.includes("*") || segment.includes(":"))) {
250
+ throw new TypeError(`mobile.routes.native route ${path} contains invalid segment ${segment}.`);
251
+ }
252
+ if (!segment.startsWith(":"))
253
+ return;
254
+ const name = segment.slice(1);
255
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(name)) {
256
+ throw new TypeError(`mobile.routes.native route ${path} has invalid parameter ${segment}.`);
257
+ }
258
+ if (parameters.has(name)) {
259
+ throw new TypeError(`mobile.routes.native route ${path} repeats parameter ${segment}.`);
260
+ }
261
+ parameters.add(name);
262
+ }, normalizeExpoNativeRoutes = (config, projectRoot) => {
244
263
  if (config.engine !== "expo")
245
264
  return {};
246
265
  const routes = config.routes?.native ?? {};
247
266
  const normalized = {};
267
+ const ownership = new Map;
248
268
  for (const [route, module] of Object.entries(routes)) {
249
269
  const path = normalizeEntry(route);
250
270
  if (path.includes("?") || path.includes("#") || path !== "/" && path.endsWith("/")) {
@@ -253,9 +273,18 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
253
273
  if (path === "/__absolute/native") {
254
274
  throw new TypeError("mobile.routes.native reserves /__absolute/native for the Expo diagnostic screen.");
255
275
  }
256
- if (path.includes("*") || path.includes(":")) {
257
- throw new TypeError(`mobile.routes.native route ${path} must be static during the Expo experiment; parameters and wildcards are not supported yet.`);
276
+ const segments = path.split("/").filter(Boolean);
277
+ if (segments[0] && EXPO_RESERVED_ROUTE_PREFIXES.has(segments[0])) {
278
+ throw new TypeError(`mobile.routes.native route ${path} conflicts with an Expo Router or Metro reserved path.`);
279
+ }
280
+ const parameters = new Set;
281
+ segments.forEach((segment, index) => validateExpoNativeRouteSegment(path, segment, index, segments.length, parameters));
282
+ const signature = segments.map((segment) => segment.startsWith(":") ? ":" : segment).join("/");
283
+ const existing = ownership.get(signature);
284
+ if (existing) {
285
+ throw new TypeError(`mobile.routes.native routes ${existing} and ${path} claim the same Expo route pattern.`);
258
286
  }
287
+ ownership.set(signature, path);
259
288
  normalized[path] = resolveProjectPath(projectRoot, requireText(module, `mobile.routes.native[${path}]`), `mobile.routes.native[${path}]`);
260
289
  }
261
290
  return Object.fromEntries(Object.entries(normalized).sort(([left], [right]) => left.localeCompare(right)));
@@ -294,6 +323,16 @@ var init_config = __esm(() => {
294
323
  APPLE_APP_ID_PREFIX_PATTERN = /^[A-Z0-9]{10}$/;
295
324
  CERTIFICATE_FINGERPRINT_PATTERN = /^[0-9A-F]{64}$/;
296
325
  HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/;
326
+ EXPO_RESERVED_ROUTE_PREFIXES = new Set([
327
+ "_expo",
328
+ "_flight",
329
+ "_sitemap",
330
+ "assets",
331
+ "expo-dev-plugins",
332
+ "inspector",
333
+ "manifest",
334
+ "public"
335
+ ]);
297
336
  });
298
337
 
299
338
  // src/utils/stringModifiers.ts
@@ -5192,6 +5231,7 @@ var getCurrentAbsoluteMobileProducerContext = () => {
5192
5231
  // src/mobile/pageProtocol.ts
5193
5232
  var ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE = "application/vnd.absolute.page+json";
5194
5233
  var ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION = 1;
5234
+ var ABSOLUTE_NATIVE_ROUTE_DATA_MEDIA_TYPE = "application/vnd.absolute.native-route+json";
5195
5235
  var BAD_REQUEST_STATUS = 400;
5196
5236
  var OK_STATUS = 200;
5197
5237
  var SERVER_ERROR_STATUS = 500;
@@ -5210,6 +5250,11 @@ var acceptsAbsoluteMobilePage = (request) => {
5210
5250
  return false;
5211
5251
  return (request.headers.get("accept") ?? "").split(",").some((value) => parseMediaType(value) === ABSOLUTE_MOBILE_PAGE_MEDIA_TYPE);
5212
5252
  };
5253
+ var acceptsAbsoluteNativeRouteData = (request) => {
5254
+ if (!request)
5255
+ return false;
5256
+ return (request.headers.get("accept") ?? "").split(",").some((value) => parseMediaType(value) === ABSOLUTE_NATIVE_ROUTE_DATA_MEDIA_TYPE);
5257
+ };
5213
5258
  var readRequiredHeader = (request, name) => {
5214
5259
  const value = request.headers.get(name)?.trim();
5215
5260
  return value ? value : undefined;
@@ -5340,6 +5385,32 @@ var finalizeArchivedMobilePage = (context, input) => {
5340
5385
  return createAbsoluteMobilePageErrorResponse(page.pageId);
5341
5386
  }
5342
5387
  };
5388
+ var finalizeAbsoluteNativeRouteDevelopmentPage = (input) => {
5389
+ if (false) {}
5390
+ if (input.request?.method !== "GET") {
5391
+ return createAbsoluteMobileInvalidRequestResponse("Native-route data requests must use GET.");
5392
+ }
5393
+ if (input.request.headers.get(MOBILE_PAGE_REQUEST_HEADERS.protocol) !== String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION)) {
5394
+ return createAbsoluteMobileInvalidRequestResponse(`Native-route data requires protocol ${ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION}.`);
5395
+ }
5396
+ const [representation] = input.compatibility.representations;
5397
+ if (!representation) {
5398
+ return createAbsoluteMobilePageErrorResponse(input.compatibility.pageId);
5399
+ }
5400
+ try {
5401
+ return envelopeResponse({
5402
+ contract: representation.contract,
5403
+ framework: input.compatibility.framework,
5404
+ kind: "page",
5405
+ pageId: input.compatibility.pageId,
5406
+ props: normalizeJsonValue(representation.mapProps(input.props)),
5407
+ status: input.status ?? OK_STATUS
5408
+ }, input.status ?? OK_STATUS);
5409
+ } catch (error) {
5410
+ console.error(`[Mobile] Failed to produce development native route ${input.compatibility.pageId}:`, error);
5411
+ return createAbsoluteMobilePageErrorResponse(input.compatibility.pageId);
5412
+ }
5413
+ };
5343
5414
  var createAbsoluteMobileInvalidRequestResponse = (message) => envelopeResponse({ kind: "invalid-request", message }, BAD_REQUEST_STATUS);
5344
5415
  var createAbsoluteMobilePageErrorResponse = (pageId) => envelopeResponse({
5345
5416
  code: "representation-failed",
@@ -5349,6 +5420,9 @@ var createAbsoluteMobilePageErrorResponse = (pageId) => envelopeResponse({
5349
5420
  }, SERVER_ERROR_STATUS);
5350
5421
  var createAbsoluteMobileUpgradeResponse = (result) => envelopeResponse(result, UPGRADE_REQUIRED_STATUS);
5351
5422
  var finalizeAbsoluteMobilePage = (input) => {
5423
+ if (acceptsAbsoluteNativeRouteData(input.request)) {
5424
+ return finalizeAbsoluteNativeRouteDevelopmentPage(input);
5425
+ }
5352
5426
  const parsed = parseAbsoluteMobilePageRequest(input.request);
5353
5427
  if (parsed.kind === "not-mobile")
5354
5428
  return;
@@ -6127,7 +6201,7 @@ import {
6127
6201
  writeFile as writeFile12
6128
6202
  } from "fs/promises";
6129
6203
  import { createHash as createHash10 } from "crypto";
6130
- import { basename as basename4, dirname as dirname10, join as join14, relative as relative10, resolve as resolve12 } from "path";
6204
+ import { basename as basename4, dirname as dirname10, join as join14, relative as relative10, resolve as resolve12, sep as sep6 } from "path";
6131
6205
  var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
6132
6206
  `;
6133
6207
  var EXPO_ASSET_EXTENSION = ".absasset";
@@ -6140,18 +6214,65 @@ var exists3 = async (path) => {
6140
6214
  return false;
6141
6215
  }
6142
6216
  };
6217
+ var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6218
+ var requireManifestString = (value, field2) => {
6219
+ if (typeof value !== "string" || !value) {
6220
+ throw new TypeError(`AbsoluteJS mobile manifest ${field2} is invalid.`);
6221
+ }
6222
+ return value;
6223
+ };
6224
+ var expoNativeDataManifest = (value) => {
6225
+ if (!isRecord8(value) || !Array.isArray(value.pages) || !Array.isArray(value.routes)) {
6226
+ throw new TypeError("AbsoluteJS mobile manifest is invalid.");
6227
+ }
6228
+ const pages = value.pages.map((page) => {
6229
+ if (!isRecord8(page))
6230
+ throw new TypeError("AbsoluteJS mobile manifest page is invalid.");
6231
+ return {
6232
+ bundleHash: requireManifestString(page.bundleHash, "page.bundleHash"),
6233
+ contract: requireManifestString(page.contract, "page.contract"),
6234
+ pageId: requireManifestString(page.pageId, "page.pageId")
6235
+ };
6236
+ });
6237
+ const routes = value.routes.flatMap((route) => {
6238
+ if (!isRecord8(route) || typeof route.method !== "string") {
6239
+ throw new TypeError("AbsoluteJS mobile manifest route is invalid.");
6240
+ }
6241
+ if (route.method !== "GET")
6242
+ return [];
6243
+ return [
6244
+ {
6245
+ method: "GET",
6246
+ pageId: requireManifestString(route.pageId, "route.pageId"),
6247
+ pattern: requireManifestString(route.pattern, "route.pattern")
6248
+ }
6249
+ ];
6250
+ });
6251
+ return {
6252
+ appBuild: requireManifestString(value.appBuild, "appBuild"),
6253
+ pages,
6254
+ productionOrigin: requireManifestString(value.productionOrigin, "productionOrigin"),
6255
+ routes,
6256
+ runtime: requireManifestString(value.runtime, "runtime")
6257
+ };
6258
+ };
6143
6259
  var portableRelative2 = (from, destination) => {
6144
6260
  const value = relative10(from, destination).replaceAll("\\", "/");
6145
6261
  return value.startsWith(".") ? value : `./${value}`;
6146
6262
  };
6147
- var routeSegments = (route) => route.split("/").filter(Boolean).map((segment) => {
6148
- if (segment.startsWith(":"))
6149
- return `[${segment.slice(1)}]`;
6150
- if (segment === "." || segment === ".." || !/^[A-Za-z0-9._~-]+$/u.test(segment)) {
6151
- throw new TypeError(`Expo native route ${route} contains an unsupported segment ${segment}.`);
6152
- }
6153
- return segment;
6154
- });
6263
+ var routeSegments = (route) => {
6264
+ const segments = route.split("/").filter(Boolean);
6265
+ return segments.map((segment, index) => {
6266
+ if (segment.startsWith(":"))
6267
+ return `[${segment.slice(1)}]`;
6268
+ if (segment === "*" && index === segments.length - 1)
6269
+ return "[...absoluteWildcard]";
6270
+ if (segment === "." || segment === ".." || !/^[A-Za-z0-9._~-]+$/u.test(segment)) {
6271
+ throw new TypeError(`Expo native route ${route} contains an unsupported segment ${segment}.`);
6272
+ }
6273
+ return segment;
6274
+ });
6275
+ };
6155
6276
  var routeFile = (project, route) => join14(project, "app", ...routeSegments(route), "index.tsx");
6156
6277
  var packageDependencies = (plan) => Object.fromEntries(plan.requiredPackages.map((spec) => {
6157
6278
  const separator = spec.lastIndexOf("@");
@@ -6629,6 +6750,7 @@ var webHostSource = (config, auth, sync) => {
6629
6750
  "/__absolute/native",
6630
6751
  ...Object.keys(config.expoNativeRoutes)
6631
6752
  ];
6753
+ const nativeRoutePatterns = nativeRoutes.map((route) => route.split("/").filter(Boolean));
6632
6754
  return `${EXPO_GENERATED_HEADER}import * as Linking from 'expo-linking';
6633
6755
  import { router, usePathname } from 'expo-router';
6634
6756
  import { useEffect, useRef, useState } from 'react';
@@ -6643,7 +6765,7 @@ ${sync ? "import { createAbsoluteExpoSyncBridge, startAbsoluteExpoSync } from '.
6643
6765
  const BRIDGE_FORMAT = 3;
6644
6766
  const MAX_MESSAGE_BYTES = 64 * 1024;
6645
6767
  const MAX_HTTP_BODY_BYTES = 48 * 1024;
6646
- const NATIVE_ROUTES = new Set(${JSON.stringify(nativeRoutes)});
6768
+ const NATIVE_ROUTE_PATTERNS = ${JSON.stringify(nativeRoutePatterns)};
6647
6769
  const PRODUCTION_ORIGIN = ${JSON.stringify(config.productionOrigin)};
6648
6770
  const DEV_ORIGIN = Platform.OS === 'android'
6649
6771
  ? process.env.EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN
@@ -6652,6 +6774,19 @@ const HMR_TARGET = Platform.OS === 'android' ? 'expo-android' : 'expo-ios';
6652
6774
  const AUTH_ENABLED = ${auth ? "true" : "false"};
6653
6775
  const SYNC_ENABLED = ${sync ? "true" : "false"};
6654
6776
 
6777
+ const isNativeRoute = (pathname: string) => {
6778
+ const segments = pathname.split('/').filter(Boolean);
6779
+ return NATIVE_ROUTE_PATTERNS.some(pattern => {
6780
+ for (let index = 0; index < pattern.length; index += 1) {
6781
+ const expected = pattern[index]!;
6782
+ if (expected === '*') return segments.length > index;
6783
+ if (segments[index] === undefined) return false;
6784
+ if (!expected.startsWith(':') && expected !== segments[index]) return false;
6785
+ }
6786
+ return segments.length === pattern.length;
6787
+ });
6788
+ };
6789
+
6655
6790
  const bridgeBootstrap = (path: string) => {
6656
6791
  const initialPath = DEV_ORIGIN
6657
6792
  ? 'location.pathname + location.search + location.hash'
@@ -6727,7 +6862,7 @@ const bridgeBootstrap = (path: string) => {
6727
6862
  const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null;
6728
6863
  if (!anchor) return;
6729
6864
  const url = new URL(anchor.href, location.href);
6730
- if (!${JSON.stringify(nativeRoutes)}.includes(url.pathname)) return;
6865
+ if (!isNativeRoute(url.pathname)) return;
6731
6866
  event.preventDefault();
6732
6867
  send({ format: 3, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
6733
6868
  }, true);
@@ -6850,7 +6985,7 @@ export function AbsoluteWebHost() {
6850
6985
  if (message.kind === 'event' && (message.event === 'navigation' || message.event === 'ready')) {
6851
6986
  const target = new URL(message.path, PRODUCTION_ORIGIN);
6852
6987
  if (target.origin !== PRODUCTION_ORIGIN) return;
6853
- if (NATIVE_ROUTES.has(target.pathname)) router.push(message.path as never);
6988
+ if (isNativeRoute(target.pathname)) router.push(message.path as never);
6854
6989
  else activeWebPath.current = message.path;
6855
6990
  return;
6856
6991
  }
@@ -6915,7 +7050,134 @@ var catchAllRouteSource = `${EXPO_GENERATED_HEADER}import { AbsoluteWebHost } fr
6915
7050
 
6916
7051
  export default AbsoluteWebHost;
6917
7052
  `;
6918
- var nativeWrapperSource = (wrapper, module) => `${EXPO_GENERATED_HEADER}export { default } from ${JSON.stringify(portableRelative2(dirname10(wrapper), module).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
7053
+ var nativeRouteRuntimeSource = (auth) => `${EXPO_GENERATED_HEADER}import { useLocalSearchParams, usePathname } from 'expo-router';
7054
+ import { type ComponentType, useEffect, useMemo, useState } from 'react';
7055
+ import { ActivityIndicator, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
7056
+ import { ABSOLUTE_MOBILE_MANIFEST } from './webAssets';
7057
+ ${auth ? "import { absoluteExpoAuth } from './AbsoluteAuth';" : ""}
7058
+
7059
+ const PAGE_MEDIA_TYPE = 'application/vnd.absolute.page+json';
7060
+ const NATIVE_DATA_MEDIA_TYPE = 'application/vnd.absolute.native-route+json';
7061
+ const PROTOCOL = 1;
7062
+ const MAX_DATA_BYTES = 1024 * 1024;
7063
+ const DEV_ORIGIN = Platform.OS === 'android'
7064
+ ? process.env.EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN
7065
+ : process.env.EXPO_PUBLIC_ABSOLUTE_DEV_IOS_ORIGIN;
7066
+
7067
+ type RouteParams = Record<string, string | string[] | undefined>;
7068
+ type NativeRouteProps<PageProps extends object, Params extends object> = {
7069
+ pageProps: Readonly<PageProps>;
7070
+ params: Readonly<Params>;
7071
+ reload: () => void;
7072
+ };
7073
+ type NativeRouteState<PageProps> =
7074
+ | { kind: 'loading' }
7075
+ | { kind: 'ready'; pageProps: PageProps }
7076
+ | { kind: 'error' };
7077
+
7078
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
7079
+ typeof value === 'object' && value !== null && !Array.isArray(value);
7080
+ const routeSegmentPattern = (segment: string) => {
7081
+ if (segment === '*') return '.*';
7082
+ if (segment.startsWith(':') && segment.endsWith('?')) return '[^/]*';
7083
+ if (segment.startsWith(':')) return '[^/]+';
7084
+ return segment.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
7085
+ };
7086
+ const matchesRoute = (pattern: string, pathname: string) =>
7087
+ new RegExp('^' + pattern.split('/').map(routeSegmentPattern).join('/') + '/?$').test(pathname);
7088
+ const requestPathFor = (pathname: string, params: RouteParams, pattern: string) => {
7089
+ const pathNames = new Set(pattern.split('/').filter(segment => segment.startsWith(':')).map(segment => segment.replace(/^:/, '').replace(/\\?$/, '')));
7090
+ if (pattern.endsWith('/*')) pathNames.add('absoluteWildcard');
7091
+ const query = new URLSearchParams();
7092
+ for (const [name, value] of Object.entries(params).sort(([left], [right]) => left.localeCompare(right))) {
7093
+ if (pathNames.has(name) || name === '#' || value === undefined) continue;
7094
+ for (const item of Array.isArray(value) ? value : [value]) query.append(name, item);
7095
+ }
7096
+ const search = query.toString();
7097
+ return pathname + (search ? '?' + search : '');
7098
+ };
7099
+ const productionPage = (pathname: string) => {
7100
+ if (!ABSOLUTE_MOBILE_MANIFEST) throw new Error('AbsoluteJS native route data is not prepared for production.');
7101
+ const route = ABSOLUTE_MOBILE_MANIFEST.routes.find(candidate => candidate.method === 'GET' && matchesRoute(candidate.pattern, pathname));
7102
+ if (!route) throw new Error('No trusted AbsoluteJS page route owns this native URL.');
7103
+ const page = ABSOLUTE_MOBILE_MANIFEST.pages.find(candidate => candidate.pageId === route.pageId);
7104
+ if (!page) throw new Error('The embedded AbsoluteJS page contract is incomplete.');
7105
+ return page;
7106
+ };
7107
+ const createDataRequest = (path: string) => {
7108
+ const development = typeof DEV_ORIGIN === 'string' && DEV_ORIGIN.length > 0;
7109
+ const origin = development ? DEV_ORIGIN : ABSOLUTE_MOBILE_MANIFEST?.productionOrigin;
7110
+ if (!origin) throw new Error('AbsoluteJS native route data has no trusted server origin.');
7111
+ const url = new URL(path, origin);
7112
+ if (url.origin !== new URL(origin).origin) throw new Error('AbsoluteJS native route data left its trusted origin.');
7113
+ const headers = new Headers();
7114
+ headers.set('accept', development ? NATIVE_DATA_MEDIA_TYPE : PAGE_MEDIA_TYPE);
7115
+ headers.set('x-absolute-mobile-protocol', String(PROTOCOL));
7116
+ let page: ReturnType<typeof productionPage> | undefined;
7117
+ if (!development) {
7118
+ page = productionPage(url.pathname);
7119
+ headers.set('x-absolute-mobile-app-build', ABSOLUTE_MOBILE_MANIFEST!.appBuild);
7120
+ headers.set('x-absolute-mobile-page-bundle', page.bundleHash);
7121
+ headers.set('x-absolute-mobile-page-contracts', page.contract);
7122
+ headers.set('x-absolute-mobile-page-id', page.pageId);
7123
+ headers.set('x-absolute-mobile-runtime', ABSOLUTE_MOBILE_MANIFEST!.runtime);
7124
+ }
7125
+ return { page, request: new Request(url, { headers, method: 'GET' }) };
7126
+ };
7127
+ const requestData = async <PageProps extends object>(path: string, signal: AbortSignal) => {
7128
+ const { page, request } = createDataRequest(path);
7129
+ const response = await ${auth ? "absoluteExpoAuth.fetchOptional" : "fetch"}(request, { redirect: 'manual', signal });
7130
+ if (new URL(response.url || request.url).origin !== new URL(request.url).origin || response.status >= 300 && response.status < 400) throw new Error('AbsoluteJS native route data redirected outside its contract.');
7131
+ const source = await response.text();
7132
+ if (new TextEncoder().encode(source).byteLength > MAX_DATA_BYTES) throw new Error('AbsoluteJS native route data exceeded 1 MiB.');
7133
+ let envelope: unknown;
7134
+ try { envelope = JSON.parse(source); } catch { throw new Error('The server did not return an AbsoluteJS page envelope.'); }
7135
+ if (!isRecord(envelope) || envelope.protocol !== PROTOCOL || !isRecord(envelope.response)) throw new Error('The server returned an invalid AbsoluteJS page envelope.');
7136
+ const result = envelope.response;
7137
+ if (result.kind === 'upgrade-required') throw new Error('This app version must be updated before opening this screen.');
7138
+ if (result.kind !== 'page' || !isRecord(result.props) || typeof result.pageId !== 'string' || typeof result.contract !== 'string') throw new Error('The server did not produce native route page props.');
7139
+ if (page && (result.pageId !== page.pageId || result.contract !== page.contract)) throw new Error('The server returned a different page contract than this app contains.');
7140
+ return result.props as PageProps;
7141
+ };
7142
+
7143
+ export const createAbsoluteNativeRoute = <
7144
+ PageProps extends object,
7145
+ Params extends object = RouteParams
7146
+ >(Component: ComponentType<NativeRouteProps<PageProps, Params>>, pattern: string) => {
7147
+ function AbsoluteNativeRouteScreen() {
7148
+ const pathname = usePathname() || '/';
7149
+ const params = useLocalSearchParams() as RouteParams;
7150
+ const parameterKey = JSON.stringify(params);
7151
+ const requestPath = useMemo(() => requestPathFor(pathname, params, pattern), [parameterKey, pathname]);
7152
+ const [revision, setRevision] = useState(0);
7153
+ const [state, setState] = useState<NativeRouteState<PageProps>>({ kind: 'loading' });
7154
+ useEffect(() => {
7155
+ const controller = new AbortController();
7156
+ setState({ kind: 'loading' });
7157
+ void requestData<PageProps>(requestPath, controller.signal).then(
7158
+ pageProps => { if (!controller.signal.aborted) setState({ kind: 'ready', pageProps }); },
7159
+ () => { if (!controller.signal.aborted) setState({ kind: 'error' }); }
7160
+ );
7161
+ return () => controller.abort();
7162
+ }, [requestPath, revision]);
7163
+ const reload = () => setRevision(value => value + 1);
7164
+ if (state.kind === 'loading') return <View style={styles.center}><ActivityIndicator accessibilityLabel="Loading screen" /></View>;
7165
+ if (state.kind === 'error') return <View style={styles.center}><Text accessibilityRole="alert" style={styles.error}>This screen could not load.</Text><Pressable accessibilityRole="button" onPress={reload} style={styles.button}><Text>Try again</Text></Pressable></View>;
7166
+ return <Component pageProps={state.pageProps} params={params as Params} reload={reload} />;
7167
+ }
7168
+ return AbsoluteNativeRouteScreen;
7169
+ };
7170
+
7171
+ const styles = StyleSheet.create({
7172
+ button: { backgroundColor: '#e2e8f0', borderRadius: 10, paddingHorizontal: 16, paddingVertical: 12 },
7173
+ center: { alignItems: 'center', flex: 1, gap: 16, justifyContent: 'center', padding: 24 },
7174
+ error: { color: '#b91c1c', fontSize: 16, textAlign: 'center' }
7175
+ });
7176
+ `;
7177
+ var nativeWrapperSource = (wrapper, module, runtime, route) => `${EXPO_GENERATED_HEADER}import ApplicationNativeRoute from ${JSON.stringify(portableRelative2(dirname10(wrapper), module).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
7178
+ import { createAbsoluteNativeRoute } from ${JSON.stringify(portableRelative2(dirname10(wrapper), runtime).replace(/\.(?:[cm]?[jt]sx?)$/u, ""))};
7179
+
7180
+ export default createAbsoluteNativeRoute(ApplicationNativeRoute, ${JSON.stringify(route)});
6919
7181
  `;
6920
7182
  var expoTsConfig = (projectRoot, project, auth, sync) => ({
6921
7183
  compilerOptions: {
@@ -6968,11 +7230,33 @@ var writeManagedFile = async (path, source, force) => {
6968
7230
  await rename11(temporary, path);
6969
7231
  return true;
6970
7232
  };
7233
+ var pruneStaleManagedExpoRoutes = async (project, expected) => {
7234
+ const appDirectory = join14(project, "app");
7235
+ if (!await exists3(appDirectory))
7236
+ return 0;
7237
+ const files = await walkFiles(appDirectory);
7238
+ const stale = (await Promise.all(files.map(async (path) => ({
7239
+ managed: path.endsWith(".tsx") && (await readFile14(path, "utf8")).startsWith(EXPO_GENERATED_HEADER),
7240
+ path
7241
+ })))).filter(({ managed, path }) => managed && !expected.has(path));
7242
+ await Promise.all(stale.map(({ path }) => rm9(path, { force: true })));
7243
+ return stale.length;
7244
+ };
6971
7245
  var jsonSource = (value) => `${JSON.stringify(value, null, "\t")}
6972
7246
  `;
6973
- var emptyWebAssetsSource = `${EXPO_GENERATED_HEADER}export const materializeAbsoluteWebBundle = async () => {
7247
+ var nativeDataManifestTypeSource = `type AbsoluteMobileManifest = {
7248
+ appBuild: string;
7249
+ pages: readonly { bundleHash: string; contract: string; pageId: string }[];
7250
+ productionOrigin: string;
7251
+ routes: readonly { method: 'GET'; pageId: string; pattern: string }[];
7252
+ runtime: string;
7253
+ };
7254
+ `;
7255
+ var emptyWebAssetsSource = `${EXPO_GENERATED_HEADER}${nativeDataManifestTypeSource}
7256
+ export const materializeAbsoluteWebBundle = async () => {
6974
7257
  throw new Error('The embedded AbsoluteJS bundle is unavailable. Run absolute prepare before a production Expo build.');
6975
7258
  };
7259
+ export const ABSOLUTE_MOBILE_MANIFEST: AbsoluteMobileManifest | undefined = undefined;
6976
7260
  `;
6977
7261
  var writeAbsoluteExpoProject = async (config, options) => {
6978
7262
  if (config.engine !== "expo")
@@ -7043,6 +7327,10 @@ node_modules/
7043
7327
  join14(project, "src", "generated", "AbsoluteDevices.ts"),
7044
7328
  devicesRuntimeSource(config, devices, authEnabled)
7045
7329
  ],
7330
+ [
7331
+ join14(project, "src", "generated", "AbsoluteNativeRoute.tsx"),
7332
+ nativeRouteRuntimeSource(authEnabled)
7333
+ ],
7046
7334
  [
7047
7335
  join14(project, "src", "generated", "AbsoluteWebHost.tsx"),
7048
7336
  webHostSource(config, auth, syncEnabled)
@@ -7062,12 +7350,14 @@ node_modules/
7062
7350
  files.set(join14(project, "app", "index.tsx"), webRouteSource);
7063
7351
  }
7064
7352
  files.set(join14(project, "app", "[...absolute].tsx"), catchAllRouteSource);
7353
+ const nativeRouteRuntime = join14(project, "src", "generated", "AbsoluteNativeRoute.tsx");
7065
7354
  for (const [route, module] of routeModules) {
7066
7355
  const wrapper = route === "/" ? join14(project, "app", "index.tsx") : routeFile(project, route);
7067
- files.set(wrapper, nativeWrapperSource(wrapper, module));
7356
+ files.set(wrapper, nativeWrapperSource(wrapper, module, nativeRouteRuntime, route));
7068
7357
  }
7358
+ const removed = await pruneStaleManagedExpoRoutes(project, new Set([...files.keys()].filter((path) => path.startsWith(`${join14(project, "app")}${sep6}`))));
7069
7359
  const changes = await Promise.all([...files].map(([path, source]) => writeManagedFile(path, source, true)));
7070
- const changed = changes.filter(Boolean).length;
7360
+ const changed = removed + changes.filter(Boolean).length;
7071
7361
  return { changed, path: project, written: [...files.keys()] };
7072
7362
  };
7073
7363
  var walkFiles = async (root, directory = root) => {
@@ -7106,11 +7396,13 @@ var installStagedDirectory = async (staging, destination) => {
7106
7396
  throw error;
7107
7397
  }
7108
7398
  };
7109
- var assetModuleSource = (assets, bundleId) => `${EXPO_GENERATED_HEADER}import { Asset } from 'expo-asset';
7399
+ var assetModuleSource = (assets, bundleId, manifest) => `${EXPO_GENERATED_HEADER}import { Asset } from 'expo-asset';
7110
7400
  import { Directory, File, Paths } from 'expo-file-system';
7111
7401
 
7402
+ ${nativeDataManifestTypeSource}
7112
7403
  declare const require: (path: string) => number;
7113
7404
  const BUNDLE_ID = ${JSON.stringify(bundleId)};
7405
+ export const ABSOLUTE_MOBILE_MANIFEST: AbsoluteMobileManifest = ${JSON.stringify(manifest)};
7114
7406
  const ASSETS = [
7115
7407
  ${assets.map(({ asset, path }) => ` { module: require(${JSON.stringify(asset)}), path: ${JSON.stringify(path)} }`).join(`,
7116
7408
  `)}
@@ -7144,9 +7436,8 @@ var syncAbsoluteExpoWebAssets = async (config) => {
7144
7436
  }
7145
7437
  const manifestPath = join14(config.bundleDirectory, "absolute-mobile-manifest.json");
7146
7438
  const manifest = JSON.parse(await readFile14(manifestPath, "utf8"));
7147
- const appBuild = typeof manifest === "object" && manifest !== null && typeof Reflect.get(manifest, "appBuild") === "string" ? String(Reflect.get(manifest, "appBuild")) : undefined;
7148
- if (!appBuild)
7149
- throw new TypeError("AbsoluteJS mobile manifest has no appBuild.");
7439
+ const nativeDataManifest = expoNativeDataManifest(manifest);
7440
+ const { appBuild } = nativeDataManifest;
7150
7441
  const files = await walkFiles(config.bundleDirectory);
7151
7442
  const bundleHash = createHash10("sha256");
7152
7443
  const filesWithContents = await Promise.all(files.map(async (file) => ({ contents: await readFile14(file), file })));
@@ -7176,7 +7467,7 @@ var syncAbsoluteExpoWebAssets = async (config) => {
7176
7467
  throw error;
7177
7468
  }
7178
7469
  const generated = join14(config.nativeProjectDirectory, "src", "generated", "webAssets.ts");
7179
- await writeManagedFile(generated, assetModuleSource(assets, bundleId), true);
7470
+ await writeManagedFile(generated, assetModuleSource(assets, bundleId, nativeDataManifest), true);
7180
7471
  return { appBuild, assets: assets.length, bundleId, path: destination };
7181
7472
  };
7182
7473
 
@@ -7493,7 +7784,7 @@ var ABSOLUTE_EXPO_BRIDGE_METHODS = [
7493
7784
  "sync.socket.open",
7494
7785
  "sync.socket.sendChunk"
7495
7786
  ];
7496
- var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
7787
+ var isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
7497
7788
  var validId = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,80}$/u.test(value);
7498
7789
  var validPath = (value) => typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && value.length <= 4096;
7499
7790
  var parseRequest = (value) => {
@@ -7503,7 +7794,7 @@ var parseRequest = (value) => {
7503
7794
  if (!method) {
7504
7795
  throw new TypeError("Expo bridge method is not allowed.");
7505
7796
  }
7506
- if (!isRecord8(value.params))
7797
+ if (!isRecord9(value.params))
7507
7798
  throw new TypeError("Expo bridge request params must be an object.");
7508
7799
  if (typeof value.path !== "string" || !validPath(value.path))
7509
7800
  throw new TypeError("Expo bridge request path is invalid.");
@@ -7539,7 +7830,7 @@ var parseRequest = (value) => {
7539
7830
  throw new TypeError("Expo bridge HTTP body exceeds 48 KiB.");
7540
7831
  if ((value.params.method === "GET" || value.params.method === "DELETE") && value.params.body !== undefined)
7541
7832
  throw new TypeError("Expo bridge HTTP method cannot contain a body.");
7542
- if (!isRecord8(value.params.headers))
7833
+ if (!isRecord9(value.params.headers))
7543
7834
  throw new TypeError("Expo bridge HTTP headers must be an object.");
7544
7835
  const headersAllowed = Object.entries(value.params.headers).every(([name, header]) => typeof header === "string" && (name.toLowerCase() === "accept" || name.toLowerCase() === "content-type" || name.toLowerCase().startsWith("x-absolute-mobile-")));
7545
7836
  if (!headersAllowed) {
@@ -7565,13 +7856,13 @@ var parseResponse = (value) => {
7565
7856
  if (typeof value.id !== "string" || !validId(value.id))
7566
7857
  throw new TypeError("Expo bridge response id is invalid.");
7567
7858
  if (value.error !== undefined) {
7568
- if (!isRecord8(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string") {
7859
+ if (!isRecord9(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string") {
7569
7860
  throw new TypeError("Expo bridge response error is invalid.");
7570
7861
  }
7571
7862
  if (value.result !== undefined)
7572
7863
  throw new TypeError("Expo bridge response cannot contain result and error.");
7573
7864
  }
7574
- if (isRecord8(value.error)) {
7865
+ if (isRecord9(value.error)) {
7575
7866
  return {
7576
7867
  error: {
7577
7868
  code: String(value.error.code),
@@ -7595,7 +7886,7 @@ var parseEvent = (value) => {
7595
7886
  }
7596
7887
  if (typeof value.path !== "string" || !validPath(value.path))
7597
7888
  throw new TypeError("Expo bridge event path is invalid.");
7598
- if (value.payload !== undefined && !isRecord8(value.payload)) {
7889
+ if (value.payload !== undefined && !isRecord9(value.payload)) {
7599
7890
  throw new TypeError("Expo bridge event payload must be an object.");
7600
7891
  }
7601
7892
  return {
@@ -7629,7 +7920,7 @@ var parseAbsoluteExpoBridgeMessage = (source) => {
7629
7920
  cause
7630
7921
  });
7631
7922
  }
7632
- if (!isRecord8(parsed) || parsed.format !== ABSOLUTE_EXPO_BRIDGE_FORMAT) {
7923
+ if (!isRecord9(parsed) || parsed.format !== ABSOLUTE_EXPO_BRIDGE_FORMAT) {
7633
7924
  throw new TypeError("Expo bridge message format is unsupported.");
7634
7925
  }
7635
7926
  if (parsed.kind === "request")
@@ -7987,7 +8278,7 @@ init_config();
7987
8278
  // src/mobile/ciWorkflow.ts
7988
8279
  import { existsSync as existsSync4 } from "fs";
7989
8280
  import { access as access11, mkdir as mkdir12, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
7990
- import { dirname as dirname11, extname as extname4, relative as relative11, resolve as resolve14, sep as sep6 } from "path";
8281
+ import { dirname as dirname11, extname as extname4, relative as relative11, resolve as resolve14, sep as sep7 } from "path";
7991
8282
  var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1;
7992
8283
  var SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
7993
8284
  var CI_ENV_INDENTATION = 6;
@@ -8019,7 +8310,7 @@ var projectPath = (projectRoot, value, field2, options = {}) => {
8019
8310
  const root = resolve14(projectRoot);
8020
8311
  const path = resolve14(root, value);
8021
8312
  const portable = relative11(root, path).replaceAll("\\", "/");
8022
- if (portable === ".." || portable.startsWith(`..${sep6}`) || portable.startsWith("../") || portable === "") {
8313
+ if (portable === ".." || portable.startsWith(`..${sep7}`) || portable.startsWith("../") || portable === "") {
8023
8314
  throw new TypeError(`${field2} must remain inside the project root.`);
8024
8315
  }
8025
8316
  if (/\r|\n/u.test(portable) || portable.startsWith("-"))
@@ -8033,7 +8324,7 @@ var workflowOutputPath = (projectRoot, value) => {
8033
8324
  const workflows = resolve14(root, ".github/workflows");
8034
8325
  const path = resolve14(root, value ?? ".github/workflows/absolute-mobile.yml");
8035
8326
  const portable = relative11(workflows, path);
8036
- if (portable === ".." || portable.startsWith(`..${sep6}`) || extname4(path) !== ".yml" && extname4(path) !== ".yaml") {
8327
+ if (portable === ".." || portable.startsWith(`..${sep7}`) || extname4(path) !== ".yml" && extname4(path) !== ".yaml") {
8037
8328
  throw new TypeError("mobile ci github --output must be a .yml or .yaml file inside .github/workflows.");
8038
8329
  }
8039
8330
  return path;
@@ -8627,7 +8918,7 @@ init_telemetryEvent();
8627
8918
  import { Elysia as Elysia3 } from "elysia";
8628
8919
  var ABSOLUTE_MOBILE_PREVIEW_PATH = "/__absolute/mobile-preview";
8629
8920
  var escapeHtml2 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
8630
- var isRecord9 = (value) => typeof value === "object" && value !== null;
8921
+ var isRecord10 = (value) => typeof value === "object" && value !== null;
8631
8922
  var normalizeEntry2 = (entry) => {
8632
8923
  const parsed = new URL(entry ?? "/", "https://absolute.invalid");
8633
8924
  return `${parsed.pathname}${parsed.search}${parsed.hash}`;
@@ -8678,7 +8969,7 @@ var createAbsoluteMobilePreviewPlugin = (mobile) => {
8678
8969
  "X-Robots-Tag": "noindex, nofollow"
8679
8970
  }
8680
8971
  })).post("/__absolute/mobile-preview-telemetry", ({ body, status }) => {
8681
- const value = isRecord9(body) ? body : undefined;
8972
+ const value = isRecord10(body) ? body : undefined;
8682
8973
  const durationMs = value?.durationMs;
8683
8974
  const platform2 = value?.platform;
8684
8975
  if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0 || durationMs > 300000 || platform2 !== "android" && platform2 !== "ios") {
@@ -9212,7 +9503,7 @@ init_nativeAuth();
9212
9503
 
9213
9504
  // src/mobile/releasePublisher.ts
9214
9505
  import { access as access12 } from "fs/promises";
9215
- import { isAbsolute as isAbsolute6, relative as relative12, resolve as resolve15, sep as sep7 } from "path";
9506
+ import { isAbsolute as isAbsolute6, relative as relative12, resolve as resolve15, sep as sep8 } from "path";
9216
9507
  import { pathToFileURL as pathToFileURL3 } from "url";
9217
9508
  var prepareAbsoluteIosRelease = async (publisher, options) => {
9218
9509
  if (typeof publisher.prepareIosRelease !== "function") {
@@ -9235,13 +9526,13 @@ var prepareAbsoluteAndroidRelease = async (publisher, options) => {
9235
9526
  }
9236
9527
  return versionCode;
9237
9528
  };
9238
- var isRecord10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9239
- var isPublisher = (value) => isRecord10(value) && typeof value.publish === "function";
9529
+ var isRecord11 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9530
+ var isPublisher = (value) => isRecord11(value) && typeof value.publish === "function";
9240
9531
  var publisherModulePath = (projectRoot, requested) => {
9241
9532
  const root = resolve15(projectRoot);
9242
9533
  const path = resolve15(root, requested);
9243
9534
  const projectRelative = relative12(root, path);
9244
- if (projectRelative === ".." || projectRelative.startsWith(`..${sep7}`) || isAbsolute6(projectRelative)) {
9535
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep8}`) || isAbsolute6(projectRelative)) {
9245
9536
  throw new TypeError("mobile publish --registry must remain inside the project.");
9246
9537
  }
9247
9538
  return path;
@@ -9252,7 +9543,7 @@ var loadAbsoluteNativeReleasePublisher = async (projectRoot, requestedModulePath
9252
9543
  throw new TypeError(`Native release registry module does not exist: ${modulePath}`);
9253
9544
  });
9254
9545
  const loaded = await import(pathToFileURL3(modulePath).href);
9255
- const publisher = isRecord10(loaded) ? loaded.default ?? loaded.registry : undefined;
9546
+ const publisher = isRecord11(loaded) ? loaded.default ?? loaded.registry : undefined;
9256
9547
  if (!isPublisher(publisher)) {
9257
9548
  throw new TypeError("Native release registry module must default-export a registry with publish(options).");
9258
9549
  }
@@ -10068,6 +10359,7 @@ export {
10068
10359
  ABSOLUTE_MOBILE_TRANSFORM_PROTOCOL,
10069
10360
  ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV,
10070
10361
  ABSOLUTE_NATIVE_AUTH_SCOPES,
10362
+ ABSOLUTE_NATIVE_ROUTE_DATA_MEDIA_TYPE,
10071
10363
  ABSOLUTE_REMOTE_MAC_EVENT_PREFIX,
10072
10364
  ABSOLUTE_REMOTE_MAC_PROTOCOL_VERSION,
10073
10365
  ABSOLUTE_SYNC_PACKAGE,
@@ -10082,6 +10374,7 @@ export {
10082
10374
  absoluteRemoteMacSshBase,
10083
10375
  absoluteRemoteProjectSyncCommands,
10084
10376
  acceptsAbsoluteMobilePage,
10377
+ acceptsAbsoluteNativeRouteData,
10085
10378
  activateAbsoluteMobilePage,
10086
10379
  applyAbsoluteNativeDeepLinks,
10087
10380
  applyAbsoluteNativeDeviceCapabilities,
@@ -10201,5 +10494,5 @@ export {
10201
10494
  writeAbsoluteMobileGithubWorkflow
10202
10495
  };
10203
10496
 
10204
- //# debugId=99CCB7E44742D46964756E2164756E21
10497
+ //# debugId=4795A6FB7632A5BE64756E2164756E21
10205
10498
  //# sourceMappingURL=index.js.map