@calo-design/cli 0.9.2 → 0.9.3

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 (2) hide show
  1. package/bin/mirror-push.js +65 -17
  2. package/package.json +1 -1
@@ -382,7 +382,7 @@ export default function RootLayout() {
382
382
  }
383
383
  fs.writeFileSync(path.join(stagedAppDir, "_layout.tsx"), managed);
384
384
  fs.writeFileSync(path.join(mirrorChromeDir, "mirror-chrome.tsx"), mirrorChromeSource({ slug, owner }));
385
- injectNativeIntent(stagedAppDir);
385
+ injectNativeIntent(stagedAppDir, slug);
386
386
  }
387
387
 
388
388
  // The launcher opens a prototype via `designchef://open/<slug>`. That deep link is
@@ -394,35 +394,83 @@ export default function RootLayout() {
394
394
  // `+native-intent.tsx` is expo-router's hook for rewriting an incoming native URL
395
395
  // before routing: send the launcher's own path to the root and leave every other
396
396
  // deep link untouched, so a prototype's own links keep working.
397
- function injectNativeIntent(stagedAppDir) {
397
+ function injectNativeIntent(stagedAppDir, slug) {
398
398
  const existing = [".tsx", ".ts", ".jsx", ".js"].find((e) =>
399
399
  fs.existsSync(path.join(stagedAppDir, "+native-intent" + e))
400
400
  );
401
401
  if (existing) {
402
- warn("this prototype has its own +native-intent — leaving it alone. If opening by QR/link lands on \"Unmatched Route\", redirect paths starting with /open to \"/\".");
402
+ warn("this prototype has its own +native-intent — leaving it alone. Launcher URLs (/open/<slug>) won't be handled: route them to \"/\" for this prototype's slug and switch expo-updates channels for other slugs.");
403
403
  return;
404
404
  }
405
405
  fs.writeFileSync(
406
406
  path.join(stagedAppDir, "+native-intent.tsx"),
407
407
  `// Managed by calo-design push.
408
- // The Mirror launcher opens this prototype with designchef://open/<slug>. That URL
409
- // is still the native initial URL after the reload into this bundle, where /open/*
410
- // is not a route without this hook expo-router renders "Unmatched Route".
411
- const LAUNCHER_PATH = /^\\/open(\\/|\$)/;
408
+ // The Mirror launcher opens prototypes with designchef://open/<slug>. Two cases
409
+ // reach THIS bundle:
410
+ // - the URL that opened this prototype survives the expo-updates reload as the
411
+ // initial URL (/open/<own slug>) — route it to "/" so expo-router doesn't
412
+ // render "Unmatched Route";
413
+ // - a designchef://open/<other> link arrives while this bundle is live — the
414
+ // launcher's JS isn't running, so this bundle performs the channel switch
415
+ // itself: point expo-updates at the other channel, download, reload.
416
+ // ("mirror" is the launcher's own channel, so open/mirror = back to the feed.)
417
+ import { Platform } from "react-native";
418
+ import * as Updates from "expo-updates";
412
419
 
413
- export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
420
+ const PROTOTYPE = ${JSON.stringify(slug)};
421
+ const UPDATES_URL = ${JSON.stringify(UPDATES_URL)};
422
+ const RUNTIME_VERSION = ${JSON.stringify(RUNTIME_VERSION)};
423
+
424
+ // One switch at a time — a second reload racing the first crashes in JSI teardown
425
+ // (see calo-design-mirror src/lib/mirror.ts). Reset on failure so retry works.
426
+ let switching = false;
427
+ function switchToChannel(channel: string): void {
428
+ if (switching) return;
429
+ let enabled = false;
430
+ try { enabled = Updates.isEnabled; } catch {}
431
+ if (!enabled) return; // dev/Expo Go — no runtime loading there
432
+ switching = true;
433
+ (async () => {
434
+ await Updates.setUpdateURLAndRequestHeadersOverride({
435
+ updateUrl: UPDATES_URL,
436
+ // EAS requires all three headers — channel alone is an HTTP 400.
437
+ requestHeaders: {
438
+ "expo-channel-name": channel,
439
+ "expo-runtime-version": Updates.runtimeVersion ?? RUNTIME_VERSION,
440
+ "expo-platform": Platform.OS,
441
+ },
442
+ });
443
+ // Download BEFORE reloading; if the channel has nothing this throws and we
444
+ // stay put instead of reloading into nothing.
445
+ await Updates.fetchUpdateAsync();
446
+ // Defer the reload off the awaited chain (JSI teardown race — see mirror.ts).
447
+ setTimeout(() => { void Updates.reloadAsync().catch(() => { switching = false; }); }, 300);
448
+ })().catch(() => { switching = false; });
449
+ }
450
+
451
+ // "/open/<slug>", "scheme://open/<slug>" (host parses as "open"), and
452
+ // "scheme:///open/<slug>" all mean the launcher; anything else is the
453
+ // prototype's own link. Returns the slug ("" for a bare /open), or null.
454
+ function launcherSlug(path: string): string | null {
414
455
  try {
415
- // Bare pathname, e.g. "/open/<slug>".
416
- if (path.startsWith("/")) return LAUNCHER_PATH.test(path) ? "/" : path;
417
- const url = new URL(path);
418
- // Both shapes reach us: "designchef://open/<slug>" parses with host "open" and
419
- // pathname "/<slug>", while "designchef:///open/<slug>" has an empty host and
420
- // pathname "/open/<slug>". Treat either as the launcher handing us this app.
421
- if (url.host === "open" || LAUNCHER_PATH.test(url.pathname)) return "/";
456
+ let pathname: string;
457
+ if (path.startsWith("/")) pathname = path;
458
+ else {
459
+ const url = new URL(path);
460
+ pathname = url.host === "open" ? "/open" + url.pathname : url.pathname;
461
+ }
462
+ const m = pathname.match(/^\\/open(?:\\/([^/?#]*))?(?:[/?#]|$)/);
463
+ return m ? decodeURIComponent(m[1] ?? "") : null;
422
464
  } catch {
423
- // Unparseable URL — hand it back untouched rather than swallowing the link.
465
+ return null;
424
466
  }
425
- return path;
467
+ }
468
+
469
+ export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
470
+ const slug = launcherSlug(path);
471
+ if (slug === null) return path; // not a launcher URL — leave this prototype's own links alone
472
+ if (slug && slug !== PROTOTYPE) switchToChannel(slug);
473
+ return "/";
426
474
  }
427
475
  `
428
476
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calo-design/cli",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "One-line setup for Calo design tooling: logs in with your Calo email and installs the calo-design skill + design-system packages. No GitHub account needed.",
5
5
  "bin": {
6
6
  "calo-design": "bin/cli.js"