@amplifyup/sdk 0.1.67 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.mjs CHANGED
@@ -434,6 +434,13 @@ function isFieldEnvelope(value) {
434
434
  const rec = value;
435
435
  return typeof rec.name === "string" && "value" in rec && !("url" in rec);
436
436
  }
437
+ var OBJECT_FIELD_KINDS = /* @__PURE__ */ new Set(["object", "array", "list"]);
438
+ function isLeafFieldKind(kind) {
439
+ if (typeof kind !== "string") return false;
440
+ const normalized = kind.trim().toLowerCase();
441
+ if (!normalized) return false;
442
+ return !OBJECT_FIELD_KINDS.has(normalized);
443
+ }
437
444
  function isPageResourceSentinel(value) {
438
445
  return !!value && typeof value === "object" && !Array.isArray(value) && typeof value.__amplifyupPageResource === "string";
439
446
  }
@@ -573,6 +580,10 @@ function toFieldEnvelopes(source, prefix = "", scope) {
573
580
  out[key] = { value: envelopeListRows(raw, owner), name: path };
574
581
  continue;
575
582
  }
583
+ if (ctx.leaf?.(path)) {
584
+ out[key] = withLeaf({ value: emptyValue(raw), name: path }, ctx);
585
+ continue;
586
+ }
576
587
  if (raw && typeof raw === "object" && !Array.isArray(raw) && !isPortableImageShape(raw) && !raw.$$typeof) {
577
588
  out[key] = toFieldEnvelopes(raw, path, ctx);
578
589
  continue;
@@ -601,15 +612,33 @@ function envelopeListRow(item, owner) {
601
612
  }
602
613
  return row;
603
614
  }
615
+ var warnedMissingFieldKind = /* @__PURE__ */ new Set();
616
+ function warnMissingFieldKind(fieldName) {
617
+ if (process.env.NODE_ENV === "production") return;
618
+ if (warnedMissingFieldKind.has(fieldName)) return;
619
+ warnedMissingFieldKind.add(fieldName);
620
+ console.warn(
621
+ `[AmplifyUp SDK] Field "${fieldName}" has no schema kind; using value-shape detection. Republish the component definition so its fields carry a type.`
622
+ );
623
+ }
604
624
  function attachFieldEnvelopes(props, sources) {
605
625
  const raw = props && typeof props === "object" ? { ...props } : {};
606
626
  if (isBrandedFields(raw.fields)) return raw;
607
627
  const { fields: existing, ...content } = raw;
608
628
  const source = existing && typeof existing === "object" && !isFieldEnvelope(existing) ? existing : content;
629
+ const { reads } = normalizeEnvelopeSources(sources);
630
+ if (reads) {
631
+ for (const [fieldName, read] of Object.entries(reads)) {
632
+ if (!read?.kind) warnMissingFieldKind(fieldName);
633
+ }
634
+ }
609
635
  return {
610
636
  ...raw,
611
637
  fields: toFieldEnvelopes(source, "", {
612
- owners: (fieldName) => listOwnerForField(fieldName, source, sources)
638
+ owners: (fieldName) => listOwnerForField(fieldName, source, sources),
639
+ // Top-level field paths equal field names; nested paths have no schema
640
+ // entry and fall back to shape-based handling.
641
+ leaf: (path) => isLeafFieldKind(reads?.[path]?.kind)
613
642
  })
614
643
  };
615
644
  }
@@ -884,6 +913,8 @@ function hydrateFieldsBag(bag, fields) {
884
913
  if (val && typeof val === "object" && !Array.isArray(val) && typeof val.name === "string" && "value" in val && !("url" in val)) {
885
914
  const env = val;
886
915
  out[key] = {
916
+ // Keep ref / readOnly / source — dropping them severs the write target.
917
+ ...val,
887
918
  name: env.name,
888
919
  value: isPageResourceSentinel2(env.value) ? hydratePageResourceValue(env.value, fields) : env.value
889
920
  };
@@ -965,6 +996,51 @@ function sdkDebugOnce(key, ...args) {
965
996
  console.log(...args);
966
997
  }
967
998
 
999
+ // src/content/layoutProviderCache.ts
1000
+ var SETTLED_TTL_MS = 5e3;
1001
+ var inflight = /* @__PURE__ */ new Map();
1002
+ var settled = /* @__PURE__ */ new Map();
1003
+ function layoutProviderCacheKey(opts) {
1004
+ const route = (opts.route || "/").split("?")[0] || "/";
1005
+ return [
1006
+ route,
1007
+ opts.trackingId || "",
1008
+ opts.instanceId || "",
1009
+ opts.preview ? "1" : "0",
1010
+ opts.working ? "1" : "0",
1011
+ opts.zoneId || "",
1012
+ opts.compositionId || ""
1013
+ ].join("|");
1014
+ }
1015
+ function getSettledLayoutProvider(key) {
1016
+ const entry = settled.get(key);
1017
+ if (!entry) return void 0;
1018
+ if (Date.now() - entry.at > SETTLED_TTL_MS) {
1019
+ settled.delete(key);
1020
+ return void 0;
1021
+ }
1022
+ return entry.value;
1023
+ }
1024
+ function dedupeLayoutProviderFetch(key, fetchFn) {
1025
+ const cached = getSettledLayoutProvider(key);
1026
+ if (cached !== void 0) return Promise.resolve(cached);
1027
+ const existing = inflight.get(key);
1028
+ if (existing) return existing;
1029
+ const pending = fetchFn().then((value) => {
1030
+ settled.set(key, { value, at: Date.now() });
1031
+ return value;
1032
+ }).finally(() => {
1033
+ inflight.delete(key);
1034
+ });
1035
+ inflight.set(key, pending);
1036
+ return pending;
1037
+ }
1038
+ function previewLayoutReady(opts) {
1039
+ if (opts.hasClientLayout) return true;
1040
+ if (opts.clientLoading) return false;
1041
+ return opts.hasServerLayout;
1042
+ }
1043
+
968
1044
  // src/edge/normalizeRoute.ts
969
1045
  function normalizeRoute(route) {
970
1046
  let path = String(route || "").trim();
@@ -2378,6 +2454,32 @@ Available Console Commands:
2378
2454
  */
2379
2455
  async fetchPageConfig(route, preview = false) {
2380
2456
  const url = this.buildLayoutProviderUrl(route, preview);
2457
+ const pageSearch = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
2458
+ const cacheKey = layoutProviderCacheKey({
2459
+ route,
2460
+ trackingId: this.getTrackingId(),
2461
+ instanceId: getInstanceId(),
2462
+ preview,
2463
+ working: pageSearch ? isWorkingContentRequest(pageSearch) : false,
2464
+ zoneId: pageSearch?.get("zoneId"),
2465
+ compositionId: pageSearch?.get("compositionId")
2466
+ });
2467
+ return dedupeLayoutProviderFetch(cacheKey, async () => {
2468
+ const startedAt = Date.now();
2469
+ sdkDebug("[AmplifyUp SDK] layout/provider request", {
2470
+ route,
2471
+ at: new Date(startedAt).toISOString()
2472
+ });
2473
+ const page = await this.fetchPageConfigUncached(url);
2474
+ sdkDebug("[AmplifyUp SDK] layout/provider response", {
2475
+ route,
2476
+ durationMs: Date.now() - startedAt,
2477
+ hasPage: !!page
2478
+ });
2479
+ return page;
2480
+ });
2481
+ }
2482
+ async fetchPageConfigUncached(url) {
2381
2483
  const response = await fetch(url);
2382
2484
  if (!response.ok) {
2383
2485
  if (response.status === 404) {
@@ -2736,6 +2838,12 @@ Available Console Commands:
2736
2838
  );
2737
2839
  return;
2738
2840
  }
2841
+ if (event.data?.type === "AMPLIFYUP_LAYOUT_PATCH") {
2842
+ window.dispatchEvent(
2843
+ new CustomEvent("amplifyup:layout-patch", { detail: event.data })
2844
+ );
2845
+ return;
2846
+ }
2739
2847
  if (event.data?.type === "COMPONENT_DELETABLE_STATUS") {
2740
2848
  const { componentId, deletable, nodeId } = event.data;
2741
2849
  const element = document.querySelector(`[data-component-id="${componentId}"]`);
@@ -3776,6 +3884,13 @@ function getSDK(adapter) {
3776
3884
  var trackContentView = (contentId, contentType, properties) => getSDK().trackContentView(contentId, contentType, properties);
3777
3885
  var refreshEventTracking = () => getSDK().refreshEventTracking();
3778
3886
  getSDK();
3887
+ function useComposerPreviewFlag(force = false) {
3888
+ const [detected, setDetected] = useState(false);
3889
+ useEffect(() => {
3890
+ if (!force && isComposerPreview()) setDetected(true);
3891
+ }, [force]);
3892
+ return force || detected;
3893
+ }
3779
3894
 
3780
3895
  // src/composer-store.ts
3781
3896
  var ComposerFieldStore = class {
@@ -3992,16 +4107,9 @@ function NodeWithFields({
3992
4107
  renderComponent,
3993
4108
  composerPreview
3994
4109
  }) {
3995
- return /* @__PURE__ */ jsx(
3996
- MaybeComposerProvider,
3997
- {
3998
- enabled: !!(composerPreview || isComposerPreview()),
3999
- layoutNodeId: nodeId,
4000
- children: renderComponent(componentId, props, slots, {
4001
- layoutNodeId: nodeId
4002
- })
4003
- }
4004
- );
4110
+ return /* @__PURE__ */ jsx(MaybeComposerProvider, { enabled: !!composerPreview, layoutNodeId: nodeId, children: renderComponent(componentId, props, slots, {
4111
+ layoutNodeId: nodeId
4112
+ }) });
4005
4113
  }
4006
4114
  function AmplifyRendererInner({
4007
4115
  renderComponent,
@@ -4012,6 +4120,7 @@ function AmplifyRendererInner({
4012
4120
  composerPreview
4013
4121
  }) {
4014
4122
  const clientHook = useAmplifyUp();
4123
+ const composerPreviewFlag = useComposerPreviewFlag(!!composerPreview);
4015
4124
  const pageConfig = serverPageConfig || clientHook.pageConfig || null;
4016
4125
  const loading = serverPageConfig ? false : clientHook.loading;
4017
4126
  const error = serverPageConfig ? null : clientHook.error;
@@ -4364,7 +4473,7 @@ function AmplifyRendererInner({
4364
4473
  props: nodeRenderProps(node),
4365
4474
  slots: slotsToPass,
4366
4475
  renderComponent,
4367
- composerPreview
4476
+ composerPreview: composerPreviewFlag
4368
4477
  }
4369
4478
  )
4370
4479
  }
@@ -4377,7 +4486,7 @@ function AmplifyRendererInner({
4377
4486
  if (layoutTree.length === 0) {
4378
4487
  return /* @__PURE__ */ jsx("main", { children: emptyComponent || /* @__PURE__ */ jsx("div", { className: "container mx-auto px-4 py-16", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "No content configured for this page." }) }) });
4379
4488
  }
4380
- return /* @__PURE__ */ jsx(ComposerProvider, { children: /* @__PURE__ */ jsx("main", { children: composerPreview || isComposerPreview() ? /* @__PURE__ */ jsx(
4489
+ return /* @__PURE__ */ jsx(ComposerProvider, { children: /* @__PURE__ */ jsx("main", { children: composerPreviewFlag ? /* @__PURE__ */ jsx(
4381
4490
  "div",
4382
4491
  {
4383
4492
  "data-slot-id": "main",
@@ -4526,7 +4635,7 @@ function AmplifyPageContent({
4526
4635
  errorComponent
4527
4636
  }) {
4528
4637
  const client = useAmplifyUp();
4529
- const preview = forceComposerPreview || isComposerPreview();
4638
+ const preview = useComposerPreviewFlag(forceComposerPreview);
4530
4639
  const clientLayout = hasAmplifyLayout(client.pageConfig) ? client.pageConfig : null;
4531
4640
  const serverLayout = hasAmplifyLayout(pageConfigProp) ? pageConfigProp : null;
4532
4641
  let pageConfig = serverLayout ?? clientLayout;
@@ -4542,10 +4651,15 @@ function AmplifyPageContent({
4542
4651
  const previewCanvas = previewEmptyComponent ?? defaultPreviewCanvas();
4543
4652
  const previewLoading = previewLoadingComponent ?? previewCanvas;
4544
4653
  if (preview) {
4654
+ const ready = previewLayoutReady({
4655
+ clientLoading: client.loading,
4656
+ hasClientLayout: !!clientLayout,
4657
+ hasServerLayout: !!serverLayout
4658
+ });
4545
4659
  return /* @__PURE__ */ jsx(
4546
4660
  AmplifyRenderer,
4547
4661
  {
4548
- pageConfig: pageConfig ?? void 0,
4662
+ pageConfig: ready ? pageConfig ?? void 0 : void 0,
4549
4663
  renderComponent,
4550
4664
  composerPreview: true,
4551
4665
  loadingComponent: loadingComponent ?? previewLoading,
@@ -5005,6 +5119,18 @@ function warnNotScalarField(component, field, fieldValue) {
5005
5119
  }
5006
5120
  return false;
5007
5121
  }
5122
+ function assertNamedEnvelope(component, field) {
5123
+ if (process.env.NODE_ENV === "production") return;
5124
+ if (!field || typeof field !== "object" || Array.isArray(field)) return;
5125
+ const rec = field;
5126
+ if (!("value" in rec)) return;
5127
+ if (typeof rec.name === "string" && rec.name) return;
5128
+ throw new Error(
5129
+ `[AmplifyUp SDK] ${component} received an envelope-shaped object with a \`value\` but no \`name\` \u2014 a partial envelope was manufactured upstream (keys: ${Object.keys(
5130
+ rec
5131
+ ).join(", ")}). Find the producer; do not strip or rebuild envelopes.`
5132
+ );
5133
+ }
5008
5134
  var warnedReadOnlyFields = /* @__PURE__ */ new Set();
5009
5135
  function warnReadOnlyRowField(name) {
5010
5136
  if (process.env.NODE_ENV === "production") return;
@@ -5040,6 +5166,7 @@ function Field(props) {
5040
5166
  const hasField = isFieldEnvelope(field);
5041
5167
  const hasValue = hasOwnProp(props, "value");
5042
5168
  const { editName, fieldValue, ref } = useBoundField(field, value, name, "Field");
5169
+ assertNamedEnvelope("Field", field);
5043
5170
  warnProjectionProps("Field", hasField, hasValue, name);
5044
5171
  if (warnNotScalarField("Field", field, fieldValue)) {
5045
5172
  return null;
@@ -5100,6 +5227,7 @@ function RichText(props) {
5100
5227
  name,
5101
5228
  "RichText"
5102
5229
  );
5230
+ assertNamedEnvelope("RichText", field);
5103
5231
  if (!hasField && !hasValueProp && !name) {
5104
5232
  return null;
5105
5233
  }
@@ -5211,6 +5339,7 @@ function Image(props) {
5211
5339
  ...imgProps
5212
5340
  } = props;
5213
5341
  const context = useContext(ComponentContext);
5342
+ assertNamedEnvelope("Image", field);
5214
5343
  const coerced = coerceImageField(field, value, name);
5215
5344
  const boundField = coerced.field;
5216
5345
  const hasField = isFieldEnvelope(boundField);
@@ -5446,6 +5575,13 @@ function AmplifyUpProvider({
5446
5575
  description: "This page has not been published yet.",
5447
5576
  layoutTree: []
5448
5577
  };
5578
+ if (result.pageConfig) {
5579
+ sdk.applyResolvedPageData(
5580
+ result.pageConfig.pageData || null,
5581
+ result.pageConfig.route,
5582
+ extractRouteParams(result.pageConfig.route, actualRoute)
5583
+ );
5584
+ }
5449
5585
  setInitialized(true);
5450
5586
  setSource((prev) => prev === result.source ? prev : result.source);
5451
5587
  setMeta(
@@ -5458,13 +5594,6 @@ function AmplifyUpProvider({
5458
5594
  }
5459
5595
  return nextConfig;
5460
5596
  });
5461
- if (result.pageConfig) {
5462
- sdk.applyResolvedPageData(
5463
- result.pageConfig.pageData || null,
5464
- result.pageConfig.route,
5465
- extractRouteParams(result.pageConfig.route, actualRoute)
5466
- );
5467
- }
5468
5597
  loadedRouteRef.current = actualRoute;
5469
5598
  } catch (err) {
5470
5599
  if (cancelled) return;
@@ -5491,6 +5620,39 @@ function AmplifyUpProvider({
5491
5620
  config.clientPersonalization,
5492
5621
  pageContextKey
5493
5622
  ]);
5623
+ useEffect(() => {
5624
+ const applyLayoutTree = (layoutTree) => {
5625
+ if (!Array.isArray(layoutTree)) return;
5626
+ setPageConfig((prev) => {
5627
+ const next = envelopePageConfig({
5628
+ ...prev || {
5629
+ id: "patched",
5630
+ route: getActualRoute(),
5631
+ title: "",
5632
+ layoutTree: []
5633
+ },
5634
+ layoutTree
5635
+ });
5636
+ return next;
5637
+ });
5638
+ setLoading(false);
5639
+ };
5640
+ const onMessage = (event) => {
5641
+ if (event.data?.type === "AMPLIFYUP_LAYOUT_PATCH") {
5642
+ applyLayoutTree(event.data.layoutTree);
5643
+ }
5644
+ };
5645
+ const onCustom = (event) => {
5646
+ const detail = event.detail || {};
5647
+ applyLayoutTree(detail.layoutTree);
5648
+ };
5649
+ window.addEventListener("message", onMessage);
5650
+ window.addEventListener("amplifyup:layout-patch", onCustom);
5651
+ return () => {
5652
+ window.removeEventListener("message", onMessage);
5653
+ window.removeEventListener("amplifyup:layout-patch", onCustom);
5654
+ };
5655
+ }, []);
5494
5656
  return /* @__PURE__ */ jsx(
5495
5657
  AmplifyUpContext.Provider,
5496
5658
  {