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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -167,7 +167,7 @@ var init_startupBanner = __esm(() => {
167
167
 
168
168
  // src/mobile/config.ts
169
169
  import { resolve as resolve6 } from "path";
170
- var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, HOSTNAME_PATTERN, resolveProjectPath = (projectRoot, value, field2) => {
170
+ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FINGERPRINT_PATTERN, HOSTNAME_PATTERN, EXPO_RESERVED_ROUTE_PREFIXES, resolveProjectPath = (projectRoot, value, field2) => {
171
171
  const root = resolve6(projectRoot);
172
172
  const path = resolve6(root, value);
173
173
  if (path !== root && !path.startsWith(`${root}/`)) {
@@ -240,11 +240,31 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
240
240
  }
241
241
  return value.match(/.{2}/g)?.join(":") ?? value;
242
242
  }))
243
- ].sort(), normalizeExpoNativeRoutes = (config, projectRoot) => {
243
+ ].sort(), validateExpoNativeRouteSegment = (path, segment, index, count, parameters) => {
244
+ if (segment === "*" && (index !== count - 1 || count === 1)) {
245
+ throw new TypeError(`mobile.routes.native route ${path} must use * once, as the final segment after a static or parameterized prefix.`);
246
+ }
247
+ if (segment === "*")
248
+ return;
249
+ if (!segment.startsWith(":") && (segment.includes("*") || segment.includes(":"))) {
250
+ throw new TypeError(`mobile.routes.native route ${path} contains invalid segment ${segment}.`);
251
+ }
252
+ if (!segment.startsWith(":"))
253
+ return;
254
+ const name = segment.slice(1);
255
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(name)) {
256
+ throw new TypeError(`mobile.routes.native route ${path} has invalid parameter ${segment}.`);
257
+ }
258
+ if (parameters.has(name)) {
259
+ throw new TypeError(`mobile.routes.native route ${path} repeats parameter ${segment}.`);
260
+ }
261
+ parameters.add(name);
262
+ }, normalizeExpoNativeRoutes = (config, projectRoot) => {
244
263
  if (config.engine !== "expo")
245
264
  return {};
246
265
  const routes = config.routes?.native ?? {};
247
266
  const normalized = {};
267
+ const ownership = new Map;
248
268
  for (const [route, module] of Object.entries(routes)) {
249
269
  const path = normalizeEntry(route);
250
270
  if (path.includes("?") || path.includes("#") || path !== "/" && path.endsWith("/")) {
@@ -253,9 +273,18 @@ var APP_ID_PATTERN, SCHEME_PATTERN, APPLE_APP_ID_PREFIX_PATTERN, CERTIFICATE_FIN
253
273
  if (path === "/__absolute/native") {
254
274
  throw new TypeError("mobile.routes.native reserves /__absolute/native for the Expo diagnostic screen.");
255
275
  }
256
- if (path.includes("*") || path.includes(":")) {
257
- throw new TypeError(`mobile.routes.native route ${path} must be static during the Expo experiment; parameters and wildcards are not supported yet.`);
276
+ const segments = path.split("/").filter(Boolean);
277
+ if (segments[0] && EXPO_RESERVED_ROUTE_PREFIXES.has(segments[0])) {
278
+ throw new TypeError(`mobile.routes.native route ${path} conflicts with an Expo Router or Metro reserved path.`);
279
+ }
280
+ const parameters = new Set;
281
+ segments.forEach((segment, index) => validateExpoNativeRouteSegment(path, segment, index, segments.length, parameters));
282
+ const signature = segments.map((segment) => segment.startsWith(":") ? ":" : segment).join("/");
283
+ const existing = ownership.get(signature);
284
+ if (existing) {
285
+ throw new TypeError(`mobile.routes.native routes ${existing} and ${path} claim the same Expo route pattern.`);
258
286
  }
287
+ ownership.set(signature, path);
259
288
  normalized[path] = resolveProjectPath(projectRoot, requireText(module, `mobile.routes.native[${path}]`), `mobile.routes.native[${path}]`);
260
289
  }
261
290
  return Object.fromEntries(Object.entries(normalized).sort(([left], [right]) => left.localeCompare(right)));
@@ -294,6 +323,16 @@ var init_config = __esm(() => {
294
323
  APPLE_APP_ID_PREFIX_PATTERN = /^[A-Z0-9]{10}$/;
295
324
  CERTIFICATE_FINGERPRINT_PATTERN = /^[0-9A-F]{64}$/;
296
325
  HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/;
326
+ EXPO_RESERVED_ROUTE_PREFIXES = new Set([
327
+ "_expo",
328
+ "_flight",
329
+ "_sitemap",
330
+ "assets",
331
+ "expo-dev-plugins",
332
+ "inspector",
333
+ "manifest",
334
+ "public"
335
+ ]);
297
336
  });
298
337
 
299
338
  // src/utils/stringModifiers.ts
@@ -6127,7 +6166,7 @@ import {
6127
6166
  writeFile as writeFile12
6128
6167
  } from "fs/promises";
6129
6168
  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";
6169
+ import { basename as basename4, dirname as dirname10, join as join14, relative as relative10, resolve as resolve12, sep as sep6 } from "path";
6131
6170
  var EXPO_GENERATED_HEADER = `// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.
6132
6171
  `;
6133
6172
  var EXPO_ASSET_EXTENSION = ".absasset";
@@ -6144,14 +6183,19 @@ var portableRelative2 = (from, destination) => {
6144
6183
  const value = relative10(from, destination).replaceAll("\\", "/");
6145
6184
  return value.startsWith(".") ? value : `./${value}`;
6146
6185
  };
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
- });
6186
+ var routeSegments = (route) => {
6187
+ const segments = route.split("/").filter(Boolean);
6188
+ return segments.map((segment, index) => {
6189
+ if (segment.startsWith(":"))
6190
+ return `[${segment.slice(1)}]`;
6191
+ if (segment === "*" && index === segments.length - 1)
6192
+ return "[...absoluteWildcard]";
6193
+ if (segment === "." || segment === ".." || !/^[A-Za-z0-9._~-]+$/u.test(segment)) {
6194
+ throw new TypeError(`Expo native route ${route} contains an unsupported segment ${segment}.`);
6195
+ }
6196
+ return segment;
6197
+ });
6198
+ };
6155
6199
  var routeFile = (project, route) => join14(project, "app", ...routeSegments(route), "index.tsx");
6156
6200
  var packageDependencies = (plan) => Object.fromEntries(plan.requiredPackages.map((spec) => {
6157
6201
  const separator = spec.lastIndexOf("@");
@@ -6629,6 +6673,7 @@ var webHostSource = (config, auth, sync) => {
6629
6673
  "/__absolute/native",
6630
6674
  ...Object.keys(config.expoNativeRoutes)
6631
6675
  ];
6676
+ const nativeRoutePatterns = nativeRoutes.map((route) => route.split("/").filter(Boolean));
6632
6677
  return `${EXPO_GENERATED_HEADER}import * as Linking from 'expo-linking';
6633
6678
  import { router, usePathname } from 'expo-router';
6634
6679
  import { useEffect, useRef, useState } from 'react';
@@ -6643,7 +6688,7 @@ ${sync ? "import { createAbsoluteExpoSyncBridge, startAbsoluteExpoSync } from '.
6643
6688
  const BRIDGE_FORMAT = 3;
6644
6689
  const MAX_MESSAGE_BYTES = 64 * 1024;
6645
6690
  const MAX_HTTP_BODY_BYTES = 48 * 1024;
6646
- const NATIVE_ROUTES = new Set(${JSON.stringify(nativeRoutes)});
6691
+ const NATIVE_ROUTE_PATTERNS = ${JSON.stringify(nativeRoutePatterns)};
6647
6692
  const PRODUCTION_ORIGIN = ${JSON.stringify(config.productionOrigin)};
6648
6693
  const DEV_ORIGIN = Platform.OS === 'android'
6649
6694
  ? process.env.EXPO_PUBLIC_ABSOLUTE_DEV_ANDROID_ORIGIN
@@ -6652,6 +6697,19 @@ const HMR_TARGET = Platform.OS === 'android' ? 'expo-android' : 'expo-ios';
6652
6697
  const AUTH_ENABLED = ${auth ? "true" : "false"};
6653
6698
  const SYNC_ENABLED = ${sync ? "true" : "false"};
6654
6699
 
6700
+ const isNativeRoute = (pathname: string) => {
6701
+ const segments = pathname.split('/').filter(Boolean);
6702
+ return NATIVE_ROUTE_PATTERNS.some(pattern => {
6703
+ for (let index = 0; index < pattern.length; index += 1) {
6704
+ const expected = pattern[index]!;
6705
+ if (expected === '*') return segments.length > index;
6706
+ if (segments[index] === undefined) return false;
6707
+ if (!expected.startsWith(':') && expected !== segments[index]) return false;
6708
+ }
6709
+ return segments.length === pattern.length;
6710
+ });
6711
+ };
6712
+
6655
6713
  const bridgeBootstrap = (path: string) => {
6656
6714
  const initialPath = DEV_ORIGIN
6657
6715
  ? 'location.pathname + location.search + location.hash'
@@ -6727,7 +6785,7 @@ const bridgeBootstrap = (path: string) => {
6727
6785
  const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null;
6728
6786
  if (!anchor) return;
6729
6787
  const url = new URL(anchor.href, location.href);
6730
- if (!${JSON.stringify(nativeRoutes)}.includes(url.pathname)) return;
6788
+ if (!isNativeRoute(url.pathname)) return;
6731
6789
  event.preventDefault();
6732
6790
  send({ format: 3, kind: 'event', event: 'navigation', path: url.pathname + url.search + url.hash });
6733
6791
  }, true);
@@ -6850,7 +6908,7 @@ export function AbsoluteWebHost() {
6850
6908
  if (message.kind === 'event' && (message.event === 'navigation' || message.event === 'ready')) {
6851
6909
  const target = new URL(message.path, PRODUCTION_ORIGIN);
6852
6910
  if (target.origin !== PRODUCTION_ORIGIN) return;
6853
- if (NATIVE_ROUTES.has(target.pathname)) router.push(message.path as never);
6911
+ if (isNativeRoute(target.pathname)) router.push(message.path as never);
6854
6912
  else activeWebPath.current = message.path;
6855
6913
  return;
6856
6914
  }
@@ -6968,6 +7026,18 @@ var writeManagedFile = async (path, source, force) => {
6968
7026
  await rename11(temporary, path);
6969
7027
  return true;
6970
7028
  };
7029
+ var pruneStaleManagedExpoRoutes = async (project, expected) => {
7030
+ const appDirectory = join14(project, "app");
7031
+ if (!await exists3(appDirectory))
7032
+ return 0;
7033
+ const files = await walkFiles(appDirectory);
7034
+ const stale = (await Promise.all(files.map(async (path) => ({
7035
+ managed: path.endsWith(".tsx") && (await readFile14(path, "utf8")).startsWith(EXPO_GENERATED_HEADER),
7036
+ path
7037
+ })))).filter(({ managed, path }) => managed && !expected.has(path));
7038
+ await Promise.all(stale.map(({ path }) => rm9(path, { force: true })));
7039
+ return stale.length;
7040
+ };
6971
7041
  var jsonSource = (value) => `${JSON.stringify(value, null, "\t")}
6972
7042
  `;
6973
7043
  var emptyWebAssetsSource = `${EXPO_GENERATED_HEADER}export const materializeAbsoluteWebBundle = async () => {
@@ -7066,8 +7136,9 @@ node_modules/
7066
7136
  const wrapper = route === "/" ? join14(project, "app", "index.tsx") : routeFile(project, route);
7067
7137
  files.set(wrapper, nativeWrapperSource(wrapper, module));
7068
7138
  }
7139
+ const removed = await pruneStaleManagedExpoRoutes(project, new Set([...files.keys()].filter((path) => path.startsWith(`${join14(project, "app")}${sep6}`))));
7069
7140
  const changes = await Promise.all([...files].map(([path, source]) => writeManagedFile(path, source, true)));
7070
- const changed = changes.filter(Boolean).length;
7141
+ const changed = removed + changes.filter(Boolean).length;
7071
7142
  return { changed, path: project, written: [...files.keys()] };
7072
7143
  };
7073
7144
  var walkFiles = async (root, directory = root) => {
@@ -7987,7 +8058,7 @@ init_config();
7987
8058
  // src/mobile/ciWorkflow.ts
7988
8059
  import { existsSync as existsSync4 } from "fs";
7989
8060
  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";
8061
+ import { dirname as dirname11, extname as extname4, relative as relative11, resolve as resolve14, sep as sep7 } from "path";
7991
8062
  var ABSOLUTE_MOBILE_CI_WORKFLOW_FORMAT = 1;
7992
8063
  var SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
7993
8064
  var CI_ENV_INDENTATION = 6;
@@ -8019,7 +8090,7 @@ var projectPath = (projectRoot, value, field2, options = {}) => {
8019
8090
  const root = resolve14(projectRoot);
8020
8091
  const path = resolve14(root, value);
8021
8092
  const portable = relative11(root, path).replaceAll("\\", "/");
8022
- if (portable === ".." || portable.startsWith(`..${sep6}`) || portable.startsWith("../") || portable === "") {
8093
+ if (portable === ".." || portable.startsWith(`..${sep7}`) || portable.startsWith("../") || portable === "") {
8023
8094
  throw new TypeError(`${field2} must remain inside the project root.`);
8024
8095
  }
8025
8096
  if (/\r|\n/u.test(portable) || portable.startsWith("-"))
@@ -8033,7 +8104,7 @@ var workflowOutputPath = (projectRoot, value) => {
8033
8104
  const workflows = resolve14(root, ".github/workflows");
8034
8105
  const path = resolve14(root, value ?? ".github/workflows/absolute-mobile.yml");
8035
8106
  const portable = relative11(workflows, path);
8036
- if (portable === ".." || portable.startsWith(`..${sep6}`) || extname4(path) !== ".yml" && extname4(path) !== ".yaml") {
8107
+ if (portable === ".." || portable.startsWith(`..${sep7}`) || extname4(path) !== ".yml" && extname4(path) !== ".yaml") {
8037
8108
  throw new TypeError("mobile ci github --output must be a .yml or .yaml file inside .github/workflows.");
8038
8109
  }
8039
8110
  return path;
@@ -9212,7 +9283,7 @@ init_nativeAuth();
9212
9283
 
9213
9284
  // src/mobile/releasePublisher.ts
9214
9285
  import { access as access12 } from "fs/promises";
9215
- import { isAbsolute as isAbsolute6, relative as relative12, resolve as resolve15, sep as sep7 } from "path";
9286
+ import { isAbsolute as isAbsolute6, relative as relative12, resolve as resolve15, sep as sep8 } from "path";
9216
9287
  import { pathToFileURL as pathToFileURL3 } from "url";
9217
9288
  var prepareAbsoluteIosRelease = async (publisher, options) => {
9218
9289
  if (typeof publisher.prepareIosRelease !== "function") {
@@ -9241,7 +9312,7 @@ var publisherModulePath = (projectRoot, requested) => {
9241
9312
  const root = resolve15(projectRoot);
9242
9313
  const path = resolve15(root, requested);
9243
9314
  const projectRelative = relative12(root, path);
9244
- if (projectRelative === ".." || projectRelative.startsWith(`..${sep7}`) || isAbsolute6(projectRelative)) {
9315
+ if (projectRelative === ".." || projectRelative.startsWith(`..${sep8}`) || isAbsolute6(projectRelative)) {
9245
9316
  throw new TypeError("mobile publish --registry must remain inside the project.");
9246
9317
  }
9247
9318
  return path;
@@ -10201,5 +10272,5 @@ export {
10201
10272
  writeAbsoluteMobileGithubWorkflow
10202
10273
  };
10203
10274
 
10204
- //# debugId=99CCB7E44742D46964756E2164756E21
10275
+ //# debugId=CFD74BC8ED7A634C64756E2164756E21
10205
10276
  //# sourceMappingURL=index.js.map