@calo-design/cli 0.9.3 → 0.9.5
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.
- package/bin/mirror-push.js +85 -23
- package/package.json +1 -1
package/bin/mirror-push.js
CHANGED
|
@@ -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,34 @@ 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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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(
|
|
404
|
+
path.join(mirrorChromeDir, "mirror-switch.ts"),
|
|
407
405
|
`// Managed by calo-design push.
|
|
408
|
-
// The Mirror launcher opens prototypes with designchef://open/<slug>.
|
|
409
|
-
//
|
|
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
|
|
414
|
-
// launcher's JS isn't running, so this bundle performs the
|
|
415
|
-
// itself: point expo-updates at the other channel, download,
|
|
416
|
-
// ("mirror" is the launcher's own channel
|
|
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 { AppState, Platform } from "react-native";
|
|
416
|
+
import * as ExpoLinking from "expo-linking";
|
|
418
417
|
import * as Updates from "expo-updates";
|
|
419
418
|
|
|
420
419
|
const PROTOTYPE = ${JSON.stringify(slug)};
|
|
421
420
|
const UPDATES_URL = ${JSON.stringify(UPDATES_URL)};
|
|
422
421
|
const RUNTIME_VERSION = ${JSON.stringify(RUNTIME_VERSION)};
|
|
423
422
|
|
|
424
|
-
// One switch at a time — a second reload racing the first crashes in JSI
|
|
425
|
-
// (see calo-design-mirror src/lib/mirror.ts). Reset on failure
|
|
423
|
+
// One switch at a time — a second reload racing the first crashes in JSI
|
|
424
|
+
// teardown (see calo-design-mirror src/lib/mirror.ts). Reset on failure.
|
|
426
425
|
let switching = false;
|
|
427
426
|
function switchToChannel(channel: string): void {
|
|
428
427
|
if (switching) return;
|
|
@@ -443,7 +442,7 @@ function switchToChannel(channel: string): void {
|
|
|
443
442
|
// Download BEFORE reloading; if the channel has nothing this throws and we
|
|
444
443
|
// stay put instead of reloading into nothing.
|
|
445
444
|
await Updates.fetchUpdateAsync();
|
|
446
|
-
// Defer the reload off the awaited chain (JSI teardown race —
|
|
445
|
+
// Defer the reload off the awaited chain (JSI teardown race — mirror.ts).
|
|
447
446
|
setTimeout(() => { void Updates.reloadAsync().catch(() => { switching = false; }); }, 300);
|
|
448
447
|
})().catch(() => { switching = false; });
|
|
449
448
|
}
|
|
@@ -466,12 +465,63 @@ function launcherSlug(path: string): string | null {
|
|
|
466
465
|
}
|
|
467
466
|
}
|
|
468
467
|
|
|
469
|
-
|
|
468
|
+
/** "/" when the URL is the launcher's (starting a switch if it names another prototype), null otherwise. */
|
|
469
|
+
export function handleLauncherPath(path: string): string | null {
|
|
470
470
|
const slug = launcherSlug(path);
|
|
471
|
-
if (slug === null) return
|
|
471
|
+
if (slug === null) return null;
|
|
472
472
|
if (slug && slug !== PROTOTYPE) switchToChannel(slug);
|
|
473
473
|
return "/";
|
|
474
474
|
}
|
|
475
|
+
|
|
476
|
+
// Warm-link coverage. After Updates.reloadAsync() the React Native
|
|
477
|
+
// Linking 'url' event no longer reaches the reloaded JS context (verified on
|
|
478
|
+
// Android: the launcher's original context gets warm links, a prototype's
|
|
479
|
+
// reloaded context does not). expo-linking has its own native event path and
|
|
480
|
+
// URL store, so listen there — and on every foreground, sweep getLinkingURL()
|
|
481
|
+
// in case the event itself was missed. lastHandled stops the sweep from
|
|
482
|
+
// re-firing on URLs we've already acted on.
|
|
483
|
+
let lastHandled: string | null = null;
|
|
484
|
+
|
|
485
|
+
function handleWarmUrl(url: string | null): void {
|
|
486
|
+
if (!url || url === lastHandled) return;
|
|
487
|
+
lastHandled = url;
|
|
488
|
+
handleLauncherPath(url);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function subscribeLauncherLinks(): { remove: () => void } {
|
|
492
|
+
// Seed with the URL that launched this context so the foreground sweep
|
|
493
|
+
// doesn't replay it (+native-intent already handled the initial URL).
|
|
494
|
+
try { lastHandled = ExpoLinking.getLinkingURL() ?? null; } catch {}
|
|
495
|
+
const linkSub = ExpoLinking.addEventListener("url", ({ url }) => handleWarmUrl(url));
|
|
496
|
+
const stateSub = AppState.addEventListener("change", (state) => {
|
|
497
|
+
if (state !== "active") return;
|
|
498
|
+
try { handleWarmUrl(ExpoLinking.getLinkingURL() ?? null); } catch {}
|
|
499
|
+
});
|
|
500
|
+
return {
|
|
501
|
+
remove: () => {
|
|
502
|
+
linkSub.remove();
|
|
503
|
+
stateSub.remove();
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
`
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const existing = [".tsx", ".ts", ".jsx", ".js"].find((e) =>
|
|
511
|
+
fs.existsSync(path.join(stagedAppDir, "+native-intent" + e))
|
|
512
|
+
);
|
|
513
|
+
if (existing) {
|
|
514
|
+
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.");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
fs.writeFileSync(
|
|
518
|
+
path.join(stagedAppDir, "+native-intent.tsx"),
|
|
519
|
+
`// Managed by calo-design push — see ../mirror-switch for the rationale.
|
|
520
|
+
import { handleLauncherPath } from "../mirror-switch";
|
|
521
|
+
|
|
522
|
+
export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
|
|
523
|
+
return handleLauncherPath(path) ?? path;
|
|
524
|
+
}
|
|
475
525
|
`
|
|
476
526
|
);
|
|
477
527
|
}
|
|
@@ -492,19 +542,26 @@ function runtimeHasSentry() {
|
|
|
492
542
|
// try/catch backstops that), so prototypes never crash BECAUSE of reporting.
|
|
493
543
|
function mirrorChromeSource({ slug, owner }) {
|
|
494
544
|
if (!SENTRY_DSN || !runtimeHasSentry()) {
|
|
495
|
-
return `import type
|
|
545
|
+
return `import { useEffect, type ReactNode } from "react";
|
|
546
|
+
import { subscribeLauncherLinks } from "./mirror-switch";
|
|
496
547
|
|
|
497
548
|
// Returning to the Mirror launcher is handled NATIVELY by the shell binary:
|
|
498
549
|
// shake the device (see calo-design-mirror/plugins/withCaloMirrorIos.js).
|
|
499
550
|
// Crash reporting was NOT injected: no Sentry DSN configured or the shared
|
|
500
551
|
// runtime predates @sentry/react-native (run \`calo-design update\`).
|
|
501
552
|
export function MirrorChrome({ children }: { children: ReactNode }) {
|
|
553
|
+
// Warm launcher links (designchef://open/<other> while this bundle runs).
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
const sub = subscribeLauncherLinks();
|
|
556
|
+
return () => sub.remove();
|
|
557
|
+
}, []);
|
|
502
558
|
return <>{children}</>;
|
|
503
559
|
}
|
|
504
560
|
`;
|
|
505
561
|
}
|
|
506
562
|
return `import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
507
563
|
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
564
|
+
import { subscribeLauncherLinks } from "./mirror-switch";
|
|
508
565
|
|
|
509
566
|
// Injected by \`calo-design push\` — crash reporting for this prototype.
|
|
510
567
|
// Returning to the Mirror launcher stays NATIVE: shake the device.
|
|
@@ -530,6 +587,11 @@ type CrashState = { error: Error | null };
|
|
|
530
587
|
export class MirrorChrome extends Component<{ children: ReactNode }, CrashState> {
|
|
531
588
|
state: CrashState = { error: null };
|
|
532
589
|
|
|
590
|
+
// Warm launcher links (designchef://open/<other> while this bundle runs).
|
|
591
|
+
private linkSub: { remove: () => void } | null = null;
|
|
592
|
+
componentDidMount(): void { this.linkSub = subscribeLauncherLinks(); }
|
|
593
|
+
componentWillUnmount(): void { this.linkSub?.remove(); }
|
|
594
|
+
|
|
533
595
|
static getDerivedStateFromError(error: Error): CrashState {
|
|
534
596
|
return { error };
|
|
535
597
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calo-design/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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"
|