@archie/devtools 0.1.12 → 0.1.14

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.
@@ -46,15 +46,33 @@ type InjectOptions = {
46
46
  localScriptUrl?: string;
47
47
  };
48
48
 
49
+ type ArchieRouteFrame = 'phone' | 'tablet' | 'desktop';
50
+ /**
51
+ * Whitelisted subset of a React Router `handle` that is forwarded in
52
+ * ARCHIE_ROUTE_UPDATE. The router's `handle` is free-form (functions, React
53
+ * elements...) and is NOT structured-cloneable in general, so only `surface`
54
+ * (non-empty string) and `frame` ('phone' | 'tablet' | 'desktop') are copied;
55
+ * any other key and any invalid value is dropped. When neither survives the
56
+ * `handle` key is omitted from the entry entirely.
57
+ */
58
+ type ArchieRouteHandle = {
59
+ surface?: string;
60
+ frame?: ArchieRouteFrame;
61
+ };
49
62
  interface ArchieRouteObjectLike {
50
63
  id?: string;
64
+ handle?: unknown;
51
65
  path?: string;
52
66
  index?: boolean;
67
+ Component?: unknown;
68
+ element?: unknown;
69
+ lazy?: unknown;
53
70
  children?: readonly ArchieRouteObjectLike[];
54
71
  }
55
72
  interface ArchieRouteMatchLike {
56
73
  route: {
57
74
  id?: string;
75
+ handle?: unknown;
58
76
  };
59
77
  pathname: string;
60
78
  params: Record<string, string | undefined>;
@@ -66,27 +84,78 @@ interface ArchieRouterStateLike {
66
84
  hash: string;
67
85
  };
68
86
  matches: readonly ArchieRouteMatchLike[];
87
+ navigation?: {
88
+ state: string;
89
+ };
90
+ fetchers?: ReadonlyMap<string, {
91
+ state: string;
92
+ }>;
69
93
  }
70
94
  interface ArchieRouterLike {
71
95
  routes: readonly ArchieRouteObjectLike[];
72
96
  state: ArchieRouterStateLike;
97
+ navigate?(to: string, opts?: {
98
+ replace?: boolean;
99
+ }): Promise<void> | void;
73
100
  subscribe(listener: (state: ArchieRouterStateLike) => void): () => void;
74
101
  }
102
+ /**
103
+ * Route information for the sitemap
104
+ */
105
+ interface ArchieGuardEvidence {
106
+ componentName: string;
107
+ roles?: string[];
108
+ source: 'self' | 'ancestor';
109
+ ancestorId?: string;
110
+ }
111
+ interface RouteInfo {
112
+ id: string | undefined;
113
+ path: string;
114
+ handle?: ArchieRouteHandle;
115
+ hasChildren: boolean;
116
+ parentId: string | null;
117
+ index: boolean;
118
+ isLayout: boolean;
119
+ componentName: string | null;
120
+ guardEvidence: ArchieGuardEvidence[];
121
+ }
122
+ /**
123
+ * Type definition for the route update message
124
+ */
125
+ interface ArchieRouteUpdateMessage {
126
+ type: 'ARCHIE_ROUTE_UPDATE';
127
+ payload: {
128
+ path: string;
129
+ search: string;
130
+ hash: string;
131
+ matches: Array<{
132
+ id: string | undefined;
133
+ pathname: string;
134
+ params: Record<string, string | undefined>;
135
+ handle?: ArchieRouteHandle;
136
+ }>;
137
+ allRoutes: RouteInfo[];
138
+ layoutRoutes: RouteInfo[];
139
+ timestamp: number;
140
+ };
141
+ }
75
142
 
76
143
  interface ArchieDevToolsRuntimeOptions {
77
144
  router: ArchieRouterLike;
78
145
  inspector?: boolean;
79
146
  inspectorOptions?: InjectOptions;
147
+ /** Origins allowed to send NAVIGATE / SNAPSHOT_DOM to the route listener. */
148
+ routeListenerOrigins?: readonly string[];
80
149
  }
81
150
 
82
151
  interface ArchieDevToolProviderProps extends ArchieDevToolsRuntimeOptions {
83
152
  children: ReactNode;
84
153
  }
85
- declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
154
+ declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, routeListenerOrigins, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
86
155
 
87
156
  interface DndProviderProps {
88
157
  children: React.ReactNode;
89
158
  }
90
159
  declare function DndProvider({ children }: DndProviderProps): react_jsx_runtime.JSX.Element;
91
160
 
92
- export { ArchieDevToolProvider, type ArchieDevToolProviderProps, DndProvider };
161
+ export { ArchieDevToolProvider, type ArchieDevToolProviderProps, type ArchieRouteFrame, type ArchieRouteHandle, type ArchieRouteUpdateMessage, DndProvider, type RouteInfo };
@@ -46,15 +46,33 @@ type InjectOptions = {
46
46
  localScriptUrl?: string;
47
47
  };
48
48
 
49
+ type ArchieRouteFrame = 'phone' | 'tablet' | 'desktop';
50
+ /**
51
+ * Whitelisted subset of a React Router `handle` that is forwarded in
52
+ * ARCHIE_ROUTE_UPDATE. The router's `handle` is free-form (functions, React
53
+ * elements...) and is NOT structured-cloneable in general, so only `surface`
54
+ * (non-empty string) and `frame` ('phone' | 'tablet' | 'desktop') are copied;
55
+ * any other key and any invalid value is dropped. When neither survives the
56
+ * `handle` key is omitted from the entry entirely.
57
+ */
58
+ type ArchieRouteHandle = {
59
+ surface?: string;
60
+ frame?: ArchieRouteFrame;
61
+ };
49
62
  interface ArchieRouteObjectLike {
50
63
  id?: string;
64
+ handle?: unknown;
51
65
  path?: string;
52
66
  index?: boolean;
67
+ Component?: unknown;
68
+ element?: unknown;
69
+ lazy?: unknown;
53
70
  children?: readonly ArchieRouteObjectLike[];
54
71
  }
55
72
  interface ArchieRouteMatchLike {
56
73
  route: {
57
74
  id?: string;
75
+ handle?: unknown;
58
76
  };
59
77
  pathname: string;
60
78
  params: Record<string, string | undefined>;
@@ -66,27 +84,78 @@ interface ArchieRouterStateLike {
66
84
  hash: string;
67
85
  };
68
86
  matches: readonly ArchieRouteMatchLike[];
87
+ navigation?: {
88
+ state: string;
89
+ };
90
+ fetchers?: ReadonlyMap<string, {
91
+ state: string;
92
+ }>;
69
93
  }
70
94
  interface ArchieRouterLike {
71
95
  routes: readonly ArchieRouteObjectLike[];
72
96
  state: ArchieRouterStateLike;
97
+ navigate?(to: string, opts?: {
98
+ replace?: boolean;
99
+ }): Promise<void> | void;
73
100
  subscribe(listener: (state: ArchieRouterStateLike) => void): () => void;
74
101
  }
102
+ /**
103
+ * Route information for the sitemap
104
+ */
105
+ interface ArchieGuardEvidence {
106
+ componentName: string;
107
+ roles?: string[];
108
+ source: 'self' | 'ancestor';
109
+ ancestorId?: string;
110
+ }
111
+ interface RouteInfo {
112
+ id: string | undefined;
113
+ path: string;
114
+ handle?: ArchieRouteHandle;
115
+ hasChildren: boolean;
116
+ parentId: string | null;
117
+ index: boolean;
118
+ isLayout: boolean;
119
+ componentName: string | null;
120
+ guardEvidence: ArchieGuardEvidence[];
121
+ }
122
+ /**
123
+ * Type definition for the route update message
124
+ */
125
+ interface ArchieRouteUpdateMessage {
126
+ type: 'ARCHIE_ROUTE_UPDATE';
127
+ payload: {
128
+ path: string;
129
+ search: string;
130
+ hash: string;
131
+ matches: Array<{
132
+ id: string | undefined;
133
+ pathname: string;
134
+ params: Record<string, string | undefined>;
135
+ handle?: ArchieRouteHandle;
136
+ }>;
137
+ allRoutes: RouteInfo[];
138
+ layoutRoutes: RouteInfo[];
139
+ timestamp: number;
140
+ };
141
+ }
75
142
 
76
143
  interface ArchieDevToolsRuntimeOptions {
77
144
  router: ArchieRouterLike;
78
145
  inspector?: boolean;
79
146
  inspectorOptions?: InjectOptions;
147
+ /** Origins allowed to send NAVIGATE / SNAPSHOT_DOM to the route listener. */
148
+ routeListenerOrigins?: readonly string[];
80
149
  }
81
150
 
82
151
  interface ArchieDevToolProviderProps extends ArchieDevToolsRuntimeOptions {
83
152
  children: ReactNode;
84
153
  }
85
- declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
154
+ declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, routeListenerOrigins, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
86
155
 
87
156
  interface DndProviderProps {
88
157
  children: React.ReactNode;
89
158
  }
90
159
  declare function DndProvider({ children }: DndProviderProps): react_jsx_runtime.JSX.Element;
91
160
 
92
- export { ArchieDevToolProvider, type ArchieDevToolProviderProps, DndProvider };
161
+ export { ArchieDevToolProvider, type ArchieDevToolProviderProps, type ArchieRouteFrame, type ArchieRouteHandle, type ArchieRouteUpdateMessage, DndProvider, type RouteInfo };
@@ -10271,7 +10271,7 @@ function isPreviewOrigin(origin) {
10271
10271
  var DEFAULT_SCRIPT_ID = "archie-inspector-script";
10272
10272
  var R2_INSPECTOR_BASE_URL = "https://pub-bf238c278d9644b39ec34ecaca1d315c.r2.dev";
10273
10273
  function readBuildVersion() {
10274
- return "0.1.12" ? "0.1.12" : null;
10274
+ return "0.1.14" ? "0.1.14" : null;
10275
10275
  }
10276
10276
  var REMOTE_INSPECTOR_VERSION = readBuildVersion();
10277
10277
  var isRemoteInspectorUrlVersioned = REMOTE_INSPECTOR_VERSION !== null;
@@ -10351,25 +10351,257 @@ function injectInspector(opts = {}) {
10351
10351
  document.head.appendChild(script);
10352
10352
  }
10353
10353
 
10354
+ // src/client/route-listener/domSnapshot.ts
10355
+ var STRIP_SELECTOR = [
10356
+ "script",
10357
+ "noscript",
10358
+ "iframe",
10359
+ "[data-inspector-badge]",
10360
+ "[data-inspector-context-menu]",
10361
+ "#archie-inspector-styles"
10362
+ ].join(",");
10363
+ var PALETTE_SELECTOR = "#archie-design-palette";
10364
+ var CSS_FETCH_TIMEOUT_MS = 2e3;
10365
+ var IDLE_TIMEOUT_MS = 500;
10366
+ var CSS_URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
10367
+ function absolutizeCssUrls(css, baseHref) {
10368
+ return css.replace(CSS_URL_RE, (full, _quote, raw) => {
10369
+ if (!raw || raw.startsWith("data:") || raw.startsWith("#")) return full;
10370
+ try {
10371
+ return `url(${new URL(raw, baseHref).href})`;
10372
+ } catch {
10373
+ return full;
10374
+ }
10375
+ });
10376
+ }
10377
+ function isExcludedSheet(sheet) {
10378
+ const owner = sheet.ownerNode;
10379
+ return !!owner?.matches?.(`${STRIP_SELECTOR},${PALETTE_SELECTOR}`);
10380
+ }
10381
+ async function fetchCssText(href) {
10382
+ const controller = new AbortController();
10383
+ const timer = setTimeout(() => controller.abort(), CSS_FETCH_TIMEOUT_MS);
10384
+ try {
10385
+ const response = await fetch(href, {
10386
+ mode: "cors",
10387
+ signal: controller.signal
10388
+ });
10389
+ return response.ok ? await response.text() : "";
10390
+ } catch {
10391
+ return "";
10392
+ } finally {
10393
+ clearTimeout(timer);
10394
+ }
10395
+ }
10396
+ async function collectSheetCss(sheet, inlinedHrefs) {
10397
+ if (sheet.disabled || isExcludedSheet(sheet)) return "";
10398
+ const baseHref = sheet.href || document.baseURI;
10399
+ let css = "";
10400
+ try {
10401
+ css = Array.from(sheet.cssRules, (rule) => rule.cssText).join("\n");
10402
+ } catch {
10403
+ css = sheet.href ? await fetchCssText(sheet.href) : "";
10404
+ }
10405
+ if (!css) {
10406
+ const owner = sheet.ownerNode;
10407
+ if (owner?.tagName === "STYLE") css = owner.textContent ?? "";
10408
+ }
10409
+ if (!css) return "";
10410
+ if (sheet.href) inlinedHrefs.add(sheet.href);
10411
+ css = absolutizeCssUrls(css, baseHref);
10412
+ const media = sheet.media?.mediaText;
10413
+ return media && media !== "all" ? `@media ${media}{${css}}` : css;
10414
+ }
10415
+ async function collectDocumentCss(inlinedHrefs) {
10416
+ const sheets = Array.from(document.styleSheets);
10417
+ const parts = await Promise.all(
10418
+ sheets.map((sheet) => collectSheetCss(sheet, inlinedHrefs))
10419
+ );
10420
+ return parts.filter(Boolean).join("\n");
10421
+ }
10422
+ function waitForIdle() {
10423
+ return new Promise((resolve) => {
10424
+ const w = window;
10425
+ if (typeof w.requestIdleCallback === "function") {
10426
+ w.requestIdleCallback(() => resolve(), { timeout: IDLE_TIMEOUT_MS });
10427
+ } else {
10428
+ setTimeout(resolve, 50);
10429
+ }
10430
+ });
10431
+ }
10432
+ async function captureDomSnapshot() {
10433
+ if (document.fonts?.ready) {
10434
+ await document.fonts.ready;
10435
+ }
10436
+ await waitForIdle();
10437
+ const inlinedHrefs = /* @__PURE__ */ new Set();
10438
+ const css = await collectDocumentCss(inlinedHrefs);
10439
+ const clone = document.documentElement.cloneNode(true);
10440
+ clone.querySelectorAll(`${STRIP_SELECTOR}, style:not(${PALETTE_SELECTOR})`).forEach((el) => el.remove());
10441
+ clone.querySelectorAll('link[rel~="stylesheet"]').forEach((link) => {
10442
+ const href = link.href;
10443
+ if (inlinedHrefs.has(href)) {
10444
+ link.remove();
10445
+ } else {
10446
+ link.setAttribute("href", href);
10447
+ }
10448
+ });
10449
+ for (const el of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
10450
+ for (const attr of Array.from(el.attributes)) {
10451
+ if (/^on[a-z]+$/i.test(attr.name)) {
10452
+ el.removeAttribute(attr.name);
10453
+ }
10454
+ }
10455
+ }
10456
+ let head = clone.querySelector("head");
10457
+ if (!head) {
10458
+ head = document.createElement("head");
10459
+ clone.insertBefore(head, clone.firstChild);
10460
+ }
10461
+ const style = document.createElement("style");
10462
+ style.textContent = css;
10463
+ head.insertBefore(style, head.firstChild);
10464
+ if (!head.querySelector("base")) {
10465
+ const base = document.createElement("base");
10466
+ base.setAttribute("href", document.baseURI);
10467
+ head.insertBefore(base, head.firstChild);
10468
+ }
10469
+ return {
10470
+ html: "<!doctype html>" + clone.outerHTML,
10471
+ path: location.pathname + location.search + location.hash,
10472
+ viewport: { width: window.innerWidth, height: window.innerHeight },
10473
+ capturedAt: Date.now()
10474
+ };
10475
+ }
10476
+
10354
10477
  // src/client/route-listener/routeListener.ts
10355
- function extractAllRoutes(routes, parentPath = "") {
10478
+ var ROUTE_FRAMES = ["phone", "tablet", "desktop"];
10479
+ function pickRouteHandle(h) {
10480
+ try {
10481
+ if (typeof h !== "object" || h === null) return void 0;
10482
+ const raw = h;
10483
+ const surface = raw.surface;
10484
+ const frame = raw.frame;
10485
+ const out = {};
10486
+ if (typeof surface === "string" && surface !== "") out.surface = surface;
10487
+ if (typeof frame === "string" && ROUTE_FRAMES.includes(frame)) {
10488
+ out.frame = frame;
10489
+ }
10490
+ return out.surface !== void 0 || out.frame !== void 0 ? out : void 0;
10491
+ } catch {
10492
+ return void 0;
10493
+ }
10494
+ }
10495
+ var GUARD_NAME_RE = /guard|protect|require|auth(?!or)/i;
10496
+ var GUARD_PRIVATE_RE = /\bprivate|Private(?=[A-Z]|$)/;
10497
+ var NON_GUARD_SUFFIX_RE = /(Layout|Provider|Context|Page|Form|Screen|View)$/;
10498
+ function isGuardName(name) {
10499
+ return !NON_GUARD_SUFFIX_RE.test(name) && (GUARD_NAME_RE.test(name) || GUARD_PRIVATE_RE.test(name));
10500
+ }
10501
+ var GUARD_PROP_NAMES = [
10502
+ "roles",
10503
+ "allowedRoles",
10504
+ "requiredRoles",
10505
+ "permission",
10506
+ "permissions"
10507
+ ];
10508
+ var MAX_ELEMENT_DEPTH = 6;
10509
+ function typeName(type) {
10510
+ if (typeof type === "string") return type;
10511
+ if (typeof type === "function") {
10512
+ const fn = type;
10513
+ return fn.displayName || fn.name || null;
10514
+ }
10515
+ if (type && typeof type === "object") {
10516
+ const obj = type;
10517
+ return obj.displayName || typeName(obj.render) || typeName(obj.type);
10518
+ }
10519
+ return null;
10520
+ }
10521
+ function isElementLike(node) {
10522
+ return !!node && typeof node === "object" && "type" in node && "props" in node;
10523
+ }
10524
+ function literalRoles(value) {
10525
+ if (typeof value === "string") return [value];
10526
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
10527
+ return value;
10528
+ }
10529
+ return void 0;
10530
+ }
10531
+ function collectGuards(node, depth, out) {
10532
+ if (depth > MAX_ELEMENT_DEPTH || node == null) return;
10533
+ if (Array.isArray(node)) {
10534
+ for (const child of node) collectGuards(child, depth, out);
10535
+ return;
10536
+ }
10537
+ if (!isElementLike(node)) return;
10538
+ const props = node.props ?? {};
10539
+ const name = typeName(node.type);
10540
+ const roleProps = GUARD_PROP_NAMES.filter((p) => p in props);
10541
+ const isNamedGuard = typeof node.type !== "string" && !!name && isGuardName(name);
10542
+ if (isNamedGuard || roleProps.length > 0) {
10543
+ const roles = roleProps.flatMap((p) => literalRoles(props[p]) ?? []);
10544
+ out.push({
10545
+ componentName: name ?? "Anonymous",
10546
+ ...roles.length > 0 ? { roles } : {}
10547
+ });
10548
+ }
10549
+ collectGuards(props.children, depth + 1, out);
10550
+ }
10551
+ function routeComponentName(route) {
10552
+ if (route.Component) return typeName(route.Component);
10553
+ if (isElementLike(route.element)) return typeName(route.element.type);
10554
+ return null;
10555
+ }
10556
+ function extractAllRoutes(routes, parentPath = "", parentId = null, inheritedGuards = []) {
10356
10557
  const result = [];
10357
10558
  for (const route of routes) {
10358
10559
  const path = route.path ? (parentPath + "/" + route.path).replace(/\/+/g, "/") : parentPath || "/";
10359
- if (route.path !== void 0 || route.index) {
10560
+ const hasChildren = !!(route.children && route.children.length > 0);
10561
+ const isLayout = !route.path && !route.index && hasChildren;
10562
+ const own = [];
10563
+ collectGuards(route.element, 0, own);
10564
+ const handle = pickRouteHandle(route.handle);
10565
+ if (route.path !== void 0 || route.index || isLayout) {
10360
10566
  result.push({
10361
10567
  id: route.id,
10362
10568
  path: route.index ? parentPath || "/" : path,
10363
- hasChildren: !!(route.children && route.children.length > 0)
10569
+ ...handle ? { handle } : {},
10570
+ hasChildren,
10571
+ parentId,
10572
+ index: !!route.index,
10573
+ isLayout,
10574
+ componentName: routeComponentName(route),
10575
+ guardEvidence: [
10576
+ ...own.map((g) => ({ ...g, source: "self" })),
10577
+ ...inheritedGuards
10578
+ ]
10364
10579
  });
10365
10580
  }
10366
10581
  if (route.children) {
10367
- result.push(...extractAllRoutes(route.children, path));
10582
+ const forChildren = [
10583
+ ...own.map(
10584
+ (g) => ({
10585
+ ...g,
10586
+ source: "ancestor",
10587
+ ...route.id !== void 0 ? { ancestorId: route.id } : {}
10588
+ })
10589
+ ),
10590
+ ...inheritedGuards
10591
+ ];
10592
+ result.push(
10593
+ ...extractAllRoutes(
10594
+ route.children,
10595
+ path,
10596
+ route.id ?? parentId,
10597
+ forChildren
10598
+ )
10599
+ );
10368
10600
  }
10369
10601
  }
10370
10602
  return result;
10371
10603
  }
10372
- function broadcastRouteState(target, state, allRoutes) {
10604
+ function broadcastRouteState(target, state, routes) {
10373
10605
  try {
10374
10606
  const message = {
10375
10607
  type: "ARCHIE_ROUTE_UPDATE",
@@ -10377,12 +10609,18 @@ function broadcastRouteState(target, state, allRoutes) {
10377
10609
  path: state.location.pathname,
10378
10610
  search: state.location.search,
10379
10611
  hash: state.location.hash,
10380
- matches: state.matches.map((m) => ({
10381
- id: m.route.id,
10382
- pathname: m.pathname,
10383
- params: m.params
10384
- })),
10385
- allRoutes,
10612
+ matches: state.matches.map((m) => {
10613
+ const handle = pickRouteHandle(m.route.handle);
10614
+ return {
10615
+ id: m.route.id,
10616
+ pathname: m.pathname,
10617
+ params: m.params,
10618
+ ...handle ? { handle } : {}
10619
+ };
10620
+ }),
10621
+ // Layouts stay out of allRoutes so consumers that predate them don't list duplicates.
10622
+ allRoutes: routes.filter((r) => !r.isLayout),
10623
+ layoutRoutes: routes.filter((r) => r.isLayout),
10386
10624
  timestamp: Date.now()
10387
10625
  }
10388
10626
  };
@@ -10390,21 +10628,174 @@ function broadcastRouteState(target, state, allRoutes) {
10390
10628
  } catch {
10391
10629
  }
10392
10630
  }
10393
- function setupArchieRouteListener(router) {
10631
+ var SETTLE_QUIET_MS = 400;
10632
+ var SETTLE_TIMEOUT_MS = 8e3;
10633
+ var SETTLE_POLL_MS = 50;
10634
+ function readPackageVersion() {
10635
+ return true ? "0.1.14" : "unknown";
10636
+ }
10637
+ var DEFAULT_ARCHIE_COMMAND_ORIGINS = getAllowedOrigins(true);
10638
+ function originPatternToRegExp(pattern) {
10639
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:\*$/, ":\\d+").replace(/\*/g, "[a-z0-9.-]+");
10640
+ return new RegExp(`^${escaped}$`, "i");
10641
+ }
10642
+ function isAllowedCommandOrigin(origin, allowed) {
10643
+ return allowed.some((pattern) => originPatternToRegExp(pattern).test(origin));
10644
+ }
10645
+ function postToTarget(target, message, targetOrigin = "*") {
10646
+ try {
10647
+ target.postMessage(message, targetOrigin);
10648
+ } catch {
10649
+ }
10650
+ }
10651
+ function isRouterBusy(router) {
10652
+ const { navigation, fetchers } = router.state;
10653
+ if (navigation && navigation.state !== "idle") return true;
10654
+ if (fetchers) {
10655
+ for (const fetcher of fetchers.values()) {
10656
+ if (fetcher.state !== "idle") return true;
10657
+ }
10658
+ }
10659
+ return false;
10660
+ }
10661
+ function waitForSettled(router, navigation) {
10662
+ return new Promise((resolve) => {
10663
+ const startedAt = Date.now();
10664
+ let lastMutationAt = Date.now();
10665
+ let navigationDone = false;
10666
+ const observer = new MutationObserver(() => {
10667
+ lastMutationAt = Date.now();
10668
+ });
10669
+ observer.observe(document, {
10670
+ subtree: true,
10671
+ childList: true,
10672
+ attributes: true,
10673
+ characterData: true
10674
+ });
10675
+ navigation.then(
10676
+ () => {
10677
+ navigationDone = true;
10678
+ },
10679
+ () => {
10680
+ navigationDone = true;
10681
+ }
10682
+ );
10683
+ const finish = (settled) => {
10684
+ clearInterval(timer);
10685
+ observer.disconnect();
10686
+ resolve(settled);
10687
+ };
10688
+ const timer = setInterval(() => {
10689
+ const now = Date.now();
10690
+ if (navigationDone && !isRouterBusy(router) && now - lastMutationAt >= SETTLE_QUIET_MS) {
10691
+ finish(true);
10692
+ } else if (now - startedAt >= SETTLE_TIMEOUT_MS) {
10693
+ finish(false);
10694
+ }
10695
+ }, SETTLE_POLL_MS);
10696
+ });
10697
+ }
10698
+ function pathnameOf(path) {
10699
+ return path.split(/[?#]/, 1)[0];
10700
+ }
10701
+ async function handleNavigate(router, target, targetOrigin, data) {
10702
+ if (typeof router.navigate !== "function") {
10703
+ const { location: location3 } = router.state;
10704
+ postToTarget(target, {
10705
+ type: "ARCHIE_NAVIGATE_RESULT",
10706
+ requestId: data.requestId,
10707
+ requestedPath: data.path,
10708
+ finalPath: location3.pathname + location3.search + location3.hash,
10709
+ matchedRouteIds: [],
10710
+ redirected: false,
10711
+ settled: false
10712
+ }, targetOrigin);
10713
+ return;
10714
+ }
10715
+ let navigation = Promise.resolve();
10716
+ try {
10717
+ navigation = Promise.resolve(
10718
+ router.navigate(data.path, { replace: data.replace })
10719
+ );
10720
+ } catch {
10721
+ navigation = Promise.resolve();
10722
+ }
10723
+ const settled = await waitForSettled(router, navigation);
10724
+ const { location: location2, matches } = router.state;
10725
+ postToTarget(target, {
10726
+ type: "ARCHIE_NAVIGATE_RESULT",
10727
+ requestId: data.requestId,
10728
+ requestedPath: data.path,
10729
+ finalPath: location2.pathname + location2.search + location2.hash,
10730
+ matchedRouteIds: matches.map((m) => m.route.id).filter((id) => typeof id === "string"),
10731
+ redirected: location2.pathname !== pathnameOf(data.path),
10732
+ settled
10733
+ }, targetOrigin);
10734
+ }
10735
+ async function handleSnapshot(target, targetOrigin, requestId) {
10736
+ try {
10737
+ const snapshot = await captureDomSnapshot();
10738
+ postToTarget(target, {
10739
+ type: "ARCHIE_SNAPSHOT_DOM_RESULT",
10740
+ requestId,
10741
+ ...snapshot
10742
+ }, targetOrigin);
10743
+ } catch (error) {
10744
+ postToTarget(target, {
10745
+ type: "ARCHIE_SNAPSHOT_DOM_FAILED",
10746
+ requestId,
10747
+ error: error instanceof Error ? error.message : String(error)
10748
+ }, targetOrigin);
10749
+ }
10750
+ }
10751
+ function broadcastCapabilities(router, target) {
10752
+ postToTarget(target, {
10753
+ type: "ARCHIE_CAPABILITIES",
10754
+ version: readPackageVersion(),
10755
+ capabilities: {
10756
+ navigate: typeof router.navigate === "function",
10757
+ snapshot: true,
10758
+ identityOverride: false
10759
+ }
10760
+ });
10761
+ }
10762
+ function setupArchieRouteListener(router, options = {}) {
10763
+ const allowedOrigins = options.allowedOrigins ?? DEFAULT_ARCHIE_COMMAND_ORIGINS;
10394
10764
  const target = window.parent !== window ? window.parent : window.opener;
10395
10765
  if (!target) {
10396
10766
  return () => {
10397
10767
  };
10398
10768
  }
10399
10769
  broadcastRouteState(target, router.state, extractAllRoutes(router.routes));
10770
+ broadcastCapabilities(router, target);
10400
10771
  const unsubscribe = router.subscribe((state) => {
10401
10772
  const currentRoutes = extractAllRoutes(router.routes);
10402
10773
  broadcastRouteState(target, state, currentRoutes);
10403
10774
  });
10404
10775
  const handleMessage = (event) => {
10405
- if (event.data?.type === "ARCHIE_QUERY_ROUTES") {
10406
- const currentRoutes = extractAllRoutes(router.routes);
10407
- broadcastRouteState(target, router.state, currentRoutes);
10776
+ if (event.source !== target) return;
10777
+ const data = event.data;
10778
+ switch (data?.type) {
10779
+ case "ARCHIE_QUERY_ROUTES":
10780
+ broadcastRouteState(
10781
+ target,
10782
+ router.state,
10783
+ extractAllRoutes(router.routes)
10784
+ );
10785
+ break;
10786
+ case "ARCHIE_QUERY_CAPABILITIES":
10787
+ broadcastCapabilities(router, target);
10788
+ break;
10789
+ case "ARCHIE_NAVIGATE":
10790
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string" && typeof data.path === "string") {
10791
+ void handleNavigate(router, target, event.origin, data);
10792
+ }
10793
+ break;
10794
+ case "ARCHIE_SNAPSHOT_DOM":
10795
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string") {
10796
+ void handleSnapshot(target, event.origin, data.requestId);
10797
+ }
10798
+ break;
10408
10799
  }
10409
10800
  };
10410
10801
  window.addEventListener("message", handleMessage);
@@ -10444,7 +10835,8 @@ function installDomMetaSanitizer() {
10444
10835
  function useArchieDevToolsRuntime({
10445
10836
  router,
10446
10837
  inspector,
10447
- inspectorOptions
10838
+ inspectorOptions,
10839
+ routeListenerOrigins
10448
10840
  }) {
10449
10841
  installDomMetaSanitizer();
10450
10842
  const stableOptionsKey = JSON.stringify(inspectorOptions ?? null);
@@ -10454,7 +10846,9 @@ function useArchieDevToolsRuntime({
10454
10846
  if (process.env.NODE_ENV !== "development") {
10455
10847
  return;
10456
10848
  }
10457
- const unsubscribe = setupArchieRouteListener(router);
10849
+ const unsubscribe = setupArchieRouteListener(router, {
10850
+ allowedOrigins: routeListenerOrigins
10851
+ });
10458
10852
  if (inspector !== false) {
10459
10853
  const useLocalInspector = readArchieLocalInspectorFlag();
10460
10854
  injectInspector({
@@ -10472,9 +10866,15 @@ function ArchieDevToolProvider({
10472
10866
  children,
10473
10867
  router,
10474
10868
  inspector,
10475
- inspectorOptions
10869
+ inspectorOptions,
10870
+ routeListenerOrigins
10476
10871
  }) {
10477
- useArchieDevToolsRuntime({ router, inspector, inspectorOptions });
10872
+ useArchieDevToolsRuntime({
10873
+ router,
10874
+ inspector,
10875
+ inspectorOptions,
10876
+ routeListenerOrigins
10877
+ });
10478
10878
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DndProvider, { children });
10479
10879
  }
10480
10880
  // Annotate the CommonJS export names for ESM import in node:
@@ -10236,7 +10236,7 @@ function isPreviewOrigin(origin) {
10236
10236
  var DEFAULT_SCRIPT_ID = "archie-inspector-script";
10237
10237
  var R2_INSPECTOR_BASE_URL = "https://pub-bf238c278d9644b39ec34ecaca1d315c.r2.dev";
10238
10238
  function readBuildVersion() {
10239
- return "0.1.12" ? "0.1.12" : null;
10239
+ return "0.1.14" ? "0.1.14" : null;
10240
10240
  }
10241
10241
  var REMOTE_INSPECTOR_VERSION = readBuildVersion();
10242
10242
  var isRemoteInspectorUrlVersioned = REMOTE_INSPECTOR_VERSION !== null;
@@ -10316,25 +10316,257 @@ function injectInspector(opts = {}) {
10316
10316
  document.head.appendChild(script);
10317
10317
  }
10318
10318
 
10319
+ // src/client/route-listener/domSnapshot.ts
10320
+ var STRIP_SELECTOR = [
10321
+ "script",
10322
+ "noscript",
10323
+ "iframe",
10324
+ "[data-inspector-badge]",
10325
+ "[data-inspector-context-menu]",
10326
+ "#archie-inspector-styles"
10327
+ ].join(",");
10328
+ var PALETTE_SELECTOR = "#archie-design-palette";
10329
+ var CSS_FETCH_TIMEOUT_MS = 2e3;
10330
+ var IDLE_TIMEOUT_MS = 500;
10331
+ var CSS_URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
10332
+ function absolutizeCssUrls(css, baseHref) {
10333
+ return css.replace(CSS_URL_RE, (full, _quote, raw) => {
10334
+ if (!raw || raw.startsWith("data:") || raw.startsWith("#")) return full;
10335
+ try {
10336
+ return `url(${new URL(raw, baseHref).href})`;
10337
+ } catch {
10338
+ return full;
10339
+ }
10340
+ });
10341
+ }
10342
+ function isExcludedSheet(sheet) {
10343
+ const owner = sheet.ownerNode;
10344
+ return !!owner?.matches?.(`${STRIP_SELECTOR},${PALETTE_SELECTOR}`);
10345
+ }
10346
+ async function fetchCssText(href) {
10347
+ const controller = new AbortController();
10348
+ const timer = setTimeout(() => controller.abort(), CSS_FETCH_TIMEOUT_MS);
10349
+ try {
10350
+ const response = await fetch(href, {
10351
+ mode: "cors",
10352
+ signal: controller.signal
10353
+ });
10354
+ return response.ok ? await response.text() : "";
10355
+ } catch {
10356
+ return "";
10357
+ } finally {
10358
+ clearTimeout(timer);
10359
+ }
10360
+ }
10361
+ async function collectSheetCss(sheet, inlinedHrefs) {
10362
+ if (sheet.disabled || isExcludedSheet(sheet)) return "";
10363
+ const baseHref = sheet.href || document.baseURI;
10364
+ let css = "";
10365
+ try {
10366
+ css = Array.from(sheet.cssRules, (rule) => rule.cssText).join("\n");
10367
+ } catch {
10368
+ css = sheet.href ? await fetchCssText(sheet.href) : "";
10369
+ }
10370
+ if (!css) {
10371
+ const owner = sheet.ownerNode;
10372
+ if (owner?.tagName === "STYLE") css = owner.textContent ?? "";
10373
+ }
10374
+ if (!css) return "";
10375
+ if (sheet.href) inlinedHrefs.add(sheet.href);
10376
+ css = absolutizeCssUrls(css, baseHref);
10377
+ const media = sheet.media?.mediaText;
10378
+ return media && media !== "all" ? `@media ${media}{${css}}` : css;
10379
+ }
10380
+ async function collectDocumentCss(inlinedHrefs) {
10381
+ const sheets = Array.from(document.styleSheets);
10382
+ const parts = await Promise.all(
10383
+ sheets.map((sheet) => collectSheetCss(sheet, inlinedHrefs))
10384
+ );
10385
+ return parts.filter(Boolean).join("\n");
10386
+ }
10387
+ function waitForIdle() {
10388
+ return new Promise((resolve) => {
10389
+ const w = window;
10390
+ if (typeof w.requestIdleCallback === "function") {
10391
+ w.requestIdleCallback(() => resolve(), { timeout: IDLE_TIMEOUT_MS });
10392
+ } else {
10393
+ setTimeout(resolve, 50);
10394
+ }
10395
+ });
10396
+ }
10397
+ async function captureDomSnapshot() {
10398
+ if (document.fonts?.ready) {
10399
+ await document.fonts.ready;
10400
+ }
10401
+ await waitForIdle();
10402
+ const inlinedHrefs = /* @__PURE__ */ new Set();
10403
+ const css = await collectDocumentCss(inlinedHrefs);
10404
+ const clone = document.documentElement.cloneNode(true);
10405
+ clone.querySelectorAll(`${STRIP_SELECTOR}, style:not(${PALETTE_SELECTOR})`).forEach((el) => el.remove());
10406
+ clone.querySelectorAll('link[rel~="stylesheet"]').forEach((link) => {
10407
+ const href = link.href;
10408
+ if (inlinedHrefs.has(href)) {
10409
+ link.remove();
10410
+ } else {
10411
+ link.setAttribute("href", href);
10412
+ }
10413
+ });
10414
+ for (const el of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
10415
+ for (const attr of Array.from(el.attributes)) {
10416
+ if (/^on[a-z]+$/i.test(attr.name)) {
10417
+ el.removeAttribute(attr.name);
10418
+ }
10419
+ }
10420
+ }
10421
+ let head = clone.querySelector("head");
10422
+ if (!head) {
10423
+ head = document.createElement("head");
10424
+ clone.insertBefore(head, clone.firstChild);
10425
+ }
10426
+ const style = document.createElement("style");
10427
+ style.textContent = css;
10428
+ head.insertBefore(style, head.firstChild);
10429
+ if (!head.querySelector("base")) {
10430
+ const base = document.createElement("base");
10431
+ base.setAttribute("href", document.baseURI);
10432
+ head.insertBefore(base, head.firstChild);
10433
+ }
10434
+ return {
10435
+ html: "<!doctype html>" + clone.outerHTML,
10436
+ path: location.pathname + location.search + location.hash,
10437
+ viewport: { width: window.innerWidth, height: window.innerHeight },
10438
+ capturedAt: Date.now()
10439
+ };
10440
+ }
10441
+
10319
10442
  // src/client/route-listener/routeListener.ts
10320
- function extractAllRoutes(routes, parentPath = "") {
10443
+ var ROUTE_FRAMES = ["phone", "tablet", "desktop"];
10444
+ function pickRouteHandle(h) {
10445
+ try {
10446
+ if (typeof h !== "object" || h === null) return void 0;
10447
+ const raw = h;
10448
+ const surface = raw.surface;
10449
+ const frame = raw.frame;
10450
+ const out = {};
10451
+ if (typeof surface === "string" && surface !== "") out.surface = surface;
10452
+ if (typeof frame === "string" && ROUTE_FRAMES.includes(frame)) {
10453
+ out.frame = frame;
10454
+ }
10455
+ return out.surface !== void 0 || out.frame !== void 0 ? out : void 0;
10456
+ } catch {
10457
+ return void 0;
10458
+ }
10459
+ }
10460
+ var GUARD_NAME_RE = /guard|protect|require|auth(?!or)/i;
10461
+ var GUARD_PRIVATE_RE = /\bprivate|Private(?=[A-Z]|$)/;
10462
+ var NON_GUARD_SUFFIX_RE = /(Layout|Provider|Context|Page|Form|Screen|View)$/;
10463
+ function isGuardName(name) {
10464
+ return !NON_GUARD_SUFFIX_RE.test(name) && (GUARD_NAME_RE.test(name) || GUARD_PRIVATE_RE.test(name));
10465
+ }
10466
+ var GUARD_PROP_NAMES = [
10467
+ "roles",
10468
+ "allowedRoles",
10469
+ "requiredRoles",
10470
+ "permission",
10471
+ "permissions"
10472
+ ];
10473
+ var MAX_ELEMENT_DEPTH = 6;
10474
+ function typeName(type) {
10475
+ if (typeof type === "string") return type;
10476
+ if (typeof type === "function") {
10477
+ const fn = type;
10478
+ return fn.displayName || fn.name || null;
10479
+ }
10480
+ if (type && typeof type === "object") {
10481
+ const obj = type;
10482
+ return obj.displayName || typeName(obj.render) || typeName(obj.type);
10483
+ }
10484
+ return null;
10485
+ }
10486
+ function isElementLike(node) {
10487
+ return !!node && typeof node === "object" && "type" in node && "props" in node;
10488
+ }
10489
+ function literalRoles(value) {
10490
+ if (typeof value === "string") return [value];
10491
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
10492
+ return value;
10493
+ }
10494
+ return void 0;
10495
+ }
10496
+ function collectGuards(node, depth, out) {
10497
+ if (depth > MAX_ELEMENT_DEPTH || node == null) return;
10498
+ if (Array.isArray(node)) {
10499
+ for (const child of node) collectGuards(child, depth, out);
10500
+ return;
10501
+ }
10502
+ if (!isElementLike(node)) return;
10503
+ const props = node.props ?? {};
10504
+ const name = typeName(node.type);
10505
+ const roleProps = GUARD_PROP_NAMES.filter((p) => p in props);
10506
+ const isNamedGuard = typeof node.type !== "string" && !!name && isGuardName(name);
10507
+ if (isNamedGuard || roleProps.length > 0) {
10508
+ const roles = roleProps.flatMap((p) => literalRoles(props[p]) ?? []);
10509
+ out.push({
10510
+ componentName: name ?? "Anonymous",
10511
+ ...roles.length > 0 ? { roles } : {}
10512
+ });
10513
+ }
10514
+ collectGuards(props.children, depth + 1, out);
10515
+ }
10516
+ function routeComponentName(route) {
10517
+ if (route.Component) return typeName(route.Component);
10518
+ if (isElementLike(route.element)) return typeName(route.element.type);
10519
+ return null;
10520
+ }
10521
+ function extractAllRoutes(routes, parentPath = "", parentId = null, inheritedGuards = []) {
10321
10522
  const result = [];
10322
10523
  for (const route of routes) {
10323
10524
  const path = route.path ? (parentPath + "/" + route.path).replace(/\/+/g, "/") : parentPath || "/";
10324
- if (route.path !== void 0 || route.index) {
10525
+ const hasChildren = !!(route.children && route.children.length > 0);
10526
+ const isLayout = !route.path && !route.index && hasChildren;
10527
+ const own = [];
10528
+ collectGuards(route.element, 0, own);
10529
+ const handle = pickRouteHandle(route.handle);
10530
+ if (route.path !== void 0 || route.index || isLayout) {
10325
10531
  result.push({
10326
10532
  id: route.id,
10327
10533
  path: route.index ? parentPath || "/" : path,
10328
- hasChildren: !!(route.children && route.children.length > 0)
10534
+ ...handle ? { handle } : {},
10535
+ hasChildren,
10536
+ parentId,
10537
+ index: !!route.index,
10538
+ isLayout,
10539
+ componentName: routeComponentName(route),
10540
+ guardEvidence: [
10541
+ ...own.map((g) => ({ ...g, source: "self" })),
10542
+ ...inheritedGuards
10543
+ ]
10329
10544
  });
10330
10545
  }
10331
10546
  if (route.children) {
10332
- result.push(...extractAllRoutes(route.children, path));
10547
+ const forChildren = [
10548
+ ...own.map(
10549
+ (g) => ({
10550
+ ...g,
10551
+ source: "ancestor",
10552
+ ...route.id !== void 0 ? { ancestorId: route.id } : {}
10553
+ })
10554
+ ),
10555
+ ...inheritedGuards
10556
+ ];
10557
+ result.push(
10558
+ ...extractAllRoutes(
10559
+ route.children,
10560
+ path,
10561
+ route.id ?? parentId,
10562
+ forChildren
10563
+ )
10564
+ );
10333
10565
  }
10334
10566
  }
10335
10567
  return result;
10336
10568
  }
10337
- function broadcastRouteState(target, state, allRoutes) {
10569
+ function broadcastRouteState(target, state, routes) {
10338
10570
  try {
10339
10571
  const message = {
10340
10572
  type: "ARCHIE_ROUTE_UPDATE",
@@ -10342,12 +10574,18 @@ function broadcastRouteState(target, state, allRoutes) {
10342
10574
  path: state.location.pathname,
10343
10575
  search: state.location.search,
10344
10576
  hash: state.location.hash,
10345
- matches: state.matches.map((m) => ({
10346
- id: m.route.id,
10347
- pathname: m.pathname,
10348
- params: m.params
10349
- })),
10350
- allRoutes,
10577
+ matches: state.matches.map((m) => {
10578
+ const handle = pickRouteHandle(m.route.handle);
10579
+ return {
10580
+ id: m.route.id,
10581
+ pathname: m.pathname,
10582
+ params: m.params,
10583
+ ...handle ? { handle } : {}
10584
+ };
10585
+ }),
10586
+ // Layouts stay out of allRoutes so consumers that predate them don't list duplicates.
10587
+ allRoutes: routes.filter((r) => !r.isLayout),
10588
+ layoutRoutes: routes.filter((r) => r.isLayout),
10351
10589
  timestamp: Date.now()
10352
10590
  }
10353
10591
  };
@@ -10355,21 +10593,174 @@ function broadcastRouteState(target, state, allRoutes) {
10355
10593
  } catch {
10356
10594
  }
10357
10595
  }
10358
- function setupArchieRouteListener(router) {
10596
+ var SETTLE_QUIET_MS = 400;
10597
+ var SETTLE_TIMEOUT_MS = 8e3;
10598
+ var SETTLE_POLL_MS = 50;
10599
+ function readPackageVersion() {
10600
+ return true ? "0.1.14" : "unknown";
10601
+ }
10602
+ var DEFAULT_ARCHIE_COMMAND_ORIGINS = getAllowedOrigins(true);
10603
+ function originPatternToRegExp(pattern) {
10604
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:\*$/, ":\\d+").replace(/\*/g, "[a-z0-9.-]+");
10605
+ return new RegExp(`^${escaped}$`, "i");
10606
+ }
10607
+ function isAllowedCommandOrigin(origin, allowed) {
10608
+ return allowed.some((pattern) => originPatternToRegExp(pattern).test(origin));
10609
+ }
10610
+ function postToTarget(target, message, targetOrigin = "*") {
10611
+ try {
10612
+ target.postMessage(message, targetOrigin);
10613
+ } catch {
10614
+ }
10615
+ }
10616
+ function isRouterBusy(router) {
10617
+ const { navigation, fetchers } = router.state;
10618
+ if (navigation && navigation.state !== "idle") return true;
10619
+ if (fetchers) {
10620
+ for (const fetcher of fetchers.values()) {
10621
+ if (fetcher.state !== "idle") return true;
10622
+ }
10623
+ }
10624
+ return false;
10625
+ }
10626
+ function waitForSettled(router, navigation) {
10627
+ return new Promise((resolve) => {
10628
+ const startedAt = Date.now();
10629
+ let lastMutationAt = Date.now();
10630
+ let navigationDone = false;
10631
+ const observer = new MutationObserver(() => {
10632
+ lastMutationAt = Date.now();
10633
+ });
10634
+ observer.observe(document, {
10635
+ subtree: true,
10636
+ childList: true,
10637
+ attributes: true,
10638
+ characterData: true
10639
+ });
10640
+ navigation.then(
10641
+ () => {
10642
+ navigationDone = true;
10643
+ },
10644
+ () => {
10645
+ navigationDone = true;
10646
+ }
10647
+ );
10648
+ const finish = (settled) => {
10649
+ clearInterval(timer);
10650
+ observer.disconnect();
10651
+ resolve(settled);
10652
+ };
10653
+ const timer = setInterval(() => {
10654
+ const now = Date.now();
10655
+ if (navigationDone && !isRouterBusy(router) && now - lastMutationAt >= SETTLE_QUIET_MS) {
10656
+ finish(true);
10657
+ } else if (now - startedAt >= SETTLE_TIMEOUT_MS) {
10658
+ finish(false);
10659
+ }
10660
+ }, SETTLE_POLL_MS);
10661
+ });
10662
+ }
10663
+ function pathnameOf(path) {
10664
+ return path.split(/[?#]/, 1)[0];
10665
+ }
10666
+ async function handleNavigate(router, target, targetOrigin, data) {
10667
+ if (typeof router.navigate !== "function") {
10668
+ const { location: location3 } = router.state;
10669
+ postToTarget(target, {
10670
+ type: "ARCHIE_NAVIGATE_RESULT",
10671
+ requestId: data.requestId,
10672
+ requestedPath: data.path,
10673
+ finalPath: location3.pathname + location3.search + location3.hash,
10674
+ matchedRouteIds: [],
10675
+ redirected: false,
10676
+ settled: false
10677
+ }, targetOrigin);
10678
+ return;
10679
+ }
10680
+ let navigation = Promise.resolve();
10681
+ try {
10682
+ navigation = Promise.resolve(
10683
+ router.navigate(data.path, { replace: data.replace })
10684
+ );
10685
+ } catch {
10686
+ navigation = Promise.resolve();
10687
+ }
10688
+ const settled = await waitForSettled(router, navigation);
10689
+ const { location: location2, matches } = router.state;
10690
+ postToTarget(target, {
10691
+ type: "ARCHIE_NAVIGATE_RESULT",
10692
+ requestId: data.requestId,
10693
+ requestedPath: data.path,
10694
+ finalPath: location2.pathname + location2.search + location2.hash,
10695
+ matchedRouteIds: matches.map((m) => m.route.id).filter((id) => typeof id === "string"),
10696
+ redirected: location2.pathname !== pathnameOf(data.path),
10697
+ settled
10698
+ }, targetOrigin);
10699
+ }
10700
+ async function handleSnapshot(target, targetOrigin, requestId) {
10701
+ try {
10702
+ const snapshot = await captureDomSnapshot();
10703
+ postToTarget(target, {
10704
+ type: "ARCHIE_SNAPSHOT_DOM_RESULT",
10705
+ requestId,
10706
+ ...snapshot
10707
+ }, targetOrigin);
10708
+ } catch (error) {
10709
+ postToTarget(target, {
10710
+ type: "ARCHIE_SNAPSHOT_DOM_FAILED",
10711
+ requestId,
10712
+ error: error instanceof Error ? error.message : String(error)
10713
+ }, targetOrigin);
10714
+ }
10715
+ }
10716
+ function broadcastCapabilities(router, target) {
10717
+ postToTarget(target, {
10718
+ type: "ARCHIE_CAPABILITIES",
10719
+ version: readPackageVersion(),
10720
+ capabilities: {
10721
+ navigate: typeof router.navigate === "function",
10722
+ snapshot: true,
10723
+ identityOverride: false
10724
+ }
10725
+ });
10726
+ }
10727
+ function setupArchieRouteListener(router, options = {}) {
10728
+ const allowedOrigins = options.allowedOrigins ?? DEFAULT_ARCHIE_COMMAND_ORIGINS;
10359
10729
  const target = window.parent !== window ? window.parent : window.opener;
10360
10730
  if (!target) {
10361
10731
  return () => {
10362
10732
  };
10363
10733
  }
10364
10734
  broadcastRouteState(target, router.state, extractAllRoutes(router.routes));
10735
+ broadcastCapabilities(router, target);
10365
10736
  const unsubscribe = router.subscribe((state) => {
10366
10737
  const currentRoutes = extractAllRoutes(router.routes);
10367
10738
  broadcastRouteState(target, state, currentRoutes);
10368
10739
  });
10369
10740
  const handleMessage = (event) => {
10370
- if (event.data?.type === "ARCHIE_QUERY_ROUTES") {
10371
- const currentRoutes = extractAllRoutes(router.routes);
10372
- broadcastRouteState(target, router.state, currentRoutes);
10741
+ if (event.source !== target) return;
10742
+ const data = event.data;
10743
+ switch (data?.type) {
10744
+ case "ARCHIE_QUERY_ROUTES":
10745
+ broadcastRouteState(
10746
+ target,
10747
+ router.state,
10748
+ extractAllRoutes(router.routes)
10749
+ );
10750
+ break;
10751
+ case "ARCHIE_QUERY_CAPABILITIES":
10752
+ broadcastCapabilities(router, target);
10753
+ break;
10754
+ case "ARCHIE_NAVIGATE":
10755
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string" && typeof data.path === "string") {
10756
+ void handleNavigate(router, target, event.origin, data);
10757
+ }
10758
+ break;
10759
+ case "ARCHIE_SNAPSHOT_DOM":
10760
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string") {
10761
+ void handleSnapshot(target, event.origin, data.requestId);
10762
+ }
10763
+ break;
10373
10764
  }
10374
10765
  };
10375
10766
  window.addEventListener("message", handleMessage);
@@ -10409,7 +10800,8 @@ function installDomMetaSanitizer() {
10409
10800
  function useArchieDevToolsRuntime({
10410
10801
  router,
10411
10802
  inspector,
10412
- inspectorOptions
10803
+ inspectorOptions,
10804
+ routeListenerOrigins
10413
10805
  }) {
10414
10806
  installDomMetaSanitizer();
10415
10807
  const stableOptionsKey = JSON.stringify(inspectorOptions ?? null);
@@ -10419,7 +10811,9 @@ function useArchieDevToolsRuntime({
10419
10811
  if (process.env.NODE_ENV !== "development") {
10420
10812
  return;
10421
10813
  }
10422
- const unsubscribe = setupArchieRouteListener(router);
10814
+ const unsubscribe = setupArchieRouteListener(router, {
10815
+ allowedOrigins: routeListenerOrigins
10816
+ });
10423
10817
  if (inspector !== false) {
10424
10818
  const useLocalInspector = readArchieLocalInspectorFlag();
10425
10819
  injectInspector({
@@ -10437,9 +10831,15 @@ function ArchieDevToolProvider({
10437
10831
  children,
10438
10832
  router,
10439
10833
  inspector,
10440
- inspectorOptions
10834
+ inspectorOptions,
10835
+ routeListenerOrigins
10441
10836
  }) {
10442
- useArchieDevToolsRuntime({ router, inspector, inspectorOptions });
10837
+ useArchieDevToolsRuntime({
10838
+ router,
10839
+ inspector,
10840
+ inspectorOptions,
10841
+ routeListenerOrigins
10842
+ });
10443
10843
  return /* @__PURE__ */ jsx6(DndProvider, { children });
10444
10844
  }
10445
10845
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archie/devtools",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "sideEffects": false,
6
6
  "repository": {