@wix/himalaya-cli 0.1316.0 → 0.1317.0

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.
@@ -21,3 +21,4 @@ export * from "./app-link.js";
21
21
  export * from "./app-identity.js";
22
22
  export * from "./app-registry-client.js";
23
23
  export * from "./site-shell.js";
24
+ export * from "./site-menu.js";
@@ -2679,6 +2679,11 @@ export interface SiteNavConfig {
2679
2679
  /** The screen an on-site URL no route owns opens in. `""`/absent ⇒ the `/` route's
2680
2680
  * screen; no `/` route either ⇒ the link does not resolve. */
2681
2681
  fallbackScreenId?: string;
2682
+ /** Where the shell's tabs and drawer come from. `"runtime"`: the device reads the
2683
+ * site's own menu as pages load and builds the NEXT launch's chrome from it
2684
+ * (`mergeSiteMenu` in `@himalaya/core/site-menu`). `"contract"`/absent: the served
2685
+ * `tabBar`/`drawer`/`sidebar` exactly as they are. OTA, like the rest of this block. */
2686
+ menuSource?: "contract" | "runtime";
2682
2687
  }
2683
2688
  /** One entry of {@link SiteNavConfig.routes}. */
2684
2689
  export interface SiteRoute {
@@ -0,0 +1,6 @@
1
+ export declare const SITE_MENU_MESSAGE_NAME = "himalayaSiteMenu";
2
+ export declare const SITE_NAV_LINKS_JS = "(function(){var q=document.querySelectorAll(\"nav a, header a, [role=navigation] a\"),o=[],i,a,t,h;for(i=0;i<q.length&&o.length<24;i++){a=q[i];t=String(a.textContent||\"\").replace(/\\s+/g,\" \").trim();h=String(a.href||\"\");if(t&&/^https?:/.test(h))o.push({label:t,href:h})}return o})()";
3
+ export declare const SITE_MENU_SYSTEM_SEGMENT = "^(popup-.*|lightbox.*|.*thank-?you.*|checkout|cart(-page)?|payment-request(-page)?|expired-reset-link|access-restricted|reservation|(event|experience|reservation|order|item)-details|(product|service|category|group|project|collection|reviews-collection|donation-thank-you)-page|post|project|collection|group|booking-(form|calendar)|plan-customization|notifications?|followers?|following|my-(?!account$)[a-z-]+|profile|search(-results)?|blank(-\\d+)?|404)$";
4
+ export declare const SITE_MENU_SYSTEM_TITLE = "\\b(thank you|\\(popup\\)|leave (a )?review|notifications|followers|side cart|order error|pending approval|my (rewards|subscriptions|bookings|orders|groups|wallet))\\b|\\b(group|service|product|project|collection|category|cart|payment request) pages?$|\\b(event|experience|reservation|item) details\\b";
5
+ export declare const SITE_MENU_MEMBER_SEGMENT = "^(members?|account(-settings)?|my-account|login|sign-?in)$";
6
+ export declare const SITE_MENU_PAGE_SCRIPT = "(function(){try{if(window.top!==window||window.__himalayaSiteMenu)return;window.__himalayaSiteMenu=1;var S=new RegExp(\"^(popup-.*|lightbox.*|.*thank-?you.*|checkout|cart(-page)?|payment-request(-page)?|expired-reset-link|access-restricted|reservation|(event|experience|reservation|order|item)-details|(product|service|category|group|project|collection|reviews-collection|donation-thank-you)-page|post|project|collection|group|booking-(form|calendar)|plan-customization|notifications?|followers?|following|my-(?!account$)[a-z-]+|profile|search(-results)?|blank(-\\\\d+)?|404)$\",\"i\"),T=new RegExp(\"\\\\b(thank you|\\\\(popup\\\\)|leave (a )?review|notifications|followers|side cart|order error|pending approval|my (rewards|subscriptions|bookings|orders|groups|wallet))\\\\b|\\\\b(group|service|product|project|collection|category|cart|payment request) pages?$|\\\\b(event|experience|reservation|item) details\\\\b\",\"i\"),M=new RegExp(\"^(members?|account(-settings)?|my-account|login|sign-?in)$\",\"i\"),n=0,p=\"\";function bridge(){var w=window.webkit;return(w&&w.messageHandlers&&w.messageHandlers.himalayaSiteMenu)||window.himalayaSiteMenu||null}function read(){var l=(function(){var q=document.querySelectorAll(\"nav a, header a, [role=navigation] a\"),o=[],i,a,t,h;for(i=0;i<q.length&&o.length<24;i++){a=q[i];t=String(a.textContent||\"\").replace(/\\s+/g,\" \").trim();h=String(a.href||\"\");if(t&&/^https?:/.test(h))o.push({label:t,href:h})}return o})(),o=[],i,u,s;for(i=0;i<l.length;i++){try{u=new URL(l[i].href)}catch(e){continue}s=u.pathname.split(\"/\").filter(Boolean).pop()||\"\";try{s=decodeURIComponent(s)}catch(e){}if(S.test(s)||M.test(s)||T.test(l[i].label))continue;o.push({label:l[i].label,href:l[i].href})}return o}function tick(){var b=bridge();if(!b)return;var o=read(),j=JSON.stringify(o);if(o.length&&(j===p||n>=5)){b.postMessage(JSON.stringify({v:1,href:String(location.href),items:o}));return}p=o.length?j:\"\";if(++n<=5)setTimeout(tick,500*n)}if(document.readyState===\"loading\")document.addEventListener(\"DOMContentLoaded\",tick);else tick()}catch(e){}})();";
@@ -0,0 +1,82 @@
1
+ import type { DrawerItem, LinkableScreen, NavTabItem, SiteNavConfig } from "./schema.js";
2
+ export { SITE_MENU_MEMBER_SEGMENT, SITE_MENU_MESSAGE_NAME, SITE_MENU_PAGE_SCRIPT, SITE_MENU_SYSTEM_SEGMENT, SITE_MENU_SYSTEM_TITLE, SITE_NAV_LINKS_JS, } from "./site-menu-script.generated.js";
3
+ /** Where a site shell's tabs and drawer come from. Absent ⇒ `"contract"`. */
4
+ export type SiteMenuSource = "contract" | "runtime";
5
+ export declare const SITE_MENU_SOURCES: readonly SiteMenuSource[];
6
+ /** The most menu items a device keeps — the analyzer's own page cap is 10. */
7
+ export declare const SITE_MENU_MAX_ITEMS = 12;
8
+ /** A menu label's cap, in code points — the contract's page-title cap. */
9
+ export declare const SITE_MENU_LABEL_MAX = 40;
10
+ /** The longest page message the native side parses, in UTF-16 units. */
11
+ export declare const SITE_MENU_PAYLOAD_MAX = 16384;
12
+ /** The longest item href the native side considers. */
13
+ export declare const SITE_MENU_HREF_MAX = 2048;
14
+ /** The most page items the native side reads before it caps (the page script caps at 24 too). */
15
+ export declare const SITE_MENU_RAW_ITEMS_MAX = 24;
16
+ /** Id prefixes for a runtime-only page's rows. A hyphen keeps them out of the page-id space
17
+ * (`[a-z0-9_]+`), so a page id can never collide with one. */
18
+ export declare const SITE_MENU_DRAWER_ID_PREFIX = "drawer_site-menu-";
19
+ export declare const SITE_MENU_SIDEBAR_ID_PREFIX = "site-menu-";
20
+ /** The icon a page the contract does not know gets. */
21
+ export declare const SITE_MENU_DEFAULT_ICON = "globe";
22
+ /** One item of a site's menu, as the device keeps it: a clean label and a path RELATIVE to the
23
+ * site's base URL (`"/"`, `"/book-online"`), percent-encoded as the page sent it. */
24
+ export interface SiteMenuItem {
25
+ label: string;
26
+ path: string;
27
+ }
28
+ /** The chrome a merge reconciles: the served tab bar, drawer and rail items, plus the linkable
29
+ * screens whose `tabId` must not name a tab the menu dropped. */
30
+ export interface SiteMenuNav {
31
+ tabs: NavTabItem[];
32
+ drawer: DrawerItem[];
33
+ sidebar: NavTabItem[];
34
+ linkableScreens: LinkableScreen[];
35
+ }
36
+ /** What a runtime menu changed relative to the contract — reported as telemetry. */
37
+ export interface SiteMenuDrift {
38
+ /** Paths the menu lists that no contract page owns. */
39
+ added: string[];
40
+ /** Contract page screens (that had a tab or a row) the menu no longer lists. */
41
+ removed: string[];
42
+ /** Contract page screens the menu labels differently. */
43
+ renamed: string[];
44
+ }
45
+ export interface SiteMenuMerge extends SiteMenuNav {
46
+ /** Row id → the absolute URL that row opens, for every runtime-only page's drawer and rail row. */
47
+ links: Record<string, string>;
48
+ drift: SiteMenuDrift;
49
+ }
50
+ /** Whether a site description opts into the runtime menu. */
51
+ export declare function siteMenuIsRuntime(site: Pick<SiteNavConfig, "menuSource"> | undefined): boolean;
52
+ /** A menu label in its kept form: control characters and whitespace runs as one space, trimmed,
53
+ * Wix's `(New)` editor marker dropped, capped at {@link SITE_MENU_LABEL_MAX} code points. */
54
+ export declare function cleanSiteMenuLabel(raw: string): string;
55
+ /** A site-relative path in comparable form: segments percent-decoded, empty ones dropped. `""` is
56
+ * the home page. */
57
+ export declare function siteMenuPathKey(path: string): string;
58
+ /**
59
+ * `href` relative to the site, or null when it is not one of the site's own pages: https only, no
60
+ * credentials, on one of `origins` (exact host, `:443` the default port, the path the origin's
61
+ * boundary or under it), and no `.`/`..` segment (a WebView resolves them, and on a free
62
+ * `<user>.wixsite.com` host `/my-site/../other` is somebody else's site). The result has a leading
63
+ * slash, no trailing one, and no query or fragment.
64
+ */
65
+ export declare function siteMenuPath(href: string, origins: readonly string[]): string | null;
66
+ /**
67
+ * The page's menu message, validated. `payload` is what the page posted — a JSON STRING
68
+ * `{ v: 1, href, items: [{ label, href }] }`, which is all the page script ever sends; anything else
69
+ * (a parsed object, a number) is refused, so the size bound always applies. Returns null for anything that
70
+ * is not a menu (malformed, oversized, wrong version, no usable item). Items are kept in the page's
71
+ * order; each must be a labelled page of the site; the first item for a path wins; at most
72
+ * {@link SITE_MENU_MAX_ITEMS}.
73
+ */
74
+ export declare function normalizeSiteMenu(payload: unknown, site: Pick<SiteNavConfig, "origins"> | undefined): SiteMenuItem[] | null;
75
+ /**
76
+ * Reconcile the contract's chrome with a site's menu (see the module header for the rules). Returns
77
+ * null — "show the contract" — when the site does not opt in (`menuSource !== "runtime"`), when
78
+ * there is no menu, or when the merge would leave the tab bar with no site page.
79
+ */
80
+ export declare function mergeSiteMenu(nav: SiteMenuNav, site: SiteNavConfig | undefined, menu: readonly SiteMenuItem[] | null | undefined): SiteMenuMerge | null;
81
+ /** Whether a merge changed anything the operator should hear about. */
82
+ export declare function siteMenuDrifted(drift: SiteMenuDrift): boolean;
@@ -71,6 +71,12 @@ export interface SiteShellConfig {
71
71
  * not a destination), so they live here rather than in {@link pages}; the site analyzer fills
72
72
  * them from the pages it actually found. Absent ⇒ that notification opens the home page. */
73
73
  memberPages?: SiteMemberPages;
74
+ /** Where the native tabs and drawer come from. Absent ⇒ `"contract"`: exactly {@link pages}.
75
+ * `"runtime"`: the device reads the site's own menu as pages load and the NEXT launch builds the
76
+ * chrome from it (`mergeSiteMenu`, `@himalaya/core/site-menu`) — {@link pages} still decides each
77
+ * page's placement and icon, and stays the chrome until a menu has been read. Served OTA in
78
+ * `navigation.site.menuSource`. */
79
+ menuSource?: "contract" | "runtime";
74
80
  }
75
81
  /** Members Area pages, by what a notification is about. */
76
82
  export interface SiteMemberPages {
@@ -2804,8 +2804,8 @@ var require_validate = __commonJS({
2804
2804
  return data;
2805
2805
  }
2806
2806
  let expr = data;
2807
- const segments = jsonPointer.split("/");
2808
- for (const segment of segments) {
2807
+ const segments2 = jsonPointer.split("/");
2808
+ for (const segment of segments2) {
2809
2809
  if (segment) {
2810
2810
  data = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)((0, util_1.unescapeJsonPointer)(segment))}`;
2811
2811
  expr = (0, codegen_1._)`${expr} && ${data}`;
@@ -4314,9 +4314,9 @@ var require_core = __commonJS({
4314
4314
  const rules = this.RULES.all;
4315
4315
  metaSchema = JSON.parse(JSON.stringify(metaSchema));
4316
4316
  for (const jsonPointer of keywordsJsonPointers) {
4317
- const segments = jsonPointer.split("/").slice(1);
4317
+ const segments2 = jsonPointer.split("/").slice(1);
4318
4318
  let keywords = metaSchema;
4319
- for (const seg of segments)
4319
+ for (const seg of segments2)
4320
4320
  keywords = keywords[seg];
4321
4321
  for (const key in rules) {
4322
4322
  const rule = rules[key];
@@ -7665,6 +7665,9 @@ function validateSiteNav(site) {
7665
7665
  if (site.fallbackScreenId !== void 0 && typeof site.fallbackScreenId !== "string") {
7666
7666
  throw new Error("defineApp: navigation.site.fallbackScreenId must be a string.");
7667
7667
  }
7668
+ if (site.menuSource !== void 0 && site.menuSource !== "contract" && site.menuSource !== "runtime") {
7669
+ throw new Error('defineApp: navigation.site.menuSource must be "contract" or "runtime".');
7670
+ }
7668
7671
  }
7669
7672
  var SPLASH_MIN_VISIBLE_MS_MAX = 2e3;
7670
7673
  var SPLASH_MAX_HOLD_MS_MAX = 8e3;
@@ -13371,7 +13374,9 @@ function siteNavConfig(site) {
13371
13374
  return {
13372
13375
  origins: siteAllowedOrigins(site.origin, site.kind),
13373
13376
  routes: site.pages.map((page) => ({ path: siteRoutePath(page.path), screenId: siteScreenId(page) })),
13374
- fallbackScreenId: SITE_PAGE_SCREEN_ID
13377
+ fallbackScreenId: SITE_PAGE_SCREEN_ID,
13378
+ // Emitted only when the site opts in, so a contract-menu app serves the same config as before.
13379
+ ...site.menuSource === "runtime" ? { menuSource: "runtime" } : {}
13375
13380
  };
13376
13381
  }
13377
13382
  function siteLinkableScreens(site) {
@@ -13507,10 +13512,224 @@ function assertSiteShellConfig(site) {
13507
13512
  if (site.layout !== void 0 && !SITE_LAYOUTS.includes(site.layout)) {
13508
13513
  throw new Error(`site layout ${JSON.stringify(site.layout)} must be one of ${SITE_LAYOUTS.join(", ")}`);
13509
13514
  }
13515
+ if (site.menuSource !== void 0 && site.menuSource !== "contract" && site.menuSource !== "runtime") {
13516
+ throw new Error(`site menuSource ${JSON.stringify(site.menuSource)} must be "contract" or "runtime"`);
13517
+ }
13510
13518
  if (!site.pages.some((page) => (page.placement ?? "tab") === "tab")) {
13511
13519
  throw new Error('a site shell needs at least one page with placement "tab"');
13512
13520
  }
13513
13521
  }
13522
+
13523
+ // ../../core/ts/src/site-menu-script.generated.ts
13524
+ var SITE_MENU_MESSAGE_NAME = "himalayaSiteMenu";
13525
+ var SITE_NAV_LINKS_JS = '(function(){var q=document.querySelectorAll("nav a, header a, [role=navigation] a"),o=[],i,a,t,h;for(i=0;i<q.length&&o.length<24;i++){a=q[i];t=String(a.textContent||"").replace(/\\s+/g," ").trim();h=String(a.href||"");if(t&&/^https?:/.test(h))o.push({label:t,href:h})}return o})()';
13526
+ var SITE_MENU_SYSTEM_SEGMENT = "^(popup-.*|lightbox.*|.*thank-?you.*|checkout|cart(-page)?|payment-request(-page)?|expired-reset-link|access-restricted|reservation|(event|experience|reservation|order|item)-details|(product|service|category|group|project|collection|reviews-collection|donation-thank-you)-page|post|project|collection|group|booking-(form|calendar)|plan-customization|notifications?|followers?|following|my-(?!account$)[a-z-]+|profile|search(-results)?|blank(-\\d+)?|404)$";
13527
+ var SITE_MENU_SYSTEM_TITLE = "\\b(thank you|\\(popup\\)|leave (a )?review|notifications|followers|side cart|order error|pending approval|my (rewards|subscriptions|bookings|orders|groups|wallet))\\b|\\b(group|service|product|project|collection|category|cart|payment request) pages?$|\\b(event|experience|reservation|item) details\\b";
13528
+ var SITE_MENU_MEMBER_SEGMENT = "^(members?|account(-settings)?|my-account|login|sign-?in)$";
13529
+ var SITE_MENU_PAGE_SCRIPT = '(function(){try{if(window.top!==window||window.__himalayaSiteMenu)return;window.__himalayaSiteMenu=1;var S=new RegExp("^(popup-.*|lightbox.*|.*thank-?you.*|checkout|cart(-page)?|payment-request(-page)?|expired-reset-link|access-restricted|reservation|(event|experience|reservation|order|item)-details|(product|service|category|group|project|collection|reviews-collection|donation-thank-you)-page|post|project|collection|group|booking-(form|calendar)|plan-customization|notifications?|followers?|following|my-(?!account$)[a-z-]+|profile|search(-results)?|blank(-\\\\d+)?|404)$","i"),T=new RegExp("\\\\b(thank you|\\\\(popup\\\\)|leave (a )?review|notifications|followers|side cart|order error|pending approval|my (rewards|subscriptions|bookings|orders|groups|wallet))\\\\b|\\\\b(group|service|product|project|collection|category|cart|payment request) pages?$|\\\\b(event|experience|reservation|item) details\\\\b","i"),M=new RegExp("^(members?|account(-settings)?|my-account|login|sign-?in)$","i"),n=0,p="";function bridge(){var w=window.webkit;return(w&&w.messageHandlers&&w.messageHandlers.himalayaSiteMenu)||window.himalayaSiteMenu||null}function read(){var l=(function(){var q=document.querySelectorAll("nav a, header a, [role=navigation] a"),o=[],i,a,t,h;for(i=0;i<q.length&&o.length<24;i++){a=q[i];t=String(a.textContent||"").replace(/\\s+/g," ").trim();h=String(a.href||"");if(t&&/^https?:/.test(h))o.push({label:t,href:h})}return o})(),o=[],i,u,s;for(i=0;i<l.length;i++){try{u=new URL(l[i].href)}catch(e){continue}s=u.pathname.split("/").filter(Boolean).pop()||"";try{s=decodeURIComponent(s)}catch(e){}if(S.test(s)||M.test(s)||T.test(l[i].label))continue;o.push({label:l[i].label,href:l[i].href})}return o}function tick(){var b=bridge();if(!b)return;var o=read(),j=JSON.stringify(o);if(o.length&&(j===p||n>=5)){b.postMessage(JSON.stringify({v:1,href:String(location.href),items:o}));return}p=o.length?j:"";if(++n<=5)setTimeout(tick,500*n)}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",tick);else tick()}catch(e){}})();';
13530
+
13531
+ // ../../core/ts/src/site-menu.ts
13532
+ var SITE_MENU_SOURCES = ["contract", "runtime"];
13533
+ var SITE_MENU_MAX_ITEMS = 12;
13534
+ var SITE_MENU_LABEL_MAX = 40;
13535
+ var SITE_MENU_PAYLOAD_MAX = 16384;
13536
+ var SITE_MENU_HREF_MAX = 2048;
13537
+ var SITE_MENU_RAW_ITEMS_MAX = 24;
13538
+ var SITE_MENU_DRAWER_ID_PREFIX = "drawer_site-menu-";
13539
+ var SITE_MENU_SIDEBAR_ID_PREFIX = "site-menu-";
13540
+ var SITE_MENU_DEFAULT_ICON = "globe";
13541
+ function siteMenuIsRuntime(site) {
13542
+ return site?.menuSource === "runtime";
13543
+ }
13544
+ function cleanSiteMenuLabel(raw) {
13545
+ const one = raw.replace(/[\u0000-\u001F\u007F\s]+/g, " ").trim().replace(/\s*\(new\)\s*$/i, "").trim();
13546
+ const points = Array.from(one);
13547
+ return points.length <= SITE_MENU_LABEL_MAX ? one : points.slice(0, SITE_MENU_LABEL_MAX).join("").trimEnd();
13548
+ }
13549
+ function segments(path) {
13550
+ return path.split("/").filter(Boolean);
13551
+ }
13552
+ function decodeSegment2(segment) {
13553
+ try {
13554
+ return decodeURIComponent(segment);
13555
+ } catch {
13556
+ return segment;
13557
+ }
13558
+ }
13559
+ function siteMenuPathKey(path) {
13560
+ const bare = path.split(/[?#]/, 1)[0] ?? "";
13561
+ return segments(bare).map(decodeSegment2).join("/");
13562
+ }
13563
+ function siteMenuPath(href, origins) {
13564
+ if (typeof href !== "string" || href.length > SITE_MENU_HREF_MAX) return null;
13565
+ const raw = href.trim();
13566
+ const rawPath = raw.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/?#]*/i, "").split(/[?#]/, 1)[0] ?? "";
13567
+ if (/%2f|%5c/i.test(rawPath)) return null;
13568
+ if (rawPath.split("/").some((seg) => {
13569
+ const d = decodeSegment2(seg);
13570
+ return d === "." || d === "..";
13571
+ })) return null;
13572
+ let parsed;
13573
+ try {
13574
+ parsed = new URL(raw);
13575
+ } catch {
13576
+ return null;
13577
+ }
13578
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password) return null;
13579
+ for (const origin of origins) {
13580
+ let o;
13581
+ try {
13582
+ o = new URL(origin);
13583
+ } catch {
13584
+ continue;
13585
+ }
13586
+ if (o.protocol !== "https:" || o.host !== parsed.host) continue;
13587
+ const base = o.pathname.replace(/\/+$/, "");
13588
+ const path = parsed.pathname;
13589
+ if (path === base || path === `${base}/` || path.startsWith(`${base}/`)) {
13590
+ return `/${segments(path.slice(base.length)).join("/")}`;
13591
+ }
13592
+ }
13593
+ return null;
13594
+ }
13595
+ function normalizeSiteMenu(payload, site) {
13596
+ if (!site?.origins?.length) return null;
13597
+ if (typeof payload !== "string" || payload.length > SITE_MENU_PAYLOAD_MAX) return null;
13598
+ let message;
13599
+ try {
13600
+ message = JSON.parse(payload);
13601
+ } catch {
13602
+ return null;
13603
+ }
13604
+ if (!message || typeof message !== "object" || Array.isArray(message)) return null;
13605
+ const { v, items } = message;
13606
+ if (v !== 1 || !Array.isArray(items)) return null;
13607
+ const out = [];
13608
+ const seen = /* @__PURE__ */ new Set();
13609
+ for (const item of items.slice(0, SITE_MENU_RAW_ITEMS_MAX)) {
13610
+ if (out.length >= SITE_MENU_MAX_ITEMS) break;
13611
+ if (!item || typeof item !== "object") continue;
13612
+ const { label, href } = item;
13613
+ if (typeof label !== "string" || typeof href !== "string") continue;
13614
+ const clean = cleanSiteMenuLabel(label);
13615
+ if (!clean) continue;
13616
+ const path = siteMenuPath(href, site.origins);
13617
+ if (path === null) continue;
13618
+ const key = siteMenuPathKey(path);
13619
+ if (seen.has(key)) continue;
13620
+ seen.add(key);
13621
+ out.push({ label: clean, path });
13622
+ }
13623
+ return out.length ? out : null;
13624
+ }
13625
+ function spliceSiteBlock(list, isSite, block) {
13626
+ const out = [];
13627
+ let inserted = false;
13628
+ for (const item of list) {
13629
+ if (!isSite(item)) {
13630
+ out.push(item);
13631
+ continue;
13632
+ }
13633
+ if (!inserted) {
13634
+ out.push(...block);
13635
+ inserted = true;
13636
+ }
13637
+ }
13638
+ return out;
13639
+ }
13640
+ function mergeSiteMenu(nav, site, menu) {
13641
+ if (!site || !siteMenuIsRuntime(site) || !menu?.length) return null;
13642
+ const routes = (site.routes ?? []).filter((r) => r?.screenId);
13643
+ if (!routes.length) return null;
13644
+ const fallback = site.fallbackScreenId ?? "";
13645
+ const base = (site.origins ?? [])[0];
13646
+ const screenByKey = /* @__PURE__ */ new Map();
13647
+ for (const route of routes) {
13648
+ const key = siteMenuPathKey(route.path ?? "");
13649
+ if (!screenByKey.has(key)) screenByKey.set(key, route.screenId);
13650
+ }
13651
+ const siteScreens2 = new Set(routes.map((r) => r.screenId));
13652
+ const home = screenByKey.get("");
13653
+ const entries = [];
13654
+ const kept = /* @__PURE__ */ new Set();
13655
+ for (const item of menu) {
13656
+ const key = siteMenuPathKey(item.path);
13657
+ const screenId = screenByKey.get(key);
13658
+ if (screenId) {
13659
+ if (kept.has(screenId)) continue;
13660
+ kept.add(screenId);
13661
+ entries.push({ kind: "page", screenId, label: screenId === home ? null : item.label });
13662
+ } else if (key !== "" && fallback && base) {
13663
+ entries.push({ kind: "new", path: item.path, label: item.label });
13664
+ }
13665
+ }
13666
+ if (home) {
13667
+ const at = entries.findIndex((e) => e.kind === "page" && e.screenId === home);
13668
+ if (at > 0) entries.unshift(...entries.splice(at, 1));
13669
+ if (at < 0) entries.unshift({ kind: "page", screenId: home, label: null });
13670
+ kept.add(home);
13671
+ }
13672
+ const isSite = (item) => !!item.screenId && siteScreens2.has(item.screenId);
13673
+ const labelOf = (e, fallbackLabel) => e.label ?? fallbackLabel;
13674
+ const siteTabs = nav.tabs.filter(isSite);
13675
+ const tabBlock = [];
13676
+ for (const e of entries) {
13677
+ if (e.kind !== "page") continue;
13678
+ const tab = siteTabs.find((t) => t.screenId === e.screenId);
13679
+ if (tab && tabBlock.length < SITE_SHELL_MAX_TABS) tabBlock.push({ ...tab, label: labelOf(e, tab.label) });
13680
+ }
13681
+ if (siteTabs.length && !tabBlock.length) return null;
13682
+ const tabs = siteTabs.length ? spliceSiteBlock(nav.tabs, isSite, tabBlock) : [...nav.tabs];
13683
+ const links = {};
13684
+ const urlOf = (path) => `${base.endsWith("/") ? base : `${base}/`}${path.replace(/^\/+/, "")}`;
13685
+ const siteRows = nav.drawer.filter(isSite);
13686
+ const siteRail = nav.sidebar.filter(isSite);
13687
+ const drawerBlock = [];
13688
+ const railBlock = [];
13689
+ let n = 0;
13690
+ for (const e of entries) {
13691
+ if (e.kind === "page") {
13692
+ const row = siteRows.find((r) => r.screenId === e.screenId);
13693
+ if (row) drawerBlock.push({ ...row, label: labelOf(e, row.label) });
13694
+ const rail = siteRail.find((r) => r.screenId === e.screenId);
13695
+ if (rail) railBlock.push({ ...rail, label: labelOf(e, rail.label) });
13696
+ continue;
13697
+ }
13698
+ n += 1;
13699
+ const url = urlOf(e.path);
13700
+ if (siteRows.length) {
13701
+ const id = `${SITE_MENU_DRAWER_ID_PREFIX}${n}`;
13702
+ drawerBlock.push({ id, screenId: fallback, label: e.label, icon: SITE_MENU_DEFAULT_ICON });
13703
+ links[id] = url;
13704
+ }
13705
+ if (siteRail.length) {
13706
+ const id = `${SITE_MENU_SIDEBAR_ID_PREFIX}${n}`;
13707
+ railBlock.push({ id, screenId: fallback, label: e.label, icon: SITE_MENU_DEFAULT_ICON });
13708
+ links[id] = url;
13709
+ }
13710
+ }
13711
+ const drawer = siteRows.length ? spliceSiteBlock(nav.drawer, isSite, drawerBlock) : [...nav.drawer];
13712
+ const sidebar = siteRail.length ? spliceSiteBlock(nav.sidebar, isSite, railBlock) : [...nav.sidebar];
13713
+ const tabIds = new Set(tabs.map((t) => t.id));
13714
+ const droppedTabs = new Set(nav.tabs.filter((t) => !tabIds.has(t.id)).map((t) => t.id));
13715
+ const linkableScreens = nav.linkableScreens.map((s) => s.tabId && droppedTabs.has(s.tabId) ? { ...s, tabId: "" } : s);
13716
+ const listed = [...nav.tabs, ...nav.drawer, ...nav.sidebar].filter(isSite);
13717
+ const removed = [];
13718
+ for (const item of listed) {
13719
+ if (!kept.has(item.screenId) && !removed.includes(item.screenId)) removed.push(item.screenId);
13720
+ }
13721
+ const renamed = [];
13722
+ for (const e of entries) {
13723
+ if (e.kind !== "page" || e.label === null) continue;
13724
+ const contract = listed.find((item) => item.screenId === e.screenId)?.label;
13725
+ if (contract !== void 0 && contract !== e.label && !renamed.includes(e.screenId)) renamed.push(e.screenId);
13726
+ }
13727
+ const added = entries.flatMap((e) => e.kind === "new" ? [e.path] : []);
13728
+ return { tabs, drawer, sidebar, linkableScreens, links, drift: { added, removed, renamed } };
13729
+ }
13730
+ function siteMenuDrifted(drift) {
13731
+ return drift.added.length > 0 || drift.removed.length > 0 || drift.renamed.length > 0;
13732
+ }
13514
13733
  export {
13515
13734
  ALL_ORIENTATIONS,
13516
13735
  APP_ID_PATTERN,
@@ -13551,6 +13770,21 @@ export {
13551
13770
  RICH_CONTENT_EDITOR_TOOLS,
13552
13771
  RTL_LOCALES,
13553
13772
  SITE_LAYOUTS,
13773
+ SITE_MENU_DEFAULT_ICON,
13774
+ SITE_MENU_DRAWER_ID_PREFIX,
13775
+ SITE_MENU_HREF_MAX,
13776
+ SITE_MENU_LABEL_MAX,
13777
+ SITE_MENU_MAX_ITEMS,
13778
+ SITE_MENU_MEMBER_SEGMENT,
13779
+ SITE_MENU_MESSAGE_NAME,
13780
+ SITE_MENU_PAGE_SCRIPT,
13781
+ SITE_MENU_PAYLOAD_MAX,
13782
+ SITE_MENU_RAW_ITEMS_MAX,
13783
+ SITE_MENU_SIDEBAR_ID_PREFIX,
13784
+ SITE_MENU_SOURCES,
13785
+ SITE_MENU_SYSTEM_SEGMENT,
13786
+ SITE_MENU_SYSTEM_TITLE,
13787
+ SITE_NAV_LINKS_JS,
13554
13788
  SITE_PAGE_SCREEN_ID,
13555
13789
  SITE_SCREEN_PREFIX,
13556
13790
  SITE_SHELL_MAX_TABS,
@@ -13584,6 +13818,7 @@ export {
13584
13818
  chassisOuterSize,
13585
13819
  checkTierCaps,
13586
13820
  chooseCandidate,
13821
+ cleanSiteMenuLabel,
13587
13822
  coerceSupportedLocale,
13588
13823
  dataSchemaScopeOf,
13589
13824
  decideSwitch,
@@ -13618,7 +13853,9 @@ export {
13618
13853
  isWidgetFamilySurface,
13619
13854
  layoutDirection,
13620
13855
  mergeIdiomOverlay,
13856
+ mergeSiteMenu,
13621
13857
  normalizeServeApp,
13858
+ normalizeSiteMenu,
13622
13859
  normalizeSiteOrigin,
13623
13860
  parseNumeric,
13624
13861
  primarySubtag,
@@ -13646,6 +13883,10 @@ export {
13646
13883
  siteAllowedOrigins,
13647
13884
  siteHost,
13648
13885
  siteLinkableScreens,
13886
+ siteMenuDrifted,
13887
+ siteMenuIsRuntime,
13888
+ siteMenuPath,
13889
+ siteMenuPathKey,
13649
13890
  siteNav,
13650
13891
  siteNavConfig,
13651
13892
  siteNavKind,
package/dist/cli.mjs CHANGED
@@ -7530,6 +7530,7 @@ function buttonVariant(style) {
7530
7530
  var SYSTEM_FONT_FAMILY, TONAL_ELEVATION_ALPHA, BUTTON_MIN_HIT_TARGET, nonNegative, nonEmpty, own, COLOR_TOKEN_PROPS, TYPOGRAPHY_TOKEN_PROPS, BUTTON_VARIANTS;
7531
7531
  var init_tokens = __esm({
7532
7532
  "../../core/ts/src/tokens.ts"() {
7533
+ "use strict";
7533
7534
  SYSTEM_FONT_FAMILY = "$system";
7534
7535
  TONAL_ELEVATION_ALPHA = [0, 0.05, 0.08, 0.11, 0.12, 0.14];
7535
7536
  BUTTON_MIN_HIT_TARGET = 44;
@@ -32256,6 +32257,9 @@ function assertSiteShellConfig(site) {
32256
32257
  if (site.layout !== void 0 && !SITE_LAYOUTS.includes(site.layout)) {
32257
32258
  throw new Error(`site layout ${JSON.stringify(site.layout)} must be one of ${SITE_LAYOUTS.join(", ")}`);
32258
32259
  }
32260
+ if (site.menuSource !== void 0 && site.menuSource !== "contract" && site.menuSource !== "runtime") {
32261
+ throw new Error(`site menuSource ${JSON.stringify(site.menuSource)} must be "contract" or "runtime"`);
32262
+ }
32259
32263
  if (!site.pages.some((page) => (page.placement ?? "tab") === "tab")) {
32260
32264
  throw new Error('a site shell needs at least one page with placement "tab"');
32261
32265
  }
@@ -32271,6 +32275,24 @@ var init_site_shell = __esm({
32271
32275
  }
32272
32276
  });
32273
32277
 
32278
+ // ../../core/ts/src/site-menu-script.generated.ts
32279
+ var SITE_NAV_LINKS_JS, SITE_MENU_SYSTEM_SEGMENT, SITE_MENU_SYSTEM_TITLE;
32280
+ var init_site_menu_script_generated = __esm({
32281
+ "../../core/ts/src/site-menu-script.generated.ts"() {
32282
+ SITE_NAV_LINKS_JS = '(function(){var q=document.querySelectorAll("nav a, header a, [role=navigation] a"),o=[],i,a,t,h;for(i=0;i<q.length&&o.length<24;i++){a=q[i];t=String(a.textContent||"").replace(/\\s+/g," ").trim();h=String(a.href||"");if(t&&/^https?:/.test(h))o.push({label:t,href:h})}return o})()';
32283
+ SITE_MENU_SYSTEM_SEGMENT = "^(popup-.*|lightbox.*|.*thank-?you.*|checkout|cart(-page)?|payment-request(-page)?|expired-reset-link|access-restricted|reservation|(event|experience|reservation|order|item)-details|(product|service|category|group|project|collection|reviews-collection|donation-thank-you)-page|post|project|collection|group|booking-(form|calendar)|plan-customization|notifications?|followers?|following|my-(?!account$)[a-z-]+|profile|search(-results)?|blank(-\\d+)?|404)$";
32284
+ SITE_MENU_SYSTEM_TITLE = "\\b(thank you|\\(popup\\)|leave (a )?review|notifications|followers|side cart|order error|pending approval|my (rewards|subscriptions|bookings|orders|groups|wallet))\\b|\\b(group|service|product|project|collection|category|cart|payment request) pages?$|\\b(event|experience|reservation|item) details\\b";
32285
+ }
32286
+ });
32287
+
32288
+ // ../../core/ts/src/site-menu.ts
32289
+ var init_site_menu = __esm({
32290
+ "../../core/ts/src/site-menu.ts"() {
32291
+ init_site_shell();
32292
+ init_site_menu_script_generated();
32293
+ }
32294
+ });
32295
+
32274
32296
  // ../../core/ts/src/index.ts
32275
32297
  var init_src4 = __esm({
32276
32298
  "../../core/ts/src/index.ts"() {
@@ -32298,6 +32320,7 @@ var init_src4 = __esm({
32298
32320
  init_app_identity();
32299
32321
  init_app_registry_client();
32300
32322
  init_site_shell();
32323
+ init_site_menu();
32301
32324
  }
32302
32325
  });
32303
32326
 
@@ -168276,6 +168299,7 @@ var init_crawl_look = __esm({
168276
168299
  init_rest();
168277
168300
  init_net_guard();
168278
168301
  init_write_artifacts();
168302
+ init_site_menu_script_generated();
168279
168303
  PLAYWRIGHT_HINT = "npx playwright install chromium";
168280
168304
  PAGE_EVAL = `(() => {
168281
168305
  const wst = {};
@@ -168335,10 +168359,10 @@ var init_crawl_look = __esm({
168335
168359
  const navAnchors = Array.from(document.querySelectorAll("nav a, header a, [role=navigation] a"));
168336
168360
  const nav = navAnchors.map((a) => (a.textContent || "").trim()).filter(Boolean).slice(0, 24);
168337
168361
  // WHERE each label goes \u2014 a site shell orders its tabs by these (RFC 0062). a.href is
168338
- // already absolute; an anchor with no text or no href is not a page link.
168339
- const navLinks = navAnchors
168340
- .map((a) => ({ label: txt(a), href: String(a.href || "") }))
168341
- .filter((l) => l.label && /^https?:/.test(l.href)).slice(0, 24);
168362
+ // already absolute; an anchor with no text or no href is not a page link. The SAME extraction a
168363
+ // site shell runs on the device for its runtime menu (#3675) \u2014 one generated source, so the menu
168364
+ // read at build time and the menu read at runtime agree.
168365
+ const navLinks = ${SITE_NAV_LINKS_JS};
168342
168366
  const headings = Array.from(document.querySelectorAll("h1,h2")).map((h) => (h.textContent || "").trim()).filter(Boolean).slice(0, 12);
168343
168367
  const hero = ((document.querySelector("h1") && document.querySelector("h1").textContent) || "").trim();
168344
168368
 
@@ -170508,6 +170532,7 @@ var DERIVED_SITE_MAX_PAGES, SITE_PAGE_TITLE_MAX, ROLE_ICON, TAB_ROLES, SYSTEM_SE
170508
170532
  var init_site_shell_derive = __esm({
170509
170533
  "src/site-analyze/site-shell-derive.ts"() {
170510
170534
  init_site_shell();
170535
+ init_site_menu_script_generated();
170511
170536
  init_traits();
170512
170537
  DERIVED_SITE_MAX_PAGES = 10;
170513
170538
  SITE_PAGE_TITLE_MAX = 40;
@@ -170528,8 +170553,8 @@ var init_site_shell_derive = __esm({
170528
170553
  other: "globe"
170529
170554
  };
170530
170555
  TAB_ROLES = /* @__PURE__ */ new Set(["shop", "book", "menu", "events", "plans", "blog", "gallery", "library"]);
170531
- SYSTEM_SEGMENT = /^(popup-.*|lightbox.*|.*thank-?you.*|checkout|cart(-page)?|payment-request(-page)?|expired-reset-link|access-restricted|reservation|(event|experience|reservation|order|item)-details|(product|service|category|group|project|collection|reviews-collection|donation-thank-you)-page|post|project|collection|group|booking-(form|calendar)|plan-customization|notifications?|followers?|following|my-(?!account$)[a-z-]+|profile|search(-results)?|blank(-\d+)?|404)$/i;
170532
- SYSTEM_TITLE = /\b(thank you|\(popup\)|leave (a )?review|notifications|followers|side cart|order error|pending approval|my (rewards|subscriptions|bookings|orders|groups|wallet))\b|\b(group|service|product|project|collection|category|cart|payment request) pages?$|\b(event|experience|reservation|item) details\b/i;
170556
+ SYSTEM_SEGMENT = new RegExp(SITE_MENU_SYSTEM_SEGMENT, "i");
170557
+ SYSTEM_TITLE = new RegExp(SITE_MENU_SYSTEM_TITLE, "i");
170533
170558
  HOME_WORD = /^(home|home ?page|main|start|accueil|inicio|início|startseite|home ?sweet ?home|בית|דף הבית|ראשי|الرئيسية|ホーム|首页|홈)$/i;
170534
170559
  MEMBER_PAGE_ROUTES = {
170535
170560
  "account/my-orders": "orders",
@@ -1058,7 +1058,7 @@
1058
1058
  "name": "SiteNavConfig",
1059
1059
  "kind": "interface",
1060
1060
  "jsdoc": "/** How an incoming site URL finds its screen. */",
1061
- "decl": "export interface SiteNavConfig {\n /** https PATH BOUNDARIES (`https://owner.wixsite.com/my-shop/`), the same form as the\n * site's WebView `allowedOrigins`: exact host, and the path is the boundary or under it. */\n origins: string[];\n /** Longest path-segment prefix wins, on the path RELATIVE to the matched origin.\n * `/` matches the home page only. */\n routes: SiteRoute[];\n /** The screen an on-site URL no route owns opens in. `\"\"`/absent ⇒ the `/` route's\n * screen; no `/` route either ⇒ the link does not resolve. */\n fallbackScreenId?: string;\n}"
1061
+ "decl": "export interface SiteNavConfig {\n /** https PATH BOUNDARIES (`https://owner.wixsite.com/my-shop/`), the same form as the\n * site's WebView `allowedOrigins`: exact host, and the path is the boundary or under it. */\n origins: string[];\n /** Longest path-segment prefix wins, on the path RELATIVE to the matched origin.\n * `/` matches the home page only. */\n routes: SiteRoute[];\n /** The screen an on-site URL no route owns opens in. `\"\"`/absent ⇒ the `/` route's\n * screen; no `/` route either ⇒ the link does not resolve. */\n fallbackScreenId?: string;\n /** Where the shell's tabs and drawer come from. `\"runtime\"`: the device reads the\n * site's own menu as pages load and builds the NEXT launch's chrome from it\n * (`mergeSiteMenu` in `@himalaya/core/site-menu`). `\"contract\"`/absent: the served\n * `tabBar`/`drawer`/`sidebar` exactly as they are. OTA, like the rest of this block. */\n menuSource?: \"contract\" | \"runtime\";\n}"
1062
1062
  },
1063
1063
  {
1064
1064
  "name": "SiteRoute",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1316.0",
3
+ "version": "0.1317.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "c93d8a8f42002d89399db4200dcc4c2052fb748a839303eb6a3faa0f"
74
+ "falconPackageHash": "c373d9c55f28192fde803e5fb69be4c7287eba81744f007b47283ab9"
75
75
  }