@calo-design/cli 0.9.2 → 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.
- package/bin/mirror-push.js +105 -21
- 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);
|
|
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,107 @@ 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, 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.
|
|
403
|
+
fs.writeFileSync(
|
|
404
|
+
path.join(mirrorChromeDir, "mirror-switch.ts"),
|
|
405
|
+
`// Managed by calo-design push.
|
|
406
|
+
// The Mirror launcher opens prototypes with designchef://open/<slug>. Cases
|
|
407
|
+
// reaching THIS bundle:
|
|
408
|
+
// - the URL that opened this prototype survives the expo-updates reload as the
|
|
409
|
+
// initial URL (/open/<own slug>) — route it to "/" so expo-router doesn't
|
|
410
|
+
// render "Unmatched Route";
|
|
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";
|
|
416
|
+
import * as Updates from "expo-updates";
|
|
417
|
+
|
|
418
|
+
const PROTOTYPE = ${JSON.stringify(slug)};
|
|
419
|
+
const UPDATES_URL = ${JSON.stringify(UPDATES_URL)};
|
|
420
|
+
const RUNTIME_VERSION = ${JSON.stringify(RUNTIME_VERSION)};
|
|
421
|
+
|
|
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.
|
|
424
|
+
let switching = false;
|
|
425
|
+
function switchToChannel(channel: string): void {
|
|
426
|
+
if (switching) return;
|
|
427
|
+
let enabled = false;
|
|
428
|
+
try { enabled = Updates.isEnabled; } catch {}
|
|
429
|
+
if (!enabled) return; // dev/Expo Go — no runtime loading there
|
|
430
|
+
switching = true;
|
|
431
|
+
(async () => {
|
|
432
|
+
await Updates.setUpdateURLAndRequestHeadersOverride({
|
|
433
|
+
updateUrl: UPDATES_URL,
|
|
434
|
+
// EAS requires all three headers — channel alone is an HTTP 400.
|
|
435
|
+
requestHeaders: {
|
|
436
|
+
"expo-channel-name": channel,
|
|
437
|
+
"expo-runtime-version": Updates.runtimeVersion ?? RUNTIME_VERSION,
|
|
438
|
+
"expo-platform": Platform.OS,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
// Download BEFORE reloading; if the channel has nothing this throws and we
|
|
442
|
+
// stay put instead of reloading into nothing.
|
|
443
|
+
await Updates.fetchUpdateAsync();
|
|
444
|
+
// Defer the reload off the awaited chain (JSI teardown race — mirror.ts).
|
|
445
|
+
setTimeout(() => { void Updates.reloadAsync().catch(() => { switching = false; }); }, 300);
|
|
446
|
+
})().catch(() => { switching = false; });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// "/open/<slug>", "scheme://open/<slug>" (host parses as "open"), and
|
|
450
|
+
// "scheme:///open/<slug>" all mean the launcher; anything else is the
|
|
451
|
+
// prototype's own link. Returns the slug ("" for a bare /open), or null.
|
|
452
|
+
function launcherSlug(path: string): string | null {
|
|
453
|
+
try {
|
|
454
|
+
let pathname: string;
|
|
455
|
+
if (path.startsWith("/")) pathname = path;
|
|
456
|
+
else {
|
|
457
|
+
const url = new URL(path);
|
|
458
|
+
pathname = url.host === "open" ? "/open" + url.pathname : url.pathname;
|
|
459
|
+
}
|
|
460
|
+
const m = pathname.match(/^\\/open(?:\\/([^/?#]*))?(?:[/?#]|$)/);
|
|
461
|
+
return m ? decodeURIComponent(m[1] ?? "") : null;
|
|
462
|
+
} catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
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 {
|
|
469
|
+
const slug = launcherSlug(path);
|
|
470
|
+
if (slug === null) return null;
|
|
471
|
+
if (slug && slug !== PROTOTYPE) switchToChannel(slug);
|
|
472
|
+
return "/";
|
|
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
|
+
|
|
398
484
|
const existing = [".tsx", ".ts", ".jsx", ".js"].find((e) =>
|
|
399
485
|
fs.existsSync(path.join(stagedAppDir, "+native-intent" + e))
|
|
400
486
|
);
|
|
401
487
|
if (existing) {
|
|
402
|
-
warn("this prototype has its own +native-intent — leaving it alone.
|
|
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.");
|
|
403
489
|
return;
|
|
404
490
|
}
|
|
405
491
|
fs.writeFileSync(
|
|
406
492
|
path.join(stagedAppDir, "+native-intent.tsx"),
|
|
407
|
-
`// Managed by calo-design push.
|
|
408
|
-
|
|
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(\\/|\$)/;
|
|
493
|
+
`// Managed by calo-design push — see ../mirror-switch for the rationale.
|
|
494
|
+
import { handleLauncherPath } from "../mirror-switch";
|
|
412
495
|
|
|
413
496
|
export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
|
|
414
|
-
|
|
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 "/";
|
|
422
|
-
} catch {
|
|
423
|
-
// Unparseable URL — hand it back untouched rather than swallowing the link.
|
|
424
|
-
}
|
|
425
|
-
return path;
|
|
497
|
+
return handleLauncherPath(path) ?? path;
|
|
426
498
|
}
|
|
427
499
|
`
|
|
428
500
|
);
|
|
@@ -444,19 +516,26 @@ function runtimeHasSentry() {
|
|
|
444
516
|
// try/catch backstops that), so prototypes never crash BECAUSE of reporting.
|
|
445
517
|
function mirrorChromeSource({ slug, owner }) {
|
|
446
518
|
if (!SENTRY_DSN || !runtimeHasSentry()) {
|
|
447
|
-
return `import type
|
|
519
|
+
return `import { useEffect, type ReactNode } from "react";
|
|
520
|
+
import { subscribeLauncherLinks } from "./mirror-switch";
|
|
448
521
|
|
|
449
522
|
// Returning to the Mirror launcher is handled NATIVELY by the shell binary:
|
|
450
523
|
// shake the device (see calo-design-mirror/plugins/withCaloMirrorIos.js).
|
|
451
524
|
// Crash reporting was NOT injected: no Sentry DSN configured or the shared
|
|
452
525
|
// runtime predates @sentry/react-native (run \`calo-design update\`).
|
|
453
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
|
+
}, []);
|
|
454
532
|
return <>{children}</>;
|
|
455
533
|
}
|
|
456
534
|
`;
|
|
457
535
|
}
|
|
458
536
|
return `import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
459
537
|
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
538
|
+
import { subscribeLauncherLinks } from "./mirror-switch";
|
|
460
539
|
|
|
461
540
|
// Injected by \`calo-design push\` — crash reporting for this prototype.
|
|
462
541
|
// Returning to the Mirror launcher stays NATIVE: shake the device.
|
|
@@ -482,6 +561,11 @@ type CrashState = { error: Error | null };
|
|
|
482
561
|
export class MirrorChrome extends Component<{ children: ReactNode }, CrashState> {
|
|
483
562
|
state: CrashState = { error: null };
|
|
484
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
|
+
|
|
485
569
|
static getDerivedStateFromError(error: Error): CrashState {
|
|
486
570
|
return { error };
|
|
487
571
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calo-design/cli",
|
|
3
|
-
"version": "0.9.
|
|
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"
|