@calo-design/cli 0.9.3 → 0.9.4

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 +59 -23
  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, slug);
385
+ injectNativeIntent(stagedAppDir, mirrorChromeDir, slug);
386
386
  }
387
387
 
388
388
  // The launcher opens a prototype via `designchef://open/<slug>`. That deep link is
@@ -394,35 +394,33 @@ 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, slug) {
398
- const existing = [".tsx", ".ts", ".jsx", ".js"].find((e) =>
399
- fs.existsSync(path.join(stagedAppDir, "+native-intent" + e))
400
- );
401
- if (existing) {
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
- return;
404
- }
397
+ function injectNativeIntent(stagedAppDir, mirrorChromeDir, slug) {
398
+ // Shared launcher-URL logic: +native-intent covers COLD starts (the initial
399
+ // URL), and MirrorChrome's Linking listener covers WARM arrivals — on Android
400
+ // a URL delivered to the running app does not reliably reach expo-router's
401
+ // redirectSystemPath, which left designchef://open/<other> showing the
402
+ // currently-loaded prototype. The `switching` latch dedupes when both fire.
405
403
  fs.writeFileSync(
406
- path.join(stagedAppDir, "+native-intent.tsx"),
404
+ path.join(mirrorChromeDir, "mirror-switch.ts"),
407
405
  `// Managed by calo-design push.
408
- // The Mirror launcher opens prototypes with designchef://open/<slug>. Two cases
409
- // reach THIS bundle:
406
+ // The Mirror launcher opens prototypes with designchef://open/<slug>. Cases
407
+ // reaching THIS bundle:
410
408
  // - the URL that opened this prototype survives the expo-updates reload as the
411
409
  // initial URL (/open/<own slug>) — route it to "/" so expo-router doesn't
412
410
  // 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";
411
+ // - a designchef://open/<other> link arrives (cold OR while this bundle is
412
+ // live) — the launcher's JS isn't running, so this bundle performs the
413
+ // channel switch itself: point expo-updates at the other channel, download,
414
+ // reload. ("mirror" is the launcher's own channel open/mirror = the feed.)
415
+ import { Linking, Platform } from "react-native";
418
416
  import * as Updates from "expo-updates";
419
417
 
420
418
  const PROTOTYPE = ${JSON.stringify(slug)};
421
419
  const UPDATES_URL = ${JSON.stringify(UPDATES_URL)};
422
420
  const RUNTIME_VERSION = ${JSON.stringify(RUNTIME_VERSION)};
423
421
 
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.
422
+ // One switch at a time — a second reload racing the first crashes in JSI
423
+ // teardown (see calo-design-mirror src/lib/mirror.ts). Reset on failure.
426
424
  let switching = false;
427
425
  function switchToChannel(channel: string): void {
428
426
  if (switching) return;
@@ -443,7 +441,7 @@ function switchToChannel(channel: string): void {
443
441
  // Download BEFORE reloading; if the channel has nothing this throws and we
444
442
  // stay put instead of reloading into nothing.
445
443
  await Updates.fetchUpdateAsync();
446
- // Defer the reload off the awaited chain (JSI teardown race — see mirror.ts).
444
+ // Defer the reload off the awaited chain (JSI teardown race — mirror.ts).
447
445
  setTimeout(() => { void Updates.reloadAsync().catch(() => { switching = false; }); }, 300);
448
446
  })().catch(() => { switching = false; });
449
447
  }
@@ -466,12 +464,38 @@ function launcherSlug(path: string): string | null {
466
464
  }
467
465
  }
468
466
 
469
- export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
467
+ /** "/" when the URL is the launcher's (starting a switch if it names another prototype), null otherwise. */
468
+ export function handleLauncherPath(path: string): string | null {
470
469
  const slug = launcherSlug(path);
471
- if (slug === null) return path; // not a launcher URL — leave this prototype's own links alone
470
+ if (slug === null) return null;
472
471
  if (slug && slug !== PROTOTYPE) switchToChannel(slug);
473
472
  return "/";
474
473
  }
474
+
475
+ /** Warm-link coverage: subscribe to URLs arriving while this bundle runs. */
476
+ export function subscribeLauncherLinks(): { remove: () => void } {
477
+ return Linking.addEventListener("url", ({ url }) => {
478
+ handleLauncherPath(url);
479
+ });
480
+ }
481
+ `
482
+ );
483
+
484
+ const existing = [".tsx", ".ts", ".jsx", ".js"].find((e) =>
485
+ fs.existsSync(path.join(stagedAppDir, "+native-intent" + e))
486
+ );
487
+ if (existing) {
488
+ warn("this prototype has its own +native-intent — leaving it alone. Launcher URLs (/open/<slug>) won't be handled there: delegate to handleLauncherPath from ../mirror-switch.");
489
+ return;
490
+ }
491
+ fs.writeFileSync(
492
+ path.join(stagedAppDir, "+native-intent.tsx"),
493
+ `// Managed by calo-design push — see ../mirror-switch for the rationale.
494
+ import { handleLauncherPath } from "../mirror-switch";
495
+
496
+ export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
497
+ return handleLauncherPath(path) ?? path;
498
+ }
475
499
  `
476
500
  );
477
501
  }
@@ -492,19 +516,26 @@ function runtimeHasSentry() {
492
516
  // try/catch backstops that), so prototypes never crash BECAUSE of reporting.
493
517
  function mirrorChromeSource({ slug, owner }) {
494
518
  if (!SENTRY_DSN || !runtimeHasSentry()) {
495
- return `import type { ReactNode } from "react";
519
+ return `import { useEffect, type ReactNode } from "react";
520
+ import { subscribeLauncherLinks } from "./mirror-switch";
496
521
 
497
522
  // Returning to the Mirror launcher is handled NATIVELY by the shell binary:
498
523
  // shake the device (see calo-design-mirror/plugins/withCaloMirrorIos.js).
499
524
  // Crash reporting was NOT injected: no Sentry DSN configured or the shared
500
525
  // runtime predates @sentry/react-native (run \`calo-design update\`).
501
526
  export function MirrorChrome({ children }: { children: ReactNode }) {
527
+ // Warm launcher links (designchef://open/<other> while this bundle runs).
528
+ useEffect(() => {
529
+ const sub = subscribeLauncherLinks();
530
+ return () => sub.remove();
531
+ }, []);
502
532
  return <>{children}</>;
503
533
  }
504
534
  `;
505
535
  }
506
536
  return `import { Component, type ErrorInfo, type ReactNode } from "react";
507
537
  import { Pressable, ScrollView, Text, View } from "react-native";
538
+ import { subscribeLauncherLinks } from "./mirror-switch";
508
539
 
509
540
  // Injected by \`calo-design push\` — crash reporting for this prototype.
510
541
  // Returning to the Mirror launcher stays NATIVE: shake the device.
@@ -530,6 +561,11 @@ type CrashState = { error: Error | null };
530
561
  export class MirrorChrome extends Component<{ children: ReactNode }, CrashState> {
531
562
  state: CrashState = { error: null };
532
563
 
564
+ // Warm launcher links (designchef://open/<other> while this bundle runs).
565
+ private linkSub: { remove: () => void } | null = null;
566
+ componentDidMount(): void { this.linkSub = subscribeLauncherLinks(); }
567
+ componentWillUnmount(): void { this.linkSub?.remove(); }
568
+
533
569
  static getDerivedStateFromError(error: Error): CrashState {
534
570
  return { error };
535
571
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calo-design/cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
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"