@archie/devtools 0.1.12 → 0.1.13

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.
@@ -50,6 +50,9 @@ interface ArchieRouteObjectLike {
50
50
  id?: string;
51
51
  path?: string;
52
52
  index?: boolean;
53
+ Component?: unknown;
54
+ element?: unknown;
55
+ lazy?: unknown;
53
56
  children?: readonly ArchieRouteObjectLike[];
54
57
  }
55
58
  interface ArchieRouteMatchLike {
@@ -66,10 +69,19 @@ interface ArchieRouterStateLike {
66
69
  hash: string;
67
70
  };
68
71
  matches: readonly ArchieRouteMatchLike[];
72
+ navigation?: {
73
+ state: string;
74
+ };
75
+ fetchers?: ReadonlyMap<string, {
76
+ state: string;
77
+ }>;
69
78
  }
70
79
  interface ArchieRouterLike {
71
80
  routes: readonly ArchieRouteObjectLike[];
72
81
  state: ArchieRouterStateLike;
82
+ navigate?(to: string, opts?: {
83
+ replace?: boolean;
84
+ }): Promise<void> | void;
73
85
  subscribe(listener: (state: ArchieRouterStateLike) => void): () => void;
74
86
  }
75
87
 
@@ -77,12 +89,14 @@ interface ArchieDevToolsRuntimeOptions {
77
89
  router: ArchieRouterLike;
78
90
  inspector?: boolean;
79
91
  inspectorOptions?: InjectOptions;
92
+ /** Origins allowed to send NAVIGATE / SNAPSHOT_DOM to the route listener. */
93
+ routeListenerOrigins?: readonly string[];
80
94
  }
81
95
 
82
96
  interface ArchieDevToolProviderProps extends ArchieDevToolsRuntimeOptions {
83
97
  children: ReactNode;
84
98
  }
85
- declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
99
+ declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, routeListenerOrigins, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
86
100
 
87
101
  interface DndProviderProps {
88
102
  children: React.ReactNode;
@@ -50,6 +50,9 @@ interface ArchieRouteObjectLike {
50
50
  id?: string;
51
51
  path?: string;
52
52
  index?: boolean;
53
+ Component?: unknown;
54
+ element?: unknown;
55
+ lazy?: unknown;
53
56
  children?: readonly ArchieRouteObjectLike[];
54
57
  }
55
58
  interface ArchieRouteMatchLike {
@@ -66,10 +69,19 @@ interface ArchieRouterStateLike {
66
69
  hash: string;
67
70
  };
68
71
  matches: readonly ArchieRouteMatchLike[];
72
+ navigation?: {
73
+ state: string;
74
+ };
75
+ fetchers?: ReadonlyMap<string, {
76
+ state: string;
77
+ }>;
69
78
  }
70
79
  interface ArchieRouterLike {
71
80
  routes: readonly ArchieRouteObjectLike[];
72
81
  state: ArchieRouterStateLike;
82
+ navigate?(to: string, opts?: {
83
+ replace?: boolean;
84
+ }): Promise<void> | void;
73
85
  subscribe(listener: (state: ArchieRouterStateLike) => void): () => void;
74
86
  }
75
87
 
@@ -77,12 +89,14 @@ interface ArchieDevToolsRuntimeOptions {
77
89
  router: ArchieRouterLike;
78
90
  inspector?: boolean;
79
91
  inspectorOptions?: InjectOptions;
92
+ /** Origins allowed to send NAVIGATE / SNAPSHOT_DOM to the route listener. */
93
+ routeListenerOrigins?: readonly string[];
80
94
  }
81
95
 
82
96
  interface ArchieDevToolProviderProps extends ArchieDevToolsRuntimeOptions {
83
97
  children: ReactNode;
84
98
  }
85
- declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
99
+ declare function ArchieDevToolProvider({ children, router, inspector, inspectorOptions, routeListenerOrigins, }: ArchieDevToolProviderProps): react_jsx_runtime.JSX.Element;
86
100
 
87
101
  interface DndProviderProps {
88
102
  children: React.ReactNode;
@@ -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.13" ? "0.1.13" : null;
10275
10275
  }
10276
10276
  var REMOTE_INSPECTOR_VERSION = readBuildVersion();
10277
10277
  var isRemoteInspectorUrlVersioned = REMOTE_INSPECTOR_VERSION !== null;
@@ -10351,25 +10351,238 @@ 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 GUARD_NAME_RE = /guard|protect|require|auth(?!or)/i;
10479
+ var GUARD_PRIVATE_RE = /\bprivate|Private(?=[A-Z]|$)/;
10480
+ var NON_GUARD_SUFFIX_RE = /(Layout|Provider|Context|Page|Form|Screen|View)$/;
10481
+ function isGuardName(name) {
10482
+ return !NON_GUARD_SUFFIX_RE.test(name) && (GUARD_NAME_RE.test(name) || GUARD_PRIVATE_RE.test(name));
10483
+ }
10484
+ var GUARD_PROP_NAMES = [
10485
+ "roles",
10486
+ "allowedRoles",
10487
+ "requiredRoles",
10488
+ "permission",
10489
+ "permissions"
10490
+ ];
10491
+ var MAX_ELEMENT_DEPTH = 6;
10492
+ function typeName(type) {
10493
+ if (typeof type === "string") return type;
10494
+ if (typeof type === "function") {
10495
+ const fn = type;
10496
+ return fn.displayName || fn.name || null;
10497
+ }
10498
+ if (type && typeof type === "object") {
10499
+ const obj = type;
10500
+ return obj.displayName || typeName(obj.render) || typeName(obj.type);
10501
+ }
10502
+ return null;
10503
+ }
10504
+ function isElementLike(node) {
10505
+ return !!node && typeof node === "object" && "type" in node && "props" in node;
10506
+ }
10507
+ function literalRoles(value) {
10508
+ if (typeof value === "string") return [value];
10509
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
10510
+ return value;
10511
+ }
10512
+ return void 0;
10513
+ }
10514
+ function collectGuards(node, depth, out) {
10515
+ if (depth > MAX_ELEMENT_DEPTH || node == null) return;
10516
+ if (Array.isArray(node)) {
10517
+ for (const child of node) collectGuards(child, depth, out);
10518
+ return;
10519
+ }
10520
+ if (!isElementLike(node)) return;
10521
+ const props = node.props ?? {};
10522
+ const name = typeName(node.type);
10523
+ const roleProps = GUARD_PROP_NAMES.filter((p) => p in props);
10524
+ const isNamedGuard = typeof node.type !== "string" && !!name && isGuardName(name);
10525
+ if (isNamedGuard || roleProps.length > 0) {
10526
+ const roles = roleProps.flatMap((p) => literalRoles(props[p]) ?? []);
10527
+ out.push({
10528
+ componentName: name ?? "Anonymous",
10529
+ ...roles.length > 0 ? { roles } : {}
10530
+ });
10531
+ }
10532
+ collectGuards(props.children, depth + 1, out);
10533
+ }
10534
+ function routeComponentName(route) {
10535
+ if (route.Component) return typeName(route.Component);
10536
+ if (isElementLike(route.element)) return typeName(route.element.type);
10537
+ return null;
10538
+ }
10539
+ function extractAllRoutes(routes, parentPath = "", parentId = null, inheritedGuards = []) {
10356
10540
  const result = [];
10357
10541
  for (const route of routes) {
10358
10542
  const path = route.path ? (parentPath + "/" + route.path).replace(/\/+/g, "/") : parentPath || "/";
10359
- if (route.path !== void 0 || route.index) {
10543
+ const hasChildren = !!(route.children && route.children.length > 0);
10544
+ const isLayout = !route.path && !route.index && hasChildren;
10545
+ const own = [];
10546
+ collectGuards(route.element, 0, own);
10547
+ if (route.path !== void 0 || route.index || isLayout) {
10360
10548
  result.push({
10361
10549
  id: route.id,
10362
10550
  path: route.index ? parentPath || "/" : path,
10363
- hasChildren: !!(route.children && route.children.length > 0)
10551
+ hasChildren,
10552
+ parentId,
10553
+ index: !!route.index,
10554
+ isLayout,
10555
+ componentName: routeComponentName(route),
10556
+ guardEvidence: [
10557
+ ...own.map((g) => ({ ...g, source: "self" })),
10558
+ ...inheritedGuards
10559
+ ]
10364
10560
  });
10365
10561
  }
10366
10562
  if (route.children) {
10367
- result.push(...extractAllRoutes(route.children, path));
10563
+ const forChildren = [
10564
+ ...own.map(
10565
+ (g) => ({
10566
+ ...g,
10567
+ source: "ancestor",
10568
+ ...route.id !== void 0 ? { ancestorId: route.id } : {}
10569
+ })
10570
+ ),
10571
+ ...inheritedGuards
10572
+ ];
10573
+ result.push(
10574
+ ...extractAllRoutes(
10575
+ route.children,
10576
+ path,
10577
+ route.id ?? parentId,
10578
+ forChildren
10579
+ )
10580
+ );
10368
10581
  }
10369
10582
  }
10370
10583
  return result;
10371
10584
  }
10372
- function broadcastRouteState(target, state, allRoutes) {
10585
+ function broadcastRouteState(target, state, routes) {
10373
10586
  try {
10374
10587
  const message = {
10375
10588
  type: "ARCHIE_ROUTE_UPDATE",
@@ -10382,7 +10595,9 @@ function broadcastRouteState(target, state, allRoutes) {
10382
10595
  pathname: m.pathname,
10383
10596
  params: m.params
10384
10597
  })),
10385
- allRoutes,
10598
+ // Layouts stay out of allRoutes so consumers that predate them don't list duplicates.
10599
+ allRoutes: routes.filter((r) => !r.isLayout),
10600
+ layoutRoutes: routes.filter((r) => r.isLayout),
10386
10601
  timestamp: Date.now()
10387
10602
  }
10388
10603
  };
@@ -10390,21 +10605,174 @@ function broadcastRouteState(target, state, allRoutes) {
10390
10605
  } catch {
10391
10606
  }
10392
10607
  }
10393
- function setupArchieRouteListener(router) {
10608
+ var SETTLE_QUIET_MS = 400;
10609
+ var SETTLE_TIMEOUT_MS = 8e3;
10610
+ var SETTLE_POLL_MS = 50;
10611
+ function readPackageVersion() {
10612
+ return true ? "0.1.13" : "unknown";
10613
+ }
10614
+ var DEFAULT_ARCHIE_COMMAND_ORIGINS = getAllowedOrigins(true);
10615
+ function originPatternToRegExp(pattern) {
10616
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:\*$/, ":\\d+").replace(/\*/g, "[a-z0-9.-]+");
10617
+ return new RegExp(`^${escaped}$`, "i");
10618
+ }
10619
+ function isAllowedCommandOrigin(origin, allowed) {
10620
+ return allowed.some((pattern) => originPatternToRegExp(pattern).test(origin));
10621
+ }
10622
+ function postToTarget(target, message, targetOrigin = "*") {
10623
+ try {
10624
+ target.postMessage(message, targetOrigin);
10625
+ } catch {
10626
+ }
10627
+ }
10628
+ function isRouterBusy(router) {
10629
+ const { navigation, fetchers } = router.state;
10630
+ if (navigation && navigation.state !== "idle") return true;
10631
+ if (fetchers) {
10632
+ for (const fetcher of fetchers.values()) {
10633
+ if (fetcher.state !== "idle") return true;
10634
+ }
10635
+ }
10636
+ return false;
10637
+ }
10638
+ function waitForSettled(router, navigation) {
10639
+ return new Promise((resolve) => {
10640
+ const startedAt = Date.now();
10641
+ let lastMutationAt = Date.now();
10642
+ let navigationDone = false;
10643
+ const observer = new MutationObserver(() => {
10644
+ lastMutationAt = Date.now();
10645
+ });
10646
+ observer.observe(document, {
10647
+ subtree: true,
10648
+ childList: true,
10649
+ attributes: true,
10650
+ characterData: true
10651
+ });
10652
+ navigation.then(
10653
+ () => {
10654
+ navigationDone = true;
10655
+ },
10656
+ () => {
10657
+ navigationDone = true;
10658
+ }
10659
+ );
10660
+ const finish = (settled) => {
10661
+ clearInterval(timer);
10662
+ observer.disconnect();
10663
+ resolve(settled);
10664
+ };
10665
+ const timer = setInterval(() => {
10666
+ const now = Date.now();
10667
+ if (navigationDone && !isRouterBusy(router) && now - lastMutationAt >= SETTLE_QUIET_MS) {
10668
+ finish(true);
10669
+ } else if (now - startedAt >= SETTLE_TIMEOUT_MS) {
10670
+ finish(false);
10671
+ }
10672
+ }, SETTLE_POLL_MS);
10673
+ });
10674
+ }
10675
+ function pathnameOf(path) {
10676
+ return path.split(/[?#]/, 1)[0];
10677
+ }
10678
+ async function handleNavigate(router, target, targetOrigin, data) {
10679
+ if (typeof router.navigate !== "function") {
10680
+ const { location: location3 } = router.state;
10681
+ postToTarget(target, {
10682
+ type: "ARCHIE_NAVIGATE_RESULT",
10683
+ requestId: data.requestId,
10684
+ requestedPath: data.path,
10685
+ finalPath: location3.pathname + location3.search + location3.hash,
10686
+ matchedRouteIds: [],
10687
+ redirected: false,
10688
+ settled: false
10689
+ }, targetOrigin);
10690
+ return;
10691
+ }
10692
+ let navigation = Promise.resolve();
10693
+ try {
10694
+ navigation = Promise.resolve(
10695
+ router.navigate(data.path, { replace: data.replace })
10696
+ );
10697
+ } catch {
10698
+ navigation = Promise.resolve();
10699
+ }
10700
+ const settled = await waitForSettled(router, navigation);
10701
+ const { location: location2, matches } = router.state;
10702
+ postToTarget(target, {
10703
+ type: "ARCHIE_NAVIGATE_RESULT",
10704
+ requestId: data.requestId,
10705
+ requestedPath: data.path,
10706
+ finalPath: location2.pathname + location2.search + location2.hash,
10707
+ matchedRouteIds: matches.map((m) => m.route.id).filter((id) => typeof id === "string"),
10708
+ redirected: location2.pathname !== pathnameOf(data.path),
10709
+ settled
10710
+ }, targetOrigin);
10711
+ }
10712
+ async function handleSnapshot(target, targetOrigin, requestId) {
10713
+ try {
10714
+ const snapshot = await captureDomSnapshot();
10715
+ postToTarget(target, {
10716
+ type: "ARCHIE_SNAPSHOT_DOM_RESULT",
10717
+ requestId,
10718
+ ...snapshot
10719
+ }, targetOrigin);
10720
+ } catch (error) {
10721
+ postToTarget(target, {
10722
+ type: "ARCHIE_SNAPSHOT_DOM_FAILED",
10723
+ requestId,
10724
+ error: error instanceof Error ? error.message : String(error)
10725
+ }, targetOrigin);
10726
+ }
10727
+ }
10728
+ function broadcastCapabilities(router, target) {
10729
+ postToTarget(target, {
10730
+ type: "ARCHIE_CAPABILITIES",
10731
+ version: readPackageVersion(),
10732
+ capabilities: {
10733
+ navigate: typeof router.navigate === "function",
10734
+ snapshot: true,
10735
+ identityOverride: false
10736
+ }
10737
+ });
10738
+ }
10739
+ function setupArchieRouteListener(router, options = {}) {
10740
+ const allowedOrigins = options.allowedOrigins ?? DEFAULT_ARCHIE_COMMAND_ORIGINS;
10394
10741
  const target = window.parent !== window ? window.parent : window.opener;
10395
10742
  if (!target) {
10396
10743
  return () => {
10397
10744
  };
10398
10745
  }
10399
10746
  broadcastRouteState(target, router.state, extractAllRoutes(router.routes));
10747
+ broadcastCapabilities(router, target);
10400
10748
  const unsubscribe = router.subscribe((state) => {
10401
10749
  const currentRoutes = extractAllRoutes(router.routes);
10402
10750
  broadcastRouteState(target, state, currentRoutes);
10403
10751
  });
10404
10752
  const handleMessage = (event) => {
10405
- if (event.data?.type === "ARCHIE_QUERY_ROUTES") {
10406
- const currentRoutes = extractAllRoutes(router.routes);
10407
- broadcastRouteState(target, router.state, currentRoutes);
10753
+ if (event.source !== target) return;
10754
+ const data = event.data;
10755
+ switch (data?.type) {
10756
+ case "ARCHIE_QUERY_ROUTES":
10757
+ broadcastRouteState(
10758
+ target,
10759
+ router.state,
10760
+ extractAllRoutes(router.routes)
10761
+ );
10762
+ break;
10763
+ case "ARCHIE_QUERY_CAPABILITIES":
10764
+ broadcastCapabilities(router, target);
10765
+ break;
10766
+ case "ARCHIE_NAVIGATE":
10767
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string" && typeof data.path === "string") {
10768
+ void handleNavigate(router, target, event.origin, data);
10769
+ }
10770
+ break;
10771
+ case "ARCHIE_SNAPSHOT_DOM":
10772
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string") {
10773
+ void handleSnapshot(target, event.origin, data.requestId);
10774
+ }
10775
+ break;
10408
10776
  }
10409
10777
  };
10410
10778
  window.addEventListener("message", handleMessage);
@@ -10444,7 +10812,8 @@ function installDomMetaSanitizer() {
10444
10812
  function useArchieDevToolsRuntime({
10445
10813
  router,
10446
10814
  inspector,
10447
- inspectorOptions
10815
+ inspectorOptions,
10816
+ routeListenerOrigins
10448
10817
  }) {
10449
10818
  installDomMetaSanitizer();
10450
10819
  const stableOptionsKey = JSON.stringify(inspectorOptions ?? null);
@@ -10454,7 +10823,9 @@ function useArchieDevToolsRuntime({
10454
10823
  if (process.env.NODE_ENV !== "development") {
10455
10824
  return;
10456
10825
  }
10457
- const unsubscribe = setupArchieRouteListener(router);
10826
+ const unsubscribe = setupArchieRouteListener(router, {
10827
+ allowedOrigins: routeListenerOrigins
10828
+ });
10458
10829
  if (inspector !== false) {
10459
10830
  const useLocalInspector = readArchieLocalInspectorFlag();
10460
10831
  injectInspector({
@@ -10472,9 +10843,15 @@ function ArchieDevToolProvider({
10472
10843
  children,
10473
10844
  router,
10474
10845
  inspector,
10475
- inspectorOptions
10846
+ inspectorOptions,
10847
+ routeListenerOrigins
10476
10848
  }) {
10477
- useArchieDevToolsRuntime({ router, inspector, inspectorOptions });
10849
+ useArchieDevToolsRuntime({
10850
+ router,
10851
+ inspector,
10852
+ inspectorOptions,
10853
+ routeListenerOrigins
10854
+ });
10478
10855
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DndProvider, { children });
10479
10856
  }
10480
10857
  // 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.13" ? "0.1.13" : null;
10240
10240
  }
10241
10241
  var REMOTE_INSPECTOR_VERSION = readBuildVersion();
10242
10242
  var isRemoteInspectorUrlVersioned = REMOTE_INSPECTOR_VERSION !== null;
@@ -10316,25 +10316,238 @@ 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 GUARD_NAME_RE = /guard|protect|require|auth(?!or)/i;
10444
+ var GUARD_PRIVATE_RE = /\bprivate|Private(?=[A-Z]|$)/;
10445
+ var NON_GUARD_SUFFIX_RE = /(Layout|Provider|Context|Page|Form|Screen|View)$/;
10446
+ function isGuardName(name) {
10447
+ return !NON_GUARD_SUFFIX_RE.test(name) && (GUARD_NAME_RE.test(name) || GUARD_PRIVATE_RE.test(name));
10448
+ }
10449
+ var GUARD_PROP_NAMES = [
10450
+ "roles",
10451
+ "allowedRoles",
10452
+ "requiredRoles",
10453
+ "permission",
10454
+ "permissions"
10455
+ ];
10456
+ var MAX_ELEMENT_DEPTH = 6;
10457
+ function typeName(type) {
10458
+ if (typeof type === "string") return type;
10459
+ if (typeof type === "function") {
10460
+ const fn = type;
10461
+ return fn.displayName || fn.name || null;
10462
+ }
10463
+ if (type && typeof type === "object") {
10464
+ const obj = type;
10465
+ return obj.displayName || typeName(obj.render) || typeName(obj.type);
10466
+ }
10467
+ return null;
10468
+ }
10469
+ function isElementLike(node) {
10470
+ return !!node && typeof node === "object" && "type" in node && "props" in node;
10471
+ }
10472
+ function literalRoles(value) {
10473
+ if (typeof value === "string") return [value];
10474
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
10475
+ return value;
10476
+ }
10477
+ return void 0;
10478
+ }
10479
+ function collectGuards(node, depth, out) {
10480
+ if (depth > MAX_ELEMENT_DEPTH || node == null) return;
10481
+ if (Array.isArray(node)) {
10482
+ for (const child of node) collectGuards(child, depth, out);
10483
+ return;
10484
+ }
10485
+ if (!isElementLike(node)) return;
10486
+ const props = node.props ?? {};
10487
+ const name = typeName(node.type);
10488
+ const roleProps = GUARD_PROP_NAMES.filter((p) => p in props);
10489
+ const isNamedGuard = typeof node.type !== "string" && !!name && isGuardName(name);
10490
+ if (isNamedGuard || roleProps.length > 0) {
10491
+ const roles = roleProps.flatMap((p) => literalRoles(props[p]) ?? []);
10492
+ out.push({
10493
+ componentName: name ?? "Anonymous",
10494
+ ...roles.length > 0 ? { roles } : {}
10495
+ });
10496
+ }
10497
+ collectGuards(props.children, depth + 1, out);
10498
+ }
10499
+ function routeComponentName(route) {
10500
+ if (route.Component) return typeName(route.Component);
10501
+ if (isElementLike(route.element)) return typeName(route.element.type);
10502
+ return null;
10503
+ }
10504
+ function extractAllRoutes(routes, parentPath = "", parentId = null, inheritedGuards = []) {
10321
10505
  const result = [];
10322
10506
  for (const route of routes) {
10323
10507
  const path = route.path ? (parentPath + "/" + route.path).replace(/\/+/g, "/") : parentPath || "/";
10324
- if (route.path !== void 0 || route.index) {
10508
+ const hasChildren = !!(route.children && route.children.length > 0);
10509
+ const isLayout = !route.path && !route.index && hasChildren;
10510
+ const own = [];
10511
+ collectGuards(route.element, 0, own);
10512
+ if (route.path !== void 0 || route.index || isLayout) {
10325
10513
  result.push({
10326
10514
  id: route.id,
10327
10515
  path: route.index ? parentPath || "/" : path,
10328
- hasChildren: !!(route.children && route.children.length > 0)
10516
+ hasChildren,
10517
+ parentId,
10518
+ index: !!route.index,
10519
+ isLayout,
10520
+ componentName: routeComponentName(route),
10521
+ guardEvidence: [
10522
+ ...own.map((g) => ({ ...g, source: "self" })),
10523
+ ...inheritedGuards
10524
+ ]
10329
10525
  });
10330
10526
  }
10331
10527
  if (route.children) {
10332
- result.push(...extractAllRoutes(route.children, path));
10528
+ const forChildren = [
10529
+ ...own.map(
10530
+ (g) => ({
10531
+ ...g,
10532
+ source: "ancestor",
10533
+ ...route.id !== void 0 ? { ancestorId: route.id } : {}
10534
+ })
10535
+ ),
10536
+ ...inheritedGuards
10537
+ ];
10538
+ result.push(
10539
+ ...extractAllRoutes(
10540
+ route.children,
10541
+ path,
10542
+ route.id ?? parentId,
10543
+ forChildren
10544
+ )
10545
+ );
10333
10546
  }
10334
10547
  }
10335
10548
  return result;
10336
10549
  }
10337
- function broadcastRouteState(target, state, allRoutes) {
10550
+ function broadcastRouteState(target, state, routes) {
10338
10551
  try {
10339
10552
  const message = {
10340
10553
  type: "ARCHIE_ROUTE_UPDATE",
@@ -10347,7 +10560,9 @@ function broadcastRouteState(target, state, allRoutes) {
10347
10560
  pathname: m.pathname,
10348
10561
  params: m.params
10349
10562
  })),
10350
- allRoutes,
10563
+ // Layouts stay out of allRoutes so consumers that predate them don't list duplicates.
10564
+ allRoutes: routes.filter((r) => !r.isLayout),
10565
+ layoutRoutes: routes.filter((r) => r.isLayout),
10351
10566
  timestamp: Date.now()
10352
10567
  }
10353
10568
  };
@@ -10355,21 +10570,174 @@ function broadcastRouteState(target, state, allRoutes) {
10355
10570
  } catch {
10356
10571
  }
10357
10572
  }
10358
- function setupArchieRouteListener(router) {
10573
+ var SETTLE_QUIET_MS = 400;
10574
+ var SETTLE_TIMEOUT_MS = 8e3;
10575
+ var SETTLE_POLL_MS = 50;
10576
+ function readPackageVersion() {
10577
+ return true ? "0.1.13" : "unknown";
10578
+ }
10579
+ var DEFAULT_ARCHIE_COMMAND_ORIGINS = getAllowedOrigins(true);
10580
+ function originPatternToRegExp(pattern) {
10581
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/:\*$/, ":\\d+").replace(/\*/g, "[a-z0-9.-]+");
10582
+ return new RegExp(`^${escaped}$`, "i");
10583
+ }
10584
+ function isAllowedCommandOrigin(origin, allowed) {
10585
+ return allowed.some((pattern) => originPatternToRegExp(pattern).test(origin));
10586
+ }
10587
+ function postToTarget(target, message, targetOrigin = "*") {
10588
+ try {
10589
+ target.postMessage(message, targetOrigin);
10590
+ } catch {
10591
+ }
10592
+ }
10593
+ function isRouterBusy(router) {
10594
+ const { navigation, fetchers } = router.state;
10595
+ if (navigation && navigation.state !== "idle") return true;
10596
+ if (fetchers) {
10597
+ for (const fetcher of fetchers.values()) {
10598
+ if (fetcher.state !== "idle") return true;
10599
+ }
10600
+ }
10601
+ return false;
10602
+ }
10603
+ function waitForSettled(router, navigation) {
10604
+ return new Promise((resolve) => {
10605
+ const startedAt = Date.now();
10606
+ let lastMutationAt = Date.now();
10607
+ let navigationDone = false;
10608
+ const observer = new MutationObserver(() => {
10609
+ lastMutationAt = Date.now();
10610
+ });
10611
+ observer.observe(document, {
10612
+ subtree: true,
10613
+ childList: true,
10614
+ attributes: true,
10615
+ characterData: true
10616
+ });
10617
+ navigation.then(
10618
+ () => {
10619
+ navigationDone = true;
10620
+ },
10621
+ () => {
10622
+ navigationDone = true;
10623
+ }
10624
+ );
10625
+ const finish = (settled) => {
10626
+ clearInterval(timer);
10627
+ observer.disconnect();
10628
+ resolve(settled);
10629
+ };
10630
+ const timer = setInterval(() => {
10631
+ const now = Date.now();
10632
+ if (navigationDone && !isRouterBusy(router) && now - lastMutationAt >= SETTLE_QUIET_MS) {
10633
+ finish(true);
10634
+ } else if (now - startedAt >= SETTLE_TIMEOUT_MS) {
10635
+ finish(false);
10636
+ }
10637
+ }, SETTLE_POLL_MS);
10638
+ });
10639
+ }
10640
+ function pathnameOf(path) {
10641
+ return path.split(/[?#]/, 1)[0];
10642
+ }
10643
+ async function handleNavigate(router, target, targetOrigin, data) {
10644
+ if (typeof router.navigate !== "function") {
10645
+ const { location: location3 } = router.state;
10646
+ postToTarget(target, {
10647
+ type: "ARCHIE_NAVIGATE_RESULT",
10648
+ requestId: data.requestId,
10649
+ requestedPath: data.path,
10650
+ finalPath: location3.pathname + location3.search + location3.hash,
10651
+ matchedRouteIds: [],
10652
+ redirected: false,
10653
+ settled: false
10654
+ }, targetOrigin);
10655
+ return;
10656
+ }
10657
+ let navigation = Promise.resolve();
10658
+ try {
10659
+ navigation = Promise.resolve(
10660
+ router.navigate(data.path, { replace: data.replace })
10661
+ );
10662
+ } catch {
10663
+ navigation = Promise.resolve();
10664
+ }
10665
+ const settled = await waitForSettled(router, navigation);
10666
+ const { location: location2, matches } = router.state;
10667
+ postToTarget(target, {
10668
+ type: "ARCHIE_NAVIGATE_RESULT",
10669
+ requestId: data.requestId,
10670
+ requestedPath: data.path,
10671
+ finalPath: location2.pathname + location2.search + location2.hash,
10672
+ matchedRouteIds: matches.map((m) => m.route.id).filter((id) => typeof id === "string"),
10673
+ redirected: location2.pathname !== pathnameOf(data.path),
10674
+ settled
10675
+ }, targetOrigin);
10676
+ }
10677
+ async function handleSnapshot(target, targetOrigin, requestId) {
10678
+ try {
10679
+ const snapshot = await captureDomSnapshot();
10680
+ postToTarget(target, {
10681
+ type: "ARCHIE_SNAPSHOT_DOM_RESULT",
10682
+ requestId,
10683
+ ...snapshot
10684
+ }, targetOrigin);
10685
+ } catch (error) {
10686
+ postToTarget(target, {
10687
+ type: "ARCHIE_SNAPSHOT_DOM_FAILED",
10688
+ requestId,
10689
+ error: error instanceof Error ? error.message : String(error)
10690
+ }, targetOrigin);
10691
+ }
10692
+ }
10693
+ function broadcastCapabilities(router, target) {
10694
+ postToTarget(target, {
10695
+ type: "ARCHIE_CAPABILITIES",
10696
+ version: readPackageVersion(),
10697
+ capabilities: {
10698
+ navigate: typeof router.navigate === "function",
10699
+ snapshot: true,
10700
+ identityOverride: false
10701
+ }
10702
+ });
10703
+ }
10704
+ function setupArchieRouteListener(router, options = {}) {
10705
+ const allowedOrigins = options.allowedOrigins ?? DEFAULT_ARCHIE_COMMAND_ORIGINS;
10359
10706
  const target = window.parent !== window ? window.parent : window.opener;
10360
10707
  if (!target) {
10361
10708
  return () => {
10362
10709
  };
10363
10710
  }
10364
10711
  broadcastRouteState(target, router.state, extractAllRoutes(router.routes));
10712
+ broadcastCapabilities(router, target);
10365
10713
  const unsubscribe = router.subscribe((state) => {
10366
10714
  const currentRoutes = extractAllRoutes(router.routes);
10367
10715
  broadcastRouteState(target, state, currentRoutes);
10368
10716
  });
10369
10717
  const handleMessage = (event) => {
10370
- if (event.data?.type === "ARCHIE_QUERY_ROUTES") {
10371
- const currentRoutes = extractAllRoutes(router.routes);
10372
- broadcastRouteState(target, router.state, currentRoutes);
10718
+ if (event.source !== target) return;
10719
+ const data = event.data;
10720
+ switch (data?.type) {
10721
+ case "ARCHIE_QUERY_ROUTES":
10722
+ broadcastRouteState(
10723
+ target,
10724
+ router.state,
10725
+ extractAllRoutes(router.routes)
10726
+ );
10727
+ break;
10728
+ case "ARCHIE_QUERY_CAPABILITIES":
10729
+ broadcastCapabilities(router, target);
10730
+ break;
10731
+ case "ARCHIE_NAVIGATE":
10732
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string" && typeof data.path === "string") {
10733
+ void handleNavigate(router, target, event.origin, data);
10734
+ }
10735
+ break;
10736
+ case "ARCHIE_SNAPSHOT_DOM":
10737
+ if (isAllowedCommandOrigin(event.origin, allowedOrigins) && typeof data.requestId === "string") {
10738
+ void handleSnapshot(target, event.origin, data.requestId);
10739
+ }
10740
+ break;
10373
10741
  }
10374
10742
  };
10375
10743
  window.addEventListener("message", handleMessage);
@@ -10409,7 +10777,8 @@ function installDomMetaSanitizer() {
10409
10777
  function useArchieDevToolsRuntime({
10410
10778
  router,
10411
10779
  inspector,
10412
- inspectorOptions
10780
+ inspectorOptions,
10781
+ routeListenerOrigins
10413
10782
  }) {
10414
10783
  installDomMetaSanitizer();
10415
10784
  const stableOptionsKey = JSON.stringify(inspectorOptions ?? null);
@@ -10419,7 +10788,9 @@ function useArchieDevToolsRuntime({
10419
10788
  if (process.env.NODE_ENV !== "development") {
10420
10789
  return;
10421
10790
  }
10422
- const unsubscribe = setupArchieRouteListener(router);
10791
+ const unsubscribe = setupArchieRouteListener(router, {
10792
+ allowedOrigins: routeListenerOrigins
10793
+ });
10423
10794
  if (inspector !== false) {
10424
10795
  const useLocalInspector = readArchieLocalInspectorFlag();
10425
10796
  injectInspector({
@@ -10437,9 +10808,15 @@ function ArchieDevToolProvider({
10437
10808
  children,
10438
10809
  router,
10439
10810
  inspector,
10440
- inspectorOptions
10811
+ inspectorOptions,
10812
+ routeListenerOrigins
10441
10813
  }) {
10442
- useArchieDevToolsRuntime({ router, inspector, inspectorOptions });
10814
+ useArchieDevToolsRuntime({
10815
+ router,
10816
+ inspector,
10817
+ inspectorOptions,
10818
+ routeListenerOrigins
10819
+ });
10443
10820
  return /* @__PURE__ */ jsx6(DndProvider, { children });
10444
10821
  }
10445
10822
  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.13",
4
4
  "description": "DevTools for Archie generated applications - Route synchronization and editor communication",
5
5
  "sideEffects": false,
6
6
  "repository": {