@caelo-cms/shared 0.10.65 → 0.10.67

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.
Files changed (38) hide show
  1. package/dist/cloud-shell-install.d.ts +4 -7
  2. package/dist/cloud-shell-install.d.ts.map +1 -1
  3. package/dist/cloud-shell-install.js +14 -19
  4. package/dist/cloud-shell-install.js.map +1 -1
  5. package/dist/markup-structure.d.ts +9 -1
  6. package/dist/markup-structure.d.ts.map +1 -1
  7. package/dist/markup-structure.js +90 -13
  8. package/dist/markup-structure.js.map +1 -1
  9. package/dist/module-id-tag.d.ts +26 -0
  10. package/dist/module-id-tag.d.ts.map +1 -0
  11. package/dist/module-id-tag.js +39 -0
  12. package/dist/module-id-tag.js.map +1 -0
  13. package/dist/nested-module-render.d.ts +4 -0
  14. package/dist/nested-module-render.d.ts.map +1 -1
  15. package/dist/nested-module-render.js +7 -1
  16. package/dist/nested-module-render.js.map +1 -1
  17. package/dist/preview-compose.d.ts +2 -1
  18. package/dist/preview-compose.d.ts.map +1 -1
  19. package/dist/preview-compose.js +3 -24
  20. package/dist/preview-compose.js.map +1 -1
  21. package/dist/release-manifest.d.ts +5 -0
  22. package/dist/release-manifest.d.ts.map +1 -1
  23. package/dist/release-manifest.js +13 -0
  24. package/dist/release-manifest.js.map +1 -1
  25. package/dist/version.d.ts +2 -2
  26. package/dist/version.js +1 -1
  27. package/package.json +1 -1
  28. package/src/cloud-shell-install.test.ts +5 -10
  29. package/src/cloud-shell-install.ts +14 -21
  30. package/src/markup-structure.test.ts +85 -0
  31. package/src/markup-structure.ts +104 -10
  32. package/src/module-id-tag.ts +38 -0
  33. package/src/nested-module-render.test.ts +98 -5
  34. package/src/nested-module-render.ts +14 -1
  35. package/src/preview-compose-nested.test.ts +7 -2
  36. package/src/preview-compose.ts +4 -22
  37. package/src/release-manifest.ts +13 -0
  38. package/src/version.ts +1 -1
@@ -53,6 +53,9 @@ const PARENT_CI_ID = "00000000-0000-0000-0000-00000000bbb1";
53
53
  const CHILD_CI_ID = "00000000-0000-0000-0000-00000000bbb2";
54
54
  const GRAND_CI_ID = "00000000-0000-0000-0000-00000000bbb3";
55
55
 
56
+ /** The id stamp every nested module root carries (#1027). */
57
+ const tag = (moduleId: string) => `data-caelo-module-id="${moduleId}"`;
58
+
56
59
  describe("renderModuleWithContent — primitive substitution", () => {
57
60
  it("replaces {{name}} with values[name] and tracks the touched module", () => {
58
61
  const resolver = buildResolver(
@@ -154,7 +157,9 @@ describe("renderModuleWithContent — nested module slots", () => {
154
157
  ],
155
158
  );
156
159
  const r = renderModuleWithContent(PARENT_MOD_ID, PARENT_CI_ID, resolver);
157
- expect(r.html).toBe("<section><div><span>deep</span></div></section>");
160
+ expect(r.html).toBe(
161
+ `<section><div ${tag(CHILD_MOD_ID)}><span ${tag(GRAND_MOD_ID)}>deep</span></div></section>`,
162
+ );
158
163
  expect(r.missingSlots).toEqual([]);
159
164
  // All three modules touched — caller uses this set for CSS/JS dedup.
160
165
  expect(r.touchedModuleIds.has(PARENT_MOD_ID)).toBe(true);
@@ -201,7 +206,7 @@ describe("renderModuleWithContent — nested module slots", () => {
201
206
  ],
202
207
  );
203
208
  const r = renderModuleWithContent(PARENT_MOD_ID, PARENT_CI_ID, resolver);
204
- expect(r.html).toBe("<ul><li>A</li><li>B</li></ul>");
209
+ expect(r.html).toBe(`<ul><li ${tag(CHILD_MOD_ID)}>A</li><li ${tag(CHILD_MOD_ID)}>B</li></ul>`);
205
210
  expect(r.missingSlots).toEqual([]);
206
211
  });
207
212
  });
@@ -697,7 +702,7 @@ describe("renderModuleWithContent — engine-routing parity (#71)", () => {
697
702
  ],
698
703
  );
699
704
  const r = renderModuleWithContent(PARENT_MOD_ID, PARENT_CI_ID, resolver);
700
- expect(r.html).toContain("<article>OK</article>");
705
+ expect(r.html).toContain(`<article ${tag(CHILD_MOD_ID)}>OK</article>`);
701
706
  expect(r.html).toContain("<!-- caelo:missing reason=module-list-malformed cards[1] -->");
702
707
  expect(r.missingSlots).toContain("module-list-malformed:cards[1]");
703
708
  });
@@ -781,7 +786,9 @@ describe("renderPlacedModule — the composer's entry point", () => {
781
786
  resolver,
782
787
  {},
783
788
  );
784
- expect(r.html).toBe("<section><div>Pro<a>Buy</a></div></section>");
789
+ expect(r.html).toBe(
790
+ `<section><div ${tag(CHILD_MOD_ID)}>Pro<a ${tag(GRAND_MOD_ID)}>Buy</a></div></section>`,
791
+ );
785
792
  expect(r.failures).toEqual([]);
786
793
  expect([...r.touchedModuleIds].sort()).toEqual([CHILD_MOD_ID, GRAND_MOD_ID].sort());
787
794
  });
@@ -848,7 +855,7 @@ describe("renderPlacedModule — only template-referenced nested fields render",
848
855
  resolver,
849
856
  {},
850
857
  );
851
- expect(r.html).toBe("<section><article>Pro</article></section>");
858
+ expect(r.html).toBe(`<section><article ${tag(CHILD_MOD_ID)}>Pro</article></section>`);
852
859
  expect(r.failures).toEqual([]);
853
860
  expect(r.missingSlots).toEqual([]);
854
861
  expect([...r.touchedModuleIds]).toEqual([CHILD_MOD_ID]);
@@ -885,3 +892,89 @@ describe("renderPlacedModule — only template-referenced nested fields render",
885
892
  expect(r.failures).toEqual([{ field: "card", reason: `content-instance-missing:${GONE_CI}` }]);
886
893
  });
887
894
  });
895
+
896
+ describe("#1027 — nested module roots carry data-caelo-module-id", () => {
897
+ // The FAQ case from the searchVIU install: a module-list of FAQ items.
898
+ const faq: NestedModuleResource = {
899
+ moduleId: PARENT_MOD_ID,
900
+ slug: "faq",
901
+ html: '<div class="faq">{{#items}}{{/items}}{{>featured}}</div>',
902
+ css: "",
903
+ js: "",
904
+ fields: [
905
+ { name: "items", kind: "module-list" },
906
+ { name: "featured", kind: "module" },
907
+ ],
908
+ };
909
+ const item: NestedModuleResource = {
910
+ moduleId: CHILD_MOD_ID,
911
+ slug: "faq-entry",
912
+ html: "<details><summary>{{question}}</summary><p>{{answer}}</p></details>",
913
+ css: "",
914
+ js: "",
915
+ fields: [
916
+ { name: "question", kind: "text" },
917
+ { name: "answer", kind: "text" },
918
+ ],
919
+ };
920
+ const values = {
921
+ items: [
922
+ { moduleId: CHILD_MOD_ID, contentInstanceId: CHILD_CI_ID },
923
+ { moduleId: CHILD_MOD_ID, contentInstanceId: GRAND_CI_ID },
924
+ ],
925
+ featured: { moduleId: CHILD_MOD_ID, contentInstanceId: CHILD_CI_ID },
926
+ };
927
+ const resolver = buildResolver(
928
+ [faq, item],
929
+ [
930
+ { id: PARENT_CI_ID, moduleId: PARENT_MOD_ID, values, deletedAt: null },
931
+ {
932
+ id: CHILD_CI_ID,
933
+ moduleId: CHILD_MOD_ID,
934
+ values: { question: "Q1", answer: "A1" },
935
+ deletedAt: null,
936
+ },
937
+ {
938
+ id: GRAND_CI_ID,
939
+ moduleId: CHILD_MOD_ID,
940
+ values: { question: "Q2", answer: "A2" },
941
+ deletedAt: null,
942
+ },
943
+ ],
944
+ );
945
+ const expected =
946
+ `<div class="faq"><details ${tag(CHILD_MOD_ID)}><summary>Q1</summary><p>A1</p></details>` +
947
+ `<details ${tag(CHILD_MOD_ID)}><summary>Q2</summary><p>A2</p></details>` +
948
+ `<details ${tag(CHILD_MOD_ID)}><summary>Q1</summary><p>A1</p></details></div>`;
949
+
950
+ it("tags module and module-list roots in the editor preview path", () => {
951
+ const r = renderModuleWithContent(PARENT_MOD_ID, PARENT_CI_ID, resolver);
952
+ // The placed module's own root is left to the composer (tagModuleId).
953
+ expect(r.html).toBe(expected);
954
+ expect(r.failures).toEqual([]);
955
+ });
956
+
957
+ it("tags them identically in the composer / static build path", () => {
958
+ const r = renderPlacedModule({ html: faq.html, fields: faq.fields, values }, resolver, {});
959
+ expect(r.html).toBe(expected);
960
+ });
961
+
962
+ it("leaves a failure comment untagged (no element to carry the id)", () => {
963
+ const r = renderPlacedModule(
964
+ {
965
+ html: faq.html,
966
+ fields: faq.fields,
967
+ values: {
968
+ items: [{ moduleId: CHILD_MOD_ID, contentInstanceId: "missing" }],
969
+ featured: values.featured,
970
+ },
971
+ },
972
+ resolver,
973
+ {},
974
+ );
975
+ expect(r.html).toBe(
976
+ '<div class="faq"><!-- caelo:missing reason=content-instance-missing missing -->' +
977
+ `<details ${tag(CHILD_MOD_ID)}><summary>Q1</summary><p>A1</p></details></div>`,
978
+ );
979
+ });
980
+ });
@@ -51,6 +51,10 @@
51
51
  * comment + missingSlots entry. Same channel; the operator sees the
52
52
  * gap in the preview, and the static generator refuses the build.
53
53
  *
54
+ * Module ids: the root element of every nested module render carries
55
+ * `data-caelo-module-id` (#1027), the same stamp the composer puts on a
56
+ * page placement's root, in the preview and the static build alike.
57
+ *
54
58
  * CSS + JS dedup: every unique module touched during recursion
55
59
  * contributes its CSS + JS once. The caller collects the seen-set out
56
60
  * of band so the page's <head>/<style> + footer scripts are stable.
@@ -58,6 +62,7 @@
58
62
 
59
63
  import type { ModuleFieldKind } from "./content.js";
60
64
  import { isLinkFailureMarker, type LinkResolver } from "./link-refs.js";
65
+ import { tagModuleId } from "./module-id-tag.js";
61
66
  import { caeloMissingComment, renderTemplate, type TemplateField } from "./template-engine.js";
62
67
 
63
68
  const MAX_RECURSION_DEPTH = 8;
@@ -343,7 +348,15 @@ function renderPartial(
343
348
  ): string | undefined {
344
349
  const hit = nestedRefForPartial(key, fieldByName, values);
345
350
  if (!hit) return undefined;
346
- return renderInner(hit.ref.moduleId, hit.ref.contentInstanceId, descend(ctx, hit.step));
351
+ // #1027 — a nested module's root carries its own id, exactly like a
352
+ // page placement's (the composer tags those), so the live-edit overlay
353
+ // and the quality checks attribute its markup to the nested module
354
+ // instead of reading it as free HTML of the parent. A failure comment
355
+ // has no element and stays as it is.
356
+ return tagModuleId(
357
+ renderInner(hit.ref.moduleId, hit.ref.contentInstanceId, descend(ctx, hit.step)),
358
+ hit.ref.moduleId,
359
+ );
347
360
  }
348
361
 
349
362
  /**
@@ -30,6 +30,9 @@ const CARD_A = "00000000-0000-0000-0000-0000000ca001";
30
30
  const CARD_B = "00000000-0000-0000-0000-0000000ca002";
31
31
  const GONE = "00000000-0000-0000-0000-00000000dead";
32
32
 
33
+ /** The id stamp every nested module root carries (#1027). */
34
+ const tag = (moduleId: string) => `data-caelo-module-id="${moduleId}"`;
35
+
33
36
  const card: NestedModuleResource = {
34
37
  moduleId: CARD_MOD,
35
38
  slug: "card",
@@ -89,7 +92,9 @@ describe("composePageWithLayout — nested modules", () => {
89
92
 
90
93
  it("renders every module-list element with the nested resolver", () => {
91
94
  const out = compose(grid(refs), resolver([card], instances));
92
- expect(out.html).toContain("<article>Starter</article><article>Pro</article>");
95
+ expect(out.html).toContain(
96
+ `<article ${tag(CARD_MOD)}>Starter</article><article ${tag(CARD_MOD)}>Pro</article>`,
97
+ );
93
98
  expect(out.html).not.toContain("needs recursive renderer");
94
99
  expect(out.moduleFailures).toEqual([]);
95
100
  });
@@ -104,7 +109,7 @@ describe("composePageWithLayout — nested modules", () => {
104
109
  grid([refs[0], { moduleId: CARD_MOD, contentInstanceId: GONE }, "not-a-ref"]),
105
110
  resolver([card], instances),
106
111
  );
107
- expect(out.html).toContain("<article>Starter</article>");
112
+ expect(out.html).toContain(`<article ${tag(CARD_MOD)}>Starter</article>`);
108
113
  expect(out.moduleFailures).toEqual([
109
114
  {
110
115
  blockName: "content",
@@ -29,6 +29,7 @@ import { BASE_TECHNICAL_CSS } from "./base-css.js";
29
29
  import type { ModuleFieldKind } from "./content.js";
30
30
  import { findInteractionIds, interactionAssets } from "./interactions.js";
31
31
  import { type LinkResolver, resolveLinkRefsInHtml } from "./link-refs.js";
32
+ import { tagModuleId } from "./module-id-tag.js";
32
33
  import {
33
34
  type NestedModuleResource,
34
35
  type NestedRenderFailure,
@@ -47,6 +48,9 @@ import { renderTemplate, type TemplateField } from "./template-engine.js";
47
48
  import { renderThemeCss as renderThemeCssFromTokens } from "./theme-render.js";
48
49
  import type { ThemeDocument } from "./themes.js";
49
50
 
51
+ // Re-exported so existing callers keep importing it from the composer.
52
+ export { tagModuleId };
53
+
50
54
  export interface ComposeModule {
51
55
  readonly moduleId: string;
52
56
  readonly slug: string;
@@ -384,16 +388,6 @@ export function composePagePreview(input: ComposeInput): ComposeOutput {
384
388
  };
385
389
  }
386
390
 
387
- /**
388
- * Insert `data-caelo-module-id="<id>"` into the first opening tag of
389
- * the module's HTML. Idempotent — re-tagging an already-tagged module
390
- * is a no-op. Comments / DOCTYPE / leading whitespace before the first
391
- * tag are tolerated. Modules that have no opening tag (pure text)
392
- * return unchanged because there's nothing to attach to.
393
- *
394
- * Exported so callers (admin preview endpoint, static generator,
395
- * tests) can reuse the same logic.
396
- */
397
391
  /**
398
392
  * P6.7.5 — return the items for a `nav-menu/<slug>` set when a module's
399
393
  * slug starts with `nav-menu-`. Returns null when the module is not a
@@ -605,18 +599,6 @@ function recordFailures(
605
599
  }
606
600
  }
607
601
 
608
- export function tagModuleId(html: string, moduleId: string): string {
609
- if (!html) return html;
610
- const firstOpen = /<([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*)>/;
611
- const m = firstOpen.exec(html);
612
- if (!m) return html;
613
- // Already tagged?
614
- const tagAttrs = m[2] ?? "";
615
- if (/\sdata-caelo-module-id\s*=/.test(tagAttrs)) return html;
616
- const replaced = `<${m[1]}${tagAttrs} data-caelo-module-id="${moduleId}">`;
617
- return html.slice(0, m.index) + replaced + html.slice(m.index + m[0].length);
618
- }
619
-
620
602
  /**
621
603
  * P6.7.6 — layout-aware composer. Runs the template composer first,
622
604
  * extracts the resulting body content, then renders the layout HTML
@@ -40,6 +40,19 @@ export const ReleaseManifestSchema = z.object({
40
40
  version: z.string().regex(SEMVER, "expected a SemVer version without a leading v"),
41
41
  channel: z.enum(RELEASE_CHANNELS),
42
42
  digests: z.object({ admin: digest, gateway: digest, ops: digest }),
43
+ /**
44
+ * #1025 — the linux/amd64 platform manifest inside each image index of
45
+ * `digests`. The release images are OCI indexes (buildx adds an
46
+ * attestation manifest even to a single-platform build), and Cloud Run
47
+ * records the platform manifest it resolved — not the index a deploy
48
+ * pinned — in a revision's `spec.containers[0].image` and
49
+ * `status.imageDigest`. Signing this form too lets an upgrader tell
50
+ * which release serves without a registry round trip. Absent on
51
+ * releases from before the field: optional, so manifestVersion stays 1
52
+ * and older readers ignore it; readers then list the index itself
53
+ * (content-addressed, so it cannot be forged either).
54
+ */
55
+ platformDigests: z.object({ admin: digest, gateway: digest, ops: digest }).optional(),
43
56
  minFromVersion: z.string().regex(SEMVER),
44
57
  infraContractVersion: z.number().int().positive(),
45
58
  migrations: z.object({
package/src/version.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * follow standard SemVer.
23
23
  */
24
24
 
25
- export const CAELO_VERSION = "0.10.65";
25
+ export const CAELO_VERSION = "0.10.67";
26
26
 
27
27
  /**
28
28
  * Deprecated alias for back-compat — early P17 work spelled this