@wix/himalaya-cli 0.849.0 → 0.850.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.
@@ -253,6 +253,34 @@ export declare function surfaceById(id: string): PreviewSurface | undefined;
253
253
  * claim a family allowlist and then be served live bindings.
254
254
  */
255
255
  export type SurfaceRenderMode = "frozen" | "interactive";
256
+ /**
257
+ * The widget family a FROZEN surface publishes under, for DISPLAY — or undefined for
258
+ * an interactive app shell.
259
+ *
260
+ * Both gate kinds that mean "frozen" carry a family: `family` because it IS the
261
+ * allowlist key, `projection` because the car still ships its tree under one
262
+ * (`car_list`). That makes this the right thing to name a surface by in a message, and
263
+ * the WRONG thing to branch on — a car and a widget are authored through different
264
+ * doors. Decisions use `isWidgetFamilySurface` below.
265
+ */
266
+ export declare function surfaceFrozenFamily(surface: PreviewSurface): string | undefined;
267
+ /**
268
+ * Is this surface an OS widget — the `defineWidget` population, as distinct from the
269
+ * car, which is frozen too?
270
+ *
271
+ * Named and shared rather than spelled `gate.kind === "family"` at each call site,
272
+ * because the two places that ask (`surfacesForApp`'s produced verdict, and
273
+ * `himi render --device`'s refusal) must give the same answer or #2499 comes back
274
+ * halfway: a surface one of them classes as a widget and the other does not is either
275
+ * advertised as supported while unrenderable, or refused with advice pointing at a
276
+ * door it was not authored through.
277
+ *
278
+ * The car is excluded deliberately, not incidentally. It is authored with
279
+ * `defineView({ surface: "car" })`, gated by a real `androidauto` key (so it never
280
+ * over-claimed), and its viewport exists for the preview — so `defineWidget` advice and
281
+ * a `/v1/widget/<id>` pointer would both be dead ends for it.
282
+ */
283
+ export declare function isWidgetFamilySurface(surface: PreviewSurface): boolean;
256
284
  export declare function surfaceRenderMode(surface: PreviewSurface): SurfaceRenderMode;
257
285
  /**
258
286
  * Ladder rungs that are not themselves surfaces — the shared fallbacks a view can
@@ -316,15 +344,48 @@ export declare function isTargeted(surface: PreviewSurface, platforms: Platforms
316
344
  export interface SurfaceAvailability {
317
345
  readonly surface: PreviewSurface;
318
346
  readonly targeted: boolean;
319
- /** When not targeted, the exact opt-in to add. Drives the picker's tooltip. */
347
+ /** When not targeted because of the BINARY envelope, the exact platforms.json
348
+ * opt-in to add. Drives the picker's tooltip. Absent for a widget family that
349
+ * is untargeted for lack of a widget — no platforms.json key opts that in. */
320
350
  readonly optIn?: string;
351
+ /**
352
+ * For a WIDGET FAMILY (`isWidgetFamilySurface`) — does this app actually PRODUCE a
353
+ * snapshot for it? Only present when the caller supplied the produced set;
354
+ * `undefined` means "not measured", which is not the same as `false` and must not be
355
+ * rendered as one.
356
+ *
357
+ * Widget families and NOTHING else, the car included — it is frozen too, but it is
358
+ * gated by a real `androidauto` key (so it never over-claimed) and is authored with
359
+ * `defineView({ surface: "car" })`, so a `defineWidget`-shaped verdict would be the
360
+ * wrong answer rather than a missing one. An app shell has nothing to produce at all.
361
+ */
362
+ readonly produced?: boolean;
363
+ /** When a widget family is untargeted because nothing produces it, what to author. */
364
+ readonly needs?: string;
321
365
  }
322
366
  /**
323
367
  * The full catalog with a targeted flag — NOT a filtered list. The picker shows
324
368
  * every surface and dims the untargeted ones, so authors discover what their app
325
369
  * could also be, instead of only seeing what it already is.
370
+ *
371
+ * `producedFamilies` — the widget families this app actually produces a snapshot for
372
+ * (`GET /v1/widgets`, or a scan of its built bundles). Supply it and a frozen surface
373
+ * reports the truth; omit it and the answer is the binary envelope alone, as before.
374
+ *
375
+ * Why it exists (#2499): the four native widget families carry `platformKey: null`
376
+ * (`web_card` and `watch_complication` are the exceptions — they ride `web`/`watchos`),
377
+ * so for those four `isTargeted` short-circuits to `true` and `himi surfaces` reported
378
+ * `widget:ios_home`, `widget:ios_lock`, `widget:android_home` and
379
+ * `widget:ios_live_activity` as targeted for EVERY app — declared widgets or not.
380
+ * A tester read that as support, found
381
+ * `/v1/widgets` empty, and summarized it as "everything advertises the capability
382
+ * except the capability". The envelope answer was not wrong so much as answering a
383
+ * different question: iOS and Android are implicit, so the binary CAN host a widget;
384
+ * whether this app HAS one is a fact about its bundles, and only the produced set
385
+ * carries it. `isTargeted` is deliberately left alone — `resolveSurfaceEnvelope` asks
386
+ * the binary question and must keep getting the binary answer.
326
387
  */
327
- export declare function surfacesForApp(platforms: PlatformsJson): readonly SurfaceAvailability[];
388
+ export declare function surfacesForApp(platforms: PlatformsJson, producedFamilies?: Iterable<string>): readonly SurfaceAvailability[];
328
389
  export interface SurfaceEnvelopeResolution {
329
390
  /** Surfaces this app actually runs, in catalog order. */
330
391
  readonly enabled: readonly string[];
@@ -9232,6 +9232,13 @@ var PREVIEW_SURFACES = [
9232
9232
  function surfaceById(id) {
9233
9233
  return PREVIEW_SURFACES.find((s) => s.id === id);
9234
9234
  }
9235
+ function surfaceFrozenFamily(surface) {
9236
+ const gate = surface.gate;
9237
+ return gate.kind === "family" || gate.kind === "projection" ? gate.family : void 0;
9238
+ }
9239
+ function isWidgetFamilySurface(surface) {
9240
+ return surface.gate.kind === "family";
9241
+ }
9235
9242
  function surfaceRenderMode(surface) {
9236
9243
  const { kind } = surface.gate;
9237
9244
  return kind === "family" || kind === "projection" ? "frozen" : "interactive";
@@ -9295,10 +9302,33 @@ function isTargeted(surface, platforms) {
9295
9302
  if (surface.platformKey === "macos") return platforms.macos !== false;
9296
9303
  return platforms[surface.platformKey] === true;
9297
9304
  }
9298
- function surfacesForApp(platforms) {
9305
+ function surfacesForApp(platforms, producedFamilies) {
9306
+ const produced = producedFamilies === void 0 ? void 0 : producedFamilies instanceof Set ? producedFamilies : new Set(producedFamilies);
9299
9307
  return PREVIEW_SURFACES.map((surface) => {
9300
- const targeted = isTargeted(surface, platforms);
9301
- return targeted ? { surface, targeted } : { surface, targeted, optIn: `"${surface.platformKey}": true` };
9308
+ const inEnvelope = isTargeted(surface, platforms);
9309
+ const family = isWidgetFamilySurface(surface) ? surfaceFrozenFamily(surface) : void 0;
9310
+ if (produced && family !== void 0) {
9311
+ const isProduced = produced.has(family);
9312
+ return inEnvelope && isProduced ? { surface, targeted: true, produced: true } : {
9313
+ surface,
9314
+ targeted: false,
9315
+ produced: isProduced,
9316
+ // The four native families carry no platform key, so there is nothing to
9317
+ // offer; `web_card` and `watch_complication` DO have one, and when that is
9318
+ // the half that is missing it is still the right advice. Hence the guard —
9319
+ // it is load-bearing for two families, not defensive padding.
9320
+ ...inEnvelope ? {} : surface.platformKey ? { optIn: `"${surface.platformKey}": true` } : {},
9321
+ ...isProduced ? {} : {
9322
+ // The rebuild half is not padding: the produced set is read off BUILT
9323
+ // bundles, so an author who has just written the registration and not
9324
+ // rebuilt lands here too, and "write defineWidget" alone would be advice
9325
+ // to write the code they are looking at.
9326
+ needs: `defineWidget({ family: "${family}", \u2026 }) in a Tier-5 bundle \u2014 and a rebuild, if you just added it`
9327
+ }
9328
+ };
9329
+ }
9330
+ if (inEnvelope) return { surface, targeted: true };
9331
+ return surface.platformKey ? { surface, targeted: false, optIn: `"${surface.platformKey}": true` } : { surface, targeted: false };
9302
9332
  });
9303
9333
  }
9304
9334
  function resolveSurfaceEnvelope(platforms, requested) {
@@ -12814,6 +12844,7 @@ export {
12814
12844
  isValidAppId,
12815
12845
  isValidStreamPolicy,
12816
12846
  isViewTarget,
12847
+ isWidgetFamilySurface,
12817
12848
  layoutDirection,
12818
12849
  parseNumeric,
12819
12850
  primarySubtag,
@@ -12834,6 +12865,7 @@ export {
12834
12865
  stripOriginParams,
12835
12866
  summarize,
12836
12867
  surfaceById,
12868
+ surfaceFrozenFamily,
12837
12869
  surfaceRenderMode,
12838
12870
  surfacesForApp,
12839
12871
  validate,
package/dist/cli.mjs CHANGED
@@ -1763,6 +1763,13 @@ var init_widget_vocabulary = __esm({
1763
1763
  function surfaceById(id) {
1764
1764
  return PREVIEW_SURFACES.find((s) => s.id === id);
1765
1765
  }
1766
+ function surfaceFrozenFamily(surface) {
1767
+ const gate = surface.gate;
1768
+ return gate.kind === "family" || gate.kind === "projection" ? gate.family : void 0;
1769
+ }
1770
+ function isWidgetFamilySurface(surface) {
1771
+ return surface.gate.kind === "family";
1772
+ }
1766
1773
  function surfaceRenderMode(surface) {
1767
1774
  const { kind } = surface.gate;
1768
1775
  return kind === "family" || kind === "projection" ? "frozen" : "interactive";
@@ -1787,10 +1794,33 @@ function isTargeted(surface, platforms) {
1787
1794
  if (surface.platformKey === "macos") return platforms.macos !== false;
1788
1795
  return platforms[surface.platformKey] === true;
1789
1796
  }
1790
- function surfacesForApp(platforms) {
1797
+ function surfacesForApp(platforms, producedFamilies) {
1798
+ const produced = producedFamilies === void 0 ? void 0 : producedFamilies instanceof Set ? producedFamilies : new Set(producedFamilies);
1791
1799
  return PREVIEW_SURFACES.map((surface) => {
1792
- const targeted = isTargeted(surface, platforms);
1793
- return targeted ? { surface, targeted } : { surface, targeted, optIn: `"${surface.platformKey}": true` };
1800
+ const inEnvelope = isTargeted(surface, platforms);
1801
+ const family = isWidgetFamilySurface(surface) ? surfaceFrozenFamily(surface) : void 0;
1802
+ if (produced && family !== void 0) {
1803
+ const isProduced = produced.has(family);
1804
+ return inEnvelope && isProduced ? { surface, targeted: true, produced: true } : {
1805
+ surface,
1806
+ targeted: false,
1807
+ produced: isProduced,
1808
+ // The four native families carry no platform key, so there is nothing to
1809
+ // offer; `web_card` and `watch_complication` DO have one, and when that is
1810
+ // the half that is missing it is still the right advice. Hence the guard —
1811
+ // it is load-bearing for two families, not defensive padding.
1812
+ ...inEnvelope ? {} : surface.platformKey ? { optIn: `"${surface.platformKey}": true` } : {},
1813
+ ...isProduced ? {} : {
1814
+ // The rebuild half is not padding: the produced set is read off BUILT
1815
+ // bundles, so an author who has just written the registration and not
1816
+ // rebuilt lands here too, and "write defineWidget" alone would be advice
1817
+ // to write the code they are looking at.
1818
+ needs: `defineWidget({ family: "${family}", \u2026 }) in a Tier-5 bundle \u2014 and a rebuild, if you just added it`
1819
+ }
1820
+ };
1821
+ }
1822
+ if (inEnvelope) return { surface, targeted: true };
1823
+ return surface.platformKey ? { surface, targeted: false, optIn: `"${surface.platformKey}": true` } : { surface, targeted: false };
1794
1824
  });
1795
1825
  }
1796
1826
  function resolveSurfaceEnvelope(platforms, requested) {
@@ -3239,6 +3269,13 @@ var init_build_lib = __esm({
3239
3269
  });
3240
3270
 
3241
3271
  // ../../core/dev-server/src/tier5-dir.ts
3272
+ var tier5_dir_exports = {};
3273
+ __export(tier5_dir_exports, {
3274
+ bundleOutDir: () => bundleOutDir,
3275
+ hasBuiltBundles: () => hasBuiltBundles,
3276
+ servedBundleName: () => servedBundleName,
3277
+ servedBundleNames: () => servedBundleNames
3278
+ });
3242
3279
  import { readdirSync as readdirSync7 } from "node:fs";
3243
3280
  import { resolve as resolve9 } from "node:path";
3244
3281
  function bundleOutDir(contentDir2) {
@@ -3251,6 +3288,25 @@ function hasBuiltBundles(contentDir2) {
3251
3288
  return false;
3252
3289
  }
3253
3290
  }
3291
+ function servedBundleName(fileName) {
3292
+ if (fileName.endsWith(".bundle.js")) return fileName.slice(0, -".bundle.js".length);
3293
+ if (fileName.endsWith(".js")) return fileName.slice(0, -".js".length);
3294
+ return void 0;
3295
+ }
3296
+ function servedBundleNames(dir) {
3297
+ const names = /* @__PURE__ */ new Set();
3298
+ let entries;
3299
+ try {
3300
+ entries = readdirSync7(dir);
3301
+ } catch {
3302
+ return names;
3303
+ }
3304
+ for (const entry of entries) {
3305
+ const name = servedBundleName(entry);
3306
+ if (name !== void 0) names.add(name);
3307
+ }
3308
+ return names;
3309
+ }
3254
3310
  var init_tier5_dir = __esm({
3255
3311
  "../../core/dev-server/src/tier5-dir.ts"() {
3256
3312
  }
@@ -4689,7 +4745,6 @@ function resolveTranslation(value, catalog, sourceCatalog) {
4689
4745
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
4690
4746
  var init_bindings = __esm({
4691
4747
  "../../core/ts/src/bindings.ts"() {
4692
- "use strict";
4693
4748
  BINDING_SIGILS = ["service", "flag", "item", "t"];
4694
4749
  BINDING_SIGIL_RE = new RegExp(
4695
4750
  `^\\$(?:${BINDING_SIGILS.filter((s) => s !== "t").join("|")})\\b|^\\$t:`
@@ -26427,7 +26482,6 @@ function shouldRevealError(args) {
26427
26482
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
26428
26483
  var init_validation = __esm({
26429
26484
  "../../core/ts/src/validation.ts"() {
26430
- "use strict";
26431
26485
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
26432
26486
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
26433
26487
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -29823,7 +29877,6 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
29823
29877
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
29824
29878
  var init_worker_subtree = __esm({
29825
29879
  "../../core/ts/src/worker-subtree.ts"() {
29826
- "use strict";
29827
29880
  init_worker_subtree_safe_types_generated();
29828
29881
  WORKER_SUBTREE_CEILINGS = Object.freeze({
29829
29882
  maxEncodedBytes: 65536,
@@ -36651,6 +36704,16 @@ var init_kits = __esm({
36651
36704
  });
36652
36705
 
36653
36706
  // ../../core/dev-server/src/widgets.ts
36707
+ var widgets_exports = {};
36708
+ __export(widgets_exports, {
36709
+ buildViewSnapshot: () => buildViewSnapshot,
36710
+ buildWidgetSnapshot: () => buildWidgetSnapshot,
36711
+ describeThrown: () => describeThrown,
36712
+ extractWidgets: () => extractWidgets,
36713
+ freezeOverrideSnapshot: () => freezeOverrideSnapshot,
36714
+ frozenViewManifestEntries: () => frozenViewManifestEntries,
36715
+ viewWidgetId: () => viewWidgetId
36716
+ });
36654
36717
  import { createHash as createHash14 } from "node:crypto";
36655
36718
  import { inspect } from "node:util";
36656
36719
  import { runInNewContext } from "node:vm";
@@ -38698,7 +38761,7 @@ async function handleDev(req, res, state) {
38698
38761
  if (req.method === "POST") return await handleWidgetPushPreview(req, res, state);
38699
38762
  const widgetId = new URL(url, "http://localhost").searchParams.get("widgetId");
38700
38763
  if (req.method === "DELETE") return await handleWidgetClearPreview(res, state, widgetId);
38701
- return await handleWidgetPreview(res, widgetId);
38764
+ return await handleWidgetPreview(res, widgetId, state.tier5Dir);
38702
38765
  }
38703
38766
  if (url === "/__dev/seed") {
38704
38767
  const seed2 = state.app.module.seed;
@@ -39319,9 +39382,8 @@ async function listBundles(dir) {
39319
39382
  continue;
39320
39383
  }
39321
39384
  for (const entry of entries) {
39322
- if (entry.endsWith(".bundle.js") || entry.endsWith(".js")) {
39323
- names.add(entry.replace(/\.bundle\.js$|\.js$/, ""));
39324
- }
39385
+ const name = servedBundleName(entry);
39386
+ if (name !== void 0) names.add(name);
39325
39387
  }
39326
39388
  }
39327
39389
  const result = [];
@@ -39348,14 +39410,14 @@ async function handleBundleGet(res, name, dir) {
39348
39410
  res.statusCode = 200;
39349
39411
  res.end(loaded.bytes);
39350
39412
  }
39351
- async function scanWidgets() {
39413
+ async function scanWidgets(dir) {
39352
39414
  const out = [];
39353
39415
  const seen = /* @__PURE__ */ new Set();
39354
39416
  const emitted = /* @__PURE__ */ new Set();
39355
- for (const { name } of await listBundles()) {
39417
+ for (const { name } of await listBundles(dir)) {
39356
39418
  if (seen.has(name)) continue;
39357
39419
  seen.add(name);
39358
- const loaded = await readBundleFile(name);
39420
+ const loaded = await readBundleFile(name, dir);
39359
39421
  if (!loaded) continue;
39360
39422
  try {
39361
39423
  const js = loaded.bytes.toString("utf8");
@@ -39372,15 +39434,15 @@ async function scanWidgets() {
39372
39434
  }
39373
39435
  return out;
39374
39436
  }
39375
- async function handleWidgetList(res) {
39376
- sendJson(res, 200, { widgets: await scanWidgets() });
39437
+ async function handleWidgetList(res, dir) {
39438
+ sendJson(res, 200, { widgets: await scanWidgets(dir) });
39377
39439
  }
39378
- async function buildSnapshotForId(widgetId) {
39440
+ async function buildSnapshotForId(widgetId, dir) {
39379
39441
  const override = widgetOverrides.get(widgetId);
39380
39442
  if (override) return { ok: true, snapshot: override };
39381
- const entry = (await scanWidgets()).find((w) => w.widgetId === widgetId);
39443
+ const entry = (await scanWidgets(dir)).find((w) => w.widgetId === widgetId);
39382
39444
  if (!entry) return { ok: false, status: 404, error: "widget_not_found" };
39383
- const loaded = await readBundleFile(entry.bundle);
39445
+ const loaded = await readBundleFile(entry.bundle, dir);
39384
39446
  if (!loaded) return { ok: false, status: 404, error: "widget_not_found" };
39385
39447
  try {
39386
39448
  const js = loaded.bytes.toString("utf8");
@@ -39394,8 +39456,8 @@ async function buildSnapshotForId(widgetId) {
39394
39456
  return { ok: false, status: 422, error: "widget_invalid", message: err.message };
39395
39457
  }
39396
39458
  }
39397
- async function handleWidgetGet(req, res, widgetId) {
39398
- const built = await buildSnapshotForId(widgetId);
39459
+ async function handleWidgetGet(req, res, widgetId, dir) {
39460
+ const built = await buildSnapshotForId(widgetId, dir);
39399
39461
  if (!built.ok) return sendJson(res, built.status, { error: built.error, widgetId, message: built.message });
39400
39462
  const etag = `"${built.snapshot.contentEtag}"`;
39401
39463
  const ifNoneMatch = req.headers["if-none-match"];
@@ -39408,9 +39470,9 @@ async function handleWidgetGet(req, res, widgetId) {
39408
39470
  res.setHeader("cache-control", `max-age=${built.snapshot.refresh.ttlSeconds}`);
39409
39471
  sendJson(res, 200, built.snapshot);
39410
39472
  }
39411
- async function handleWidgetPreview(res, widgetId) {
39473
+ async function handleWidgetPreview(res, widgetId, dir) {
39412
39474
  if (!widgetId) return sendJson(res, 400, { error: "missing_widgetId" });
39413
- const built = await buildSnapshotForId(widgetId);
39475
+ const built = await buildSnapshotForId(widgetId, dir);
39414
39476
  if (!built.ok) return sendJson(res, built.status, { error: built.error, widgetId, message: built.message });
39415
39477
  sendJson(res, 200, built.snapshot);
39416
39478
  }
@@ -40029,10 +40091,10 @@ function createDevServer(opts = {}) {
40029
40091
  if (bundleMatch && (req.method === "GET" || req.method === "HEAD"))
40030
40092
  return await handleBundleGet(res, bundleMatch[1], state.tier5Dir);
40031
40093
  if (url.split("?")[0] === "/v1/widgets" && (req.method === "GET" || req.method === "HEAD"))
40032
- return await handleWidgetList(res);
40094
+ return await handleWidgetList(res, state.tier5Dir);
40033
40095
  const widgetMatch = /^\/v1\/widget\/([A-Za-z0-9_.\-]+)$/.exec(url.split("?")[0]);
40034
40096
  if (widgetMatch && (req.method === "GET" || req.method === "HEAD"))
40035
- return await handleWidgetGet(req, res, widgetMatch[1]);
40097
+ return await handleWidgetGet(req, res, widgetMatch[1], state.tier5Dir);
40036
40098
  const ahManifest = /^\/v1\/action-handlers\/([A-Za-z0-9_.\-]+)$/.exec(url.split("?")[0]);
40037
40099
  if (ahManifest && (req.method === "GET" || req.method === "HEAD"))
40038
40100
  return handleActionHandlersManifest(res, ahManifest[1]);
@@ -40046,7 +40108,10 @@ function createDevServer(opts = {}) {
40046
40108
  homeScreenId: state.app?.meta.homeScreenId,
40047
40109
  screens: state.app?.meta.screenIds ?? [],
40048
40110
  templatedPatterns: state.app?.meta.templatedPatterns ?? [],
40049
- tier5Bundles: await listBundles(),
40111
+ // Same dir, same reason as the routes above: an off-repo app's own
40112
+ // bundles must show up in its introspection body, not just the
40113
+ // repo-global ones it didn't build.
40114
+ tier5Bundles: await listBundles(state.tier5Dir),
40050
40115
  toggles: state.toggles,
40051
40116
  admin: "/web/future/app.html"
40052
40117
  });
@@ -40487,6 +40552,7 @@ __export(build_exports2, {
40487
40552
  SOURCEMAP_ON_DEMAND_PATH: () => SOURCEMAP_ON_DEMAND_PATH,
40488
40553
  bakeCatalogs: () => bakeCatalogs,
40489
40554
  bakeNeedsLiveOrigin: () => bakeNeedsLiveOrigin,
40555
+ bakeWidgets: () => bakeWidgets,
40490
40556
  buildRelease: () => buildRelease,
40491
40557
  buildReleaseFromContent: () => buildReleaseFromContent,
40492
40558
  bundleNameFromUrl: () => bundleNameFromUrl,
@@ -40585,6 +40651,77 @@ function sha2564(buf) {
40585
40651
  function sri2(buf) {
40586
40652
  return "sha256-" + createHash18("sha256").update(buf).digest("base64");
40587
40653
  }
40654
+ async function widgetFailureDetail(res) {
40655
+ try {
40656
+ const body = JSON.parse(await res.text());
40657
+ const detail = typeof body.message === "string" && body.message ? body.message : typeof body.error === "string" && body.error ? body.error : "";
40658
+ return detail ? `: ${detail}` : "";
40659
+ } catch {
40660
+ return "";
40661
+ }
40662
+ }
40663
+ function ownBundleNames(dir) {
40664
+ if (!dir) return void 0;
40665
+ return servedBundleNames(dir);
40666
+ }
40667
+ async function bakeWidgets(fetchPath, putWidget, ownBundles) {
40668
+ const widgets = {};
40669
+ const failures = [];
40670
+ const isOurs = (bundle) => ownBundles !== void 0 && bundle !== void 0 && ownBundles.has(bundle);
40671
+ const listRes = await fetchPath("/v1/widgets");
40672
+ if (!listRes.ok) {
40673
+ failures.push({
40674
+ status: listRes.status,
40675
+ message: `the widget manifest could not be read (GET /v1/widgets \u2192 ${listRes.status}) \u2014 this release carries no widget snapshots, whether or not the app declares any`
40676
+ });
40677
+ return { widgets, failures };
40678
+ }
40679
+ let list;
40680
+ try {
40681
+ const parsed = JSON.parse(await listRes.text()).widgets ?? [];
40682
+ if (!Array.isArray(parsed)) throw new Error("widgets is not an array");
40683
+ list = parsed;
40684
+ } catch {
40685
+ failures.push({
40686
+ status: listRes.status,
40687
+ message: "the widget manifest came back malformed \u2014 this release carries no widget snapshots"
40688
+ });
40689
+ return { widgets, failures };
40690
+ }
40691
+ for (const { widgetId, bundle } of list) {
40692
+ const res = await fetchPath(`/v1/widget/${encodeURIComponent(widgetId)}`);
40693
+ if (!res.ok) {
40694
+ if (isOurs(bundle)) {
40695
+ failures.push({
40696
+ widgetId,
40697
+ status: res.status,
40698
+ message: `widget "${widgetId}" is declared but did not produce a snapshot (GET /v1/widget/${widgetId} \u2192 ${res.status}${await widgetFailureDetail(res)}) \u2014 the release does not carry it`
40699
+ });
40700
+ }
40701
+ continue;
40702
+ }
40703
+ let snap;
40704
+ try {
40705
+ snap = JSON.parse(await res.text());
40706
+ } catch {
40707
+ if (isOurs(bundle)) {
40708
+ failures.push({
40709
+ widgetId,
40710
+ status: res.status,
40711
+ message: `widget "${widgetId}" answered ${res.status} with a body that is not a snapshot \u2014 the release does not carry it`
40712
+ });
40713
+ }
40714
+ continue;
40715
+ }
40716
+ const json5 = JSON.stringify(snap);
40717
+ await putWidget(widgetId, json5);
40718
+ widgets[widgetId] = {
40719
+ contentEtag: snap.contentEtag ?? sha2564(json5),
40720
+ ttlSeconds: snap.refresh?.ttlSeconds ?? 0
40721
+ };
40722
+ }
40723
+ return { widgets, failures };
40724
+ }
40588
40725
  async function snapshotTokens(app, base, fetchTokens, put) {
40589
40726
  const res = await fetchTokens();
40590
40727
  if (!res.ok) {
@@ -40781,6 +40918,7 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
40781
40918
  const screenIds = Object.keys(preloaded.module.screens.exact);
40782
40919
  const failedToBuild = [];
40783
40920
  const missingBundles = [];
40921
+ const widgetBakeFailures = [];
40784
40922
  const lint = [];
40785
40923
  const renders = [];
40786
40924
  const baked = [];
@@ -41314,21 +41452,16 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
41314
41452
  }
41315
41453
  }
41316
41454
  if (store.putWidget) {
41317
- const wlRes = await devFetch(`${base}/v1/widgets`, { headers: { "X-Himalaya-App": app } });
41318
- if (wlRes.ok) {
41319
- const { widgets: list } = await wlRes.json();
41320
- for (const { widgetId } of list ?? []) {
41321
- const wRes = await devFetch(`${base}/v1/widget/${encodeURIComponent(widgetId)}`, { headers: { "X-Himalaya-App": app } });
41322
- if (!wRes.ok) continue;
41323
- const snap = await wRes.json();
41324
- const json5 = JSON.stringify(snap);
41325
- await store.putWidget(app, widgetId, json5);
41326
- widgets[widgetId] = {
41327
- contentEtag: snap.contentEtag ?? sha2564(json5),
41328
- ttlSeconds: snap.refresh?.ttlSeconds ?? 0
41329
- };
41330
- }
41331
- }
41455
+ const bakedWidgets = await bakeWidgets(
41456
+ (path) => devFetch(`${base}${path}`, { headers: { "X-Himalaya-App": app } }),
41457
+ (widgetId, json5) => store.putWidget(app, widgetId, json5),
41458
+ // `opts.tier5Dir`, NOT the `tier5Dir` local — that one falls back to the
41459
+ // repo-global dir, where this package's widgets cannot be told from anyone
41460
+ // else's and the filter would claim every app's rows as ours.
41461
+ ownBundleNames(opts.tier5Dir)
41462
+ );
41463
+ Object.assign(widgets, bakedWidgets.widgets);
41464
+ widgetBakeFailures.push(...bakedWidgets.failures);
41332
41465
  }
41333
41466
  } finally {
41334
41467
  await new Promise((r) => dev.close(() => r()));
@@ -41417,6 +41550,7 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
41417
41550
  return named ? { ...v, message: `${v.message} \u2014 bundle "${named.name}"` } : v;
41418
41551
  }),
41419
41552
  ...tokensSnapshotFailure ? { tokensSnapshotFailure } : {},
41553
+ ...widgetBakeFailures.length ? { widgetBakeFailures } : {},
41420
41554
  ...collect?.lint ? { lint, workers } : {},
41421
41555
  ...collect?.render ? { renders } : {},
41422
41556
  ...collect?.descriptors ? { descriptors: baked, linkableScreenIds: [...linkable] } : {},
@@ -59318,6 +59452,17 @@ function summarizeLint(app, bres, outcome, strict, extraIssues = []) {
59318
59452
  message: tokensFailure.message
59319
59453
  });
59320
59454
  }
59455
+ for (const failure of outcome.widgetBakeFailures ?? []) {
59456
+ issues.push({
59457
+ screen: null,
59458
+ pass: "widgets",
59459
+ severity: strict ? "error" : "warning",
59460
+ ruleCode: "widget-not-produced",
59461
+ ...failure.widgetId ? { widgetId: failure.widgetId } : {},
59462
+ status: failure.status,
59463
+ message: failure.message
59464
+ });
59465
+ }
59321
59466
  const errorIssues = issues.filter((i) => i.severity === "error");
59322
59467
  const workers = (outcome.workers ?? []).filter((w) => w.threw || w.console.length);
59323
59468
  const workerThrows = workers.filter((w) => w.threw);
@@ -60408,6 +60553,12 @@ function wixPlayerTestflightTarget(requested) {
60408
60553
  return target === "bo" || target === "prod" ? target : void 0;
60409
60554
  }
60410
60555
  var SURFACE_IDS = `${PREVIEW_SURFACES.filter((s) => !s.id.startsWith("widget:")).map((s) => s.id).join("|")}|widget:<family>`;
60556
+ var RENDER_DEVICE_SURFACE_IDS = PREVIEW_SURFACES.filter((s) => s.gate.kind !== "family").map((s) => s.id).join("|");
60557
+ var PRODUCED_HINTS = {
60558
+ "not-measured": "widget production was not measured \u2014 `--measure-widgets` evaluates this package's built bundles (it RUNS their code), or ask a running app: GET /v1/widgets. `himi validate --strict` fails a package whose declared widget does not produce.",
60559
+ unbuilt: "this package has no dist/bundles, so there is nothing to measure \u2014 run `himi build` first. A package with no Tier-5 bundle can declare no widget at all.",
60560
+ "producer-unavailable": "the widget producer is not resolvable in this install, so production cannot be measured here at all \u2014 building this package will not change that. Ask a running app instead: GET /v1/widgets."
60561
+ };
60411
60562
  var DEVELOPER_PORTAL_URL = `${ADMIN_ORIGIN_BO}/#/developer`;
60412
60563
  var MAX_APP_SOURCE_BYTES_HELP = "4 MiB (4,194,304 bytes)";
60413
60564
  var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
@@ -60493,8 +60644,9 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
60493
60644
  --input '{"id":"\u2026"}' supplies navigation input before the worker boots;
60494
60645
  --load-budget <ms> raises/lowers the per-service load() budget (--live defaults to 30000);
60495
60646
  --width 390 / --device iphone renders the PNG at phone width (default: desktop);
60496
- --device also takes any surface id (${SURFACE_IDS})
60647
+ --device also takes an app-shell surface id (${RENDER_DEVICE_SURFACE_IDS})
60497
60648
  at its catalog viewport width (himi surfaces lists them);
60649
+ a widget family is NOT a device \u2014 fetch /v1/widget/<widgetId>;
60498
60650
  --rtl sets ambient direction (dir=rtl + $env:isRTL)
60499
60651
  himi icons [--json] the valid Icon.name vocabulary (offline)
60500
60652
  himi fonts [list] [--licenses] the curated font vocabulary a typography.*.family may
@@ -60633,11 +60785,14 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
60633
60785
  print web + himi:// + agent-preview JSON URLs
60634
60786
  --surface ${SURFACE_IDS}
60635
60787
  frames the web preview as that surface (?surface=)
60636
- himi surfaces [--content <dir>] [--app <app> --release <id>]
60788
+ himi surfaces [--content <dir>] [--app <app> --release <id>] [--measure-widgets]
60637
60789
  the surface catalog vs the LOCAL package's envelope
60638
60790
  (platforms.json): targeted surfaces, the opt-in for the
60639
60791
  rest, and (with --app --release) per-surface preview
60640
60792
  links. For a pushed release's envelope: himi context
60793
+ --measure-widgets additionally reports which widget
60794
+ families this package PRODUCES \u2014 it evaluates the
60795
+ built bundles, i.e. RUNS their code; off by default
60641
60796
  himi context --app <app> --release <id> [--screen <id>] [--include telemetry]
60642
60797
  fetch the agent preview envelope (GET /v1/preview/a/\u2026)
60643
60798
  himi app-docs list|get <spec|mobile-ux> --app <app> [--release <id>]
@@ -60862,7 +61017,7 @@ var COMMANDS = [
60862
61017
  { name: "share", summary: "publish or revoke a content-addressed `.himi` share link. The package must be a validated archive for an already-published release; the resulting https URL is safe to QR, copy, or open in Himi Player.", required: "--file <package.himi> [--expires-at <ISO>] | revoke --app <app> <shareId>" },
60863
61018
  { name: "run", summary: "serve a .himi package on localhost and open it in a browser \u2014 no serve plane, no dev-server, no setup. Given no argument it builds the current content package first, so `himi run` is the inner loop: edit, run, look at it. The SPA and /v1 are served from ONE origin because Tier-5 bundle fetches on web are same-origin-only. --chrome preview frames the app in a device chassis; --chrome app serves it as a plain web app. Blocks until Ctrl-C, like any dev server. --browser runs the whole plane INSIDE the tab (service worker + in-memory store) so no app code executes in this process or on any pod.", required: "[<package.himi|dir|url>] [--browser] [--content <dir>] [--port <n>] [--chrome preview|app] [--screen <id>] [--surface <id>] [--no-open] [--spa <url>]" },
60864
61019
  { name: "preview", summary: "print the shareable web URL + himi:// deep link + agent /v1/preview JSON URLs for an app+release; --surface frames the web preview as any catalog surface (?surface=)", required: "--app --release [--screen] [--surface]" },
60865
- { name: "surfaces", summary: "the surface catalog vs this app's envelope (platforms.json): targeted surfaces, the opt-in key for the rest, and per-surface preview links with --app --release", required: "[--content <dir>] [--app --release]" },
61020
+ { name: "surfaces", summary: "the surface catalog vs this app's envelope (platforms.json): targeted surfaces, the opt-in key for the rest, per-surface preview links with --app --release, and which widget families the package produces with --measure-widgets (which runs its built bundles)", required: "[--content <dir>] [--app --release] [--measure-widgets]" },
60866
61021
  { name: "context", summary: "fetch the agent preview envelope (GET /v1/preview/a/<app>) \u2014 ok/issues/screens/links; exit 1 if ok:false", required: "--app --release [--screen] [--include telemetry]" },
60867
61022
  { name: "app-docs", summary: "retrieve the public-safe SPEC.md and MOBILE-UX.md carried by an immutable release; `list` is JSON metadata and `get` prints one Markdown document", required: "list|get <spec|mobile-ux> --app <app> [--release <id>]" },
60868
61023
  { name: "telemetry", summary: "head-loop telemetry read-back (GET /v1/telemetry/events + logs): screen_view, action_*, nav/error events, worker_console logs. Default includes recent events + logs; --summary/--crashes add aggregate/crash-stack reads; --follow emits bounded JSONL updates.", required: "--app --release [--screen] [--type] [--since] [--limit] [--follow] [--summary] [--crashes]" },
@@ -61699,6 +61854,60 @@ async function loadAuthoring() {
61699
61854
  throw new AuthoringUnavailable();
61700
61855
  }
61701
61856
  }
61857
+ async function producedWidgetFamilies(dir) {
61858
+ let widgetsModule;
61859
+ let tier5Dir;
61860
+ try {
61861
+ const [w, t] = await Promise.all([
61862
+ Promise.resolve().then(() => (init_widgets(), widgets_exports)),
61863
+ Promise.resolve().then(() => (init_tier5_dir(), tier5_dir_exports))
61864
+ ]);
61865
+ widgetsModule = w;
61866
+ tier5Dir = t;
61867
+ } catch {
61868
+ return { families: void 0, source: "producer-unavailable" };
61869
+ }
61870
+ const outDir = tier5Dir.bundleOutDir(dir);
61871
+ try {
61872
+ readdirSync23(outDir);
61873
+ } catch {
61874
+ return { families: void 0, source: "unbuilt" };
61875
+ }
61876
+ const names = [...tier5Dir.servedBundleNames(outDir)];
61877
+ const families = /* @__PURE__ */ new Set();
61878
+ const emitted = /* @__PURE__ */ new Set();
61879
+ for (const name of names) {
61880
+ let js;
61881
+ for (const candidate of [`${name}.bundle.js`, `${name}.js`, name]) {
61882
+ try {
61883
+ js = readFileSync63(join49(outDir, candidate), "utf8");
61884
+ break;
61885
+ } catch {
61886
+ }
61887
+ }
61888
+ if (js === void 0) continue;
61889
+ let extracted;
61890
+ try {
61891
+ extracted = widgetsModule.extractWidgets(js);
61892
+ } catch {
61893
+ continue;
61894
+ }
61895
+ for (const [widgetId, def] of Object.entries(extracted.widgets)) {
61896
+ try {
61897
+ widgetsModule.buildWidgetSnapshot(js, widgetId);
61898
+ families.add(def.family);
61899
+ } catch {
61900
+ }
61901
+ }
61902
+ for (const entry of widgetsModule.frozenViewManifestEntries(extracted.views, emitted)) {
61903
+ try {
61904
+ if (widgetsModule.buildViewSnapshot(js, entry.view.viewId, entry.view.surfaceId)) families.add(entry.family);
61905
+ } catch {
61906
+ }
61907
+ }
61908
+ }
61909
+ return { families, source: "dist/bundles" };
61910
+ }
61702
61911
  async function run(argv, io = defaultIO) {
61703
61912
  const { _: pos, flags } = parseFlags(argv);
61704
61913
  const command = pos[0];
@@ -62729,7 +62938,21 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
62729
62938
  if (deviceName) {
62730
62939
  renderWidth = DEVICE_WIDTHS[deviceName.toLowerCase()];
62731
62940
  if (!renderWidth) {
62732
- io.error(JSON.stringify({ error: `unknown --device "${deviceName}"`, devices: Object.keys(DEVICE_WIDTHS) }));
62941
+ const devices = Object.keys(DEVICE_WIDTHS).filter((d) => surfaceById(d)?.gate.kind !== "family");
62942
+ io.error(JSON.stringify({ error: `unknown --device "${deviceName}"`, devices }));
62943
+ return 2;
62944
+ }
62945
+ const surface = surfaceById(deviceName.toLowerCase());
62946
+ if (surface && isWidgetFamilySurface(surface)) {
62947
+ io.error(JSON.stringify({
62948
+ error: `--device "${deviceName}" is a widget family ("${surfaceFrozenFamily(surface)}"), not a viewport. himi render draws a screen; a widget is produced from defineWidget and addressed by widgetId, so rendering the screen at ${renderWidth}px would look like a widget and be none.`,
62949
+ surface: surface.id,
62950
+ renderMode: surfaceRenderMode(surface),
62951
+ instead: [
62952
+ "GET /v1/widgets \u2014 the widgets this app actually produces",
62953
+ "GET /v1/widget/<widgetId> \u2014 the frozen snapshot the OS renders"
62954
+ ]
62955
+ }));
62733
62956
  return 2;
62734
62957
  }
62735
62958
  } else if (typeof flags.width === "string" && flags.width) {
@@ -63918,6 +64141,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
63918
64141
  const release = str5(flags.release);
63919
64142
  const base = serveBase();
63920
64143
  const withLinks = !!(app && release && base);
64144
+ const produced = flags["measure-widgets"] === true ? await producedWidgetFamilies(dir) : { families: void 0, source: "not-measured" };
63921
64145
  print({
63922
64146
  ok: true,
63923
64147
  // The verb describes the LOCAL content package (its platforms.json) — for
@@ -63925,12 +64149,39 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
63925
64149
  // preview envelope reports `surfaces` from the release manifest).
63926
64150
  envelopeSource: "local",
63927
64151
  envelope: platforms,
63928
- enabled: resolveSurfaceEnvelope(platforms).enabled,
63929
- surfaces: surfacesForApp(platforms).map(({ surface, targeted, optIn }) => ({
64152
+ // Narrowed by the produced set when there IS one, and not merely reported
64153
+ // beside it. `enabled` is the short field an agent reads first, so leaving it
64154
+ // as the raw envelope would have kept listing `widget:ios_home` two lines above
64155
+ // a row saying `produced: false, needs: defineWidget(…)` — #2499's exact
64156
+ // over-claim, surviving inside the output added to remove it. `producedSource`
64157
+ // right below says how (or whether) the narrowing was measured.
64158
+ enabled: (() => {
64159
+ const envelope = resolveSurfaceEnvelope(platforms).enabled;
64160
+ if (!produced.families) return envelope;
64161
+ return envelope.filter((id) => {
64162
+ const surface = surfaceById(id);
64163
+ return !surface || !isWidgetFamilySurface(surface) || produced.families.has(surfaceFrozenFamily(surface));
64164
+ });
64165
+ })(),
64166
+ // Says which question the `produced` column answered, and — when it could not
64167
+ // be answered — WHICH way it failed. The three non-answers call for different
64168
+ // things and must not collapse into one: "not-measured" wants `--measure-widgets`
64169
+ // (and consents to running the package's code), "unbuilt" wants `himi build`,
64170
+ // and "producer-unavailable" wants neither — nothing about this package will
64171
+ // change that one.
64172
+ producedSource: produced.source,
64173
+ // …and the hint has to branch on WHICH of them it was, or it hands back advice the
64174
+ // caller has already taken: telling someone who just passed `--measure-widgets`
64175
+ // to pass `--measure-widgets` is exactly the "everything advertises the capability"
64176
+ // shape this issue is about, in miniature.
64177
+ ...produced.families ? {} : { producedHint: PRODUCED_HINTS[produced.source] },
64178
+ surfaces: surfacesForApp(platforms, produced.families).map(({ surface, targeted, optIn, produced: isProduced, needs }) => ({
63930
64179
  id: surface.id,
63931
64180
  label: surface.label,
63932
64181
  targeted,
63933
64182
  viewport: surface.viewport,
64183
+ ...isProduced === void 0 ? {} : { produced: isProduced },
64184
+ ...needs ? { needs } : {},
63934
64185
  ...optIn ? { optIn: `platforms.json: { ${optIn} }` } : {},
63935
64186
  ...withLinks && targeted ? { previewWeb: previewWebUrl(base, app, release, void 0, surface.id) } : {}
63936
64187
  })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.849.0",
3
+ "version": "0.850.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": [
@@ -63,5 +63,5 @@
63
63
  "artifactId": "himalaya-cli"
64
64
  }
65
65
  },
66
- "falconPackageHash": "464c186de522efebf80157a9bca4d7349bdbf3331ac8adfaf726f4d4"
66
+ "falconPackageHash": "17dae723ff059af1c4e2d1d27c07a030f3551c1ab3228d66cc5b6795"
67
67
  }