meno-core 1.0.19 → 1.0.21

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 (50) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/lib/client/core/ComponentBuilder.test.ts +68 -56
  3. package/lib/client/core/ComponentBuilder.ts +6 -4
  4. package/lib/client/core/builders/embedBuilder.ts +10 -1
  5. package/lib/client/core/builders/index.ts +6 -2
  6. package/lib/client/core/builders/{cmsListBuilder.ts → listBuilder.ts} +227 -95
  7. package/lib/client/responsiveStyleResolver.test.ts +12 -12
  8. package/lib/client/responsiveStyleResolver.ts +19 -7
  9. package/lib/client/routing/Router.tsx +35 -7
  10. package/lib/client/templateEngine.test.ts +126 -0
  11. package/lib/client/templateEngine.ts +53 -13
  12. package/lib/server/jsonLoader.test.ts +4 -1
  13. package/lib/server/jsonLoader.ts +64 -15
  14. package/lib/server/services/configService.ts +68 -13
  15. package/lib/server/ssr/attributeBuilder.ts +8 -0
  16. package/lib/server/ssr/index.ts +1 -1
  17. package/lib/server/ssr/ssrRenderer.ts +245 -111
  18. package/lib/server/ssrRenderer.test.ts +197 -3
  19. package/lib/server/validateStyleCoverage.ts +14 -17
  20. package/lib/shared/breakpoints.test.ts +210 -23
  21. package/lib/shared/breakpoints.ts +124 -17
  22. package/lib/shared/constants.test.ts +1 -1
  23. package/lib/shared/constants.ts +5 -1
  24. package/lib/shared/cssGeneration.test.ts +17 -0
  25. package/lib/shared/cssGeneration.ts +49 -12
  26. package/lib/shared/index.ts +3 -0
  27. package/lib/shared/itemTemplateUtils.test.ts +44 -2
  28. package/lib/shared/itemTemplateUtils.ts +15 -2
  29. package/lib/shared/nodeUtils.ts +23 -4
  30. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +2 -2
  31. package/lib/shared/registry/nodeTypes/ListNodeType.ts +186 -0
  32. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -0
  33. package/lib/shared/registry/nodeTypes/index.ts +6 -5
  34. package/lib/shared/responsiveScaling.test.ts +87 -0
  35. package/lib/shared/responsiveScaling.ts +33 -29
  36. package/lib/shared/responsiveStyleUtils.test.ts +7 -7
  37. package/lib/shared/responsiveStyleUtils.ts +22 -16
  38. package/lib/shared/styleNodeUtils.ts +5 -5
  39. package/lib/shared/styleValueRegistry.ts +60 -5
  40. package/lib/shared/tree/PathBuilder.ts +3 -3
  41. package/lib/shared/treePathUtils.ts +7 -5
  42. package/lib/shared/types/cms.ts +4 -57
  43. package/lib/shared/types/components.ts +45 -4
  44. package/lib/shared/types/index.ts +13 -0
  45. package/lib/shared/utilityClassConfig.ts +14 -0
  46. package/lib/shared/utilityClassMapper.ts +43 -2
  47. package/lib/shared/validation/propValidator.ts +9 -1
  48. package/lib/shared/validation/schemas.ts +60 -14
  49. package/package.json +1 -1
  50. package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +0 -109
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npx tsc:*)"
5
+ ]
6
+ }
7
+ }
@@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, mock } from "bun:test";
2
2
  import { ComponentBuilder } from "./ComponentBuilder";
3
3
  import { ComponentRegistry } from "../componentRegistry";
4
4
  import { ElementRegistry } from "../elementRegistry";
5
- import type { ComponentNode, CMSListNode, CMSItem } from "../../shared/types";
5
+ import type { ComponentNode, ListNode, CMSItem } from "../../shared/types";
6
6
  import { NODE_TYPE } from "../../shared/constants";
7
7
  import { createMockElementRegistry } from "../../test-utils/mocks";
8
8
 
@@ -677,7 +677,7 @@ describe("ComponentBuilder", () => {
677
677
  });
678
678
  });
679
679
 
680
- describe("buildComponent - CMS List", () => {
680
+ describe("buildComponent - List (Collection Mode)", () => {
681
681
  // Helper to create mock CMS items
682
682
  const createMockCMSItems = (count: number): CMSItem[] => {
683
683
  return Array.from({ length: count }, (_, i) => ({
@@ -688,21 +688,22 @@ describe("ComponentBuilder", () => {
688
688
  }));
689
689
  };
690
690
 
691
- // Helper to create a CMS List node
692
- const createCMSListNode = (
693
- collection: string,
691
+ // Helper to create a List node with sourceType: 'collection'
692
+ const createCollectionListNode = (
693
+ source: string,
694
694
  children: ComponentNode[],
695
695
  options: { limit?: number; offset?: number } = {}
696
- ): CMSListNode => ({
697
- type: NODE_TYPE.CMS_LIST,
698
- collection,
696
+ ): ListNode => ({
697
+ type: NODE_TYPE.LIST,
698
+ sourceType: 'collection',
699
+ source,
699
700
  children,
700
701
  ...options,
701
702
  });
702
703
 
703
704
  test("should render children for each CMS item", () => {
704
705
  const items = createMockCMSItems(3);
705
- const node = createCMSListNode("posts", [
706
+ const node = createCollectionListNode("posts", [
706
707
  { type: "node", tag: "div", children: ["Item content"] },
707
708
  ]);
708
709
 
@@ -721,7 +722,7 @@ describe("ComponentBuilder", () => {
721
722
 
722
723
  test("should add data-cms-item-index attribute to each item wrapper", () => {
723
724
  const items = createMockCMSItems(3);
724
- const node = createCMSListNode("posts", [
725
+ const node = createCollectionListNode("posts", [
725
726
  { type: "node", tag: "div", children: ["Item content"] },
726
727
  ]);
727
728
 
@@ -749,7 +750,7 @@ describe("ComponentBuilder", () => {
749
750
  const items: CMSItem[] = [
750
751
  { _id: "1", _slug: "test", title: "Hello World" },
751
752
  ];
752
- const node = createCMSListNode("posts", [
753
+ const node = createCollectionListNode("posts", [
753
754
  { type: "node", tag: "h1", children: ["{{item.title}}"] },
754
755
  ]);
755
756
 
@@ -765,7 +766,7 @@ describe("ComponentBuilder", () => {
765
766
 
766
767
  test("should interpolate {{itemIndex}}, {{itemFirst}}, {{itemLast}}", () => {
767
768
  const items = createMockCMSItems(3);
768
- const node = createCMSListNode("posts", [
769
+ const node = createCollectionListNode("posts", [
769
770
  { type: "node", tag: "span", children: ["Index: {{itemIndex}}, First: {{itemFirst}}, Last: {{itemLast}}"] },
770
771
  ]);
771
772
 
@@ -782,7 +783,7 @@ describe("ComponentBuilder", () => {
782
783
  });
783
784
 
784
785
  test("should show empty state when no items", () => {
785
- const node = createCMSListNode("posts", [
786
+ const node = createCollectionListNode("posts", [
786
787
  { type: "node", tag: "div", children: ["Item"] },
787
788
  ]);
788
789
 
@@ -799,7 +800,7 @@ describe("ComponentBuilder", () => {
799
800
  });
800
801
 
801
802
  test("should show empty state when collection not in map", () => {
802
- const node = createCMSListNode("posts", [
803
+ const node = createCollectionListNode("posts", [
803
804
  { type: "node", tag: "div", children: ["Item"] },
804
805
  ]);
805
806
 
@@ -814,7 +815,7 @@ describe("ComponentBuilder", () => {
814
815
 
815
816
  test("should apply limit to items", () => {
816
817
  const items = createMockCMSItems(5);
817
- const node = createCMSListNode("posts", [
818
+ const node = createCollectionListNode("posts", [
818
819
  { type: "node", tag: "div", children: ["Item"] },
819
820
  ], { limit: 2 });
820
821
 
@@ -831,7 +832,7 @@ describe("ComponentBuilder", () => {
831
832
 
832
833
  test("should apply offset to items", () => {
833
834
  const items = createMockCMSItems(5);
834
- const node = createCMSListNode("posts", [
835
+ const node = createCollectionListNode("posts", [
835
836
  { type: "node", tag: "div", children: ["Item"] },
836
837
  ], { offset: 2 });
837
838
 
@@ -848,7 +849,7 @@ describe("ComponentBuilder", () => {
848
849
 
849
850
  test("should apply both limit and offset", () => {
850
851
  const items = createMockCMSItems(5);
851
- const node = createCMSListNode("posts", [
852
+ const node = createCollectionListNode("posts", [
852
853
  { type: "node", tag: "div", children: ["Item"] },
853
854
  ], { offset: 1, limit: 2 });
854
855
 
@@ -870,9 +871,10 @@ describe("ComponentBuilder", () => {
870
871
  { _id: "3", _slug: "post3", featured: true, title: "Another Featured" },
871
872
  { _id: "4", _slug: "post4", featured: false, title: "Regular Post 2" },
872
873
  ];
873
- const node: CMSListNode = {
874
- type: NODE_TYPE.CMS_LIST,
875
- collection: "posts",
874
+ const node: ListNode = {
875
+ type: NODE_TYPE.LIST,
876
+ sourceType: 'collection',
877
+ source: "posts",
876
878
  filter: { featured: true },
877
879
  children: [{ type: "node", tag: "div", children: ["Item"] }],
878
880
  };
@@ -895,9 +897,10 @@ describe("ComponentBuilder", () => {
895
897
  { _id: "3", _slug: "post3", price: 200, title: "Expensive 2" },
896
898
  { _id: "4", _slug: "post4", price: 75, title: "Mid" },
897
899
  ];
898
- const node: CMSListNode = {
899
- type: NODE_TYPE.CMS_LIST,
900
- collection: "posts",
900
+ const node: ListNode = {
901
+ type: NODE_TYPE.LIST,
902
+ sourceType: 'collection',
903
+ source: "posts",
901
904
  filter: { field: "price", operator: "gt", value: 100 },
902
905
  children: [{ type: "node", tag: "div", children: ["Item"] }],
903
906
  };
@@ -919,9 +922,10 @@ describe("ComponentBuilder", () => {
919
922
  { _id: "2", _slug: "post2", category: "news", title: "News Post" },
920
923
  { _id: "3", _slug: "post3", category: "tech", title: "Tech Post 2" },
921
924
  ];
922
- const node: CMSListNode = {
923
- type: NODE_TYPE.CMS_LIST,
924
- collection: "posts",
925
+ const node: ListNode = {
926
+ type: NODE_TYPE.LIST,
927
+ sourceType: 'collection',
928
+ source: "posts",
925
929
  filter: { category: "{{cms.category}}" },
926
930
  children: [{ type: "node", tag: "div", children: ["Item"] }],
927
931
  };
@@ -944,9 +948,10 @@ describe("ComponentBuilder", () => {
944
948
  { _id: "2", _slug: "a", title: "A" },
945
949
  { _id: "3", _slug: "b", title: "B" },
946
950
  ];
947
- const node: CMSListNode = {
948
- type: NODE_TYPE.CMS_LIST,
949
- collection: "posts",
951
+ const node: ListNode = {
952
+ type: NODE_TYPE.LIST,
953
+ sourceType: 'collection',
954
+ source: "posts",
950
955
  sort: { field: "title", order: "asc" },
951
956
  children: [{ type: "node", tag: "div", children: ["{{item.title}}"] }],
952
957
  };
@@ -969,9 +974,10 @@ describe("ComponentBuilder", () => {
969
974
  { _id: "2", _slug: "p2", date: 300 },
970
975
  { _id: "3", _slug: "p3", date: 200 },
971
976
  ];
972
- const node: CMSListNode = {
973
- type: NODE_TYPE.CMS_LIST,
974
- collection: "posts",
977
+ const node: ListNode = {
978
+ type: NODE_TYPE.LIST,
979
+ sourceType: 'collection',
980
+ source: "posts",
975
981
  sort: { field: "date", order: "desc" },
976
982
  children: [{ type: "node", tag: "div", children: ["Item"] }],
977
983
  };
@@ -996,9 +1002,10 @@ describe("ComponentBuilder", () => {
996
1002
  { _id: "4", _slug: "p4", featured: true, title: "Featured 2" },
997
1003
  { _id: "5", _slug: "p5", featured: false, title: "Regular 3" },
998
1004
  ];
999
- const node: CMSListNode = {
1000
- type: NODE_TYPE.CMS_LIST,
1001
- collection: "posts",
1005
+ const node: ListNode = {
1006
+ type: NODE_TYPE.LIST,
1007
+ sourceType: 'collection',
1008
+ source: "posts",
1002
1009
  filter: { featured: true },
1003
1010
  limit: 3, // Without proper order, this would limit before filtering
1004
1011
  children: [{ type: "node", tag: "div", children: ["Item"] }],
@@ -1024,9 +1031,10 @@ describe("ComponentBuilder", () => {
1024
1031
  { _id: "4", _slug: "p4", date: 400 },
1025
1032
  { _id: "5", _slug: "p5", date: 50 },
1026
1033
  ];
1027
- const node: CMSListNode = {
1028
- type: NODE_TYPE.CMS_LIST,
1029
- collection: "posts",
1034
+ const node: ListNode = {
1035
+ type: NODE_TYPE.LIST,
1036
+ sourceType: 'collection',
1037
+ source: "posts",
1030
1038
  sort: { field: "date", order: "desc" },
1031
1039
  limit: 2, // Should get 2 most recent (highest date)
1032
1040
  children: [{ type: "node", tag: "div", children: ["Item"] }],
@@ -1050,9 +1058,10 @@ describe("ComponentBuilder", () => {
1050
1058
  { _id: "3", _slug: "p3", featured: true, price: 50 },
1051
1059
  { _id: "4", _slug: "p4", featured: true, price: 200 },
1052
1060
  ];
1053
- const node: CMSListNode = {
1054
- type: NODE_TYPE.CMS_LIST,
1055
- collection: "posts",
1061
+ const node: ListNode = {
1062
+ type: NODE_TYPE.LIST,
1063
+ sourceType: 'collection',
1064
+ source: "posts",
1056
1065
  filter: [
1057
1066
  { field: "featured", value: true },
1058
1067
  { field: "price", operator: "gte", value: 100 }
@@ -1531,7 +1540,7 @@ describe("ComponentBuilder", () => {
1531
1540
  });
1532
1541
  });
1533
1542
 
1534
- describe("Edge Case 7: CMS List Paths Tracking", () => {
1543
+ describe("Edge Case 7: List Paths Tracking (Collection Mode)", () => {
1535
1544
  const createMockCMSItems = (count: number): CMSItem[] => {
1536
1545
  return Array.from({ length: count }, (_, i) => ({
1537
1546
  _id: `item-${i}`,
@@ -1540,11 +1549,12 @@ describe("ComponentBuilder", () => {
1540
1549
  }));
1541
1550
  };
1542
1551
 
1543
- test("should pass cmsListPaths through buildChildren", () => {
1544
- // Create a simple CMS list with items
1545
- const node: CMSListNode = {
1546
- type: NODE_TYPE.CMS_LIST,
1547
- collection: "posts",
1552
+ test("should pass listPaths through buildChildren", () => {
1553
+ // Create a simple list with items
1554
+ const node: ListNode = {
1555
+ type: NODE_TYPE.LIST,
1556
+ sourceType: 'collection',
1557
+ source: "posts",
1548
1558
  children: [{ type: "node", tag: "div", children: ["Item"] }],
1549
1559
  };
1550
1560
 
@@ -1555,21 +1565,23 @@ describe("ComponentBuilder", () => {
1555
1565
  elementPath: [0],
1556
1566
  });
1557
1567
 
1558
- // If cmsListPaths tracking works, the result should render without errors
1568
+ // If listPaths tracking works, the result should render without errors
1559
1569
  expect(result).not.toBeNull();
1560
1570
  const element = result as { props?: { children?: unknown[] } };
1561
1571
  expect(element?.props?.children?.length).toBe(2);
1562
1572
  });
1563
1573
 
1564
- test("should track cmsListPaths in nested CMS lists", () => {
1565
- // Outer CMS list with inner CMS list
1566
- const outerNode: CMSListNode = {
1567
- type: NODE_TYPE.CMS_LIST,
1568
- collection: "categories",
1574
+ test("should track listPaths in nested lists", () => {
1575
+ // Outer list with inner list
1576
+ const outerNode: ListNode = {
1577
+ type: NODE_TYPE.LIST,
1578
+ sourceType: 'collection',
1579
+ source: "categories",
1569
1580
  children: [
1570
1581
  {
1571
- type: NODE_TYPE.CMS_LIST,
1572
- collection: "posts",
1582
+ type: NODE_TYPE.LIST,
1583
+ sourceType: 'collection',
1584
+ source: "posts",
1573
1585
  children: [{ type: "node", tag: "span", children: ["Post"] }],
1574
1586
  } as any,
1575
1587
  ],
@@ -1584,7 +1596,7 @@ describe("ComponentBuilder", () => {
1584
1596
  elementPath: [0],
1585
1597
  });
1586
1598
 
1587
- // Nested CMS lists should render correctly with path tracking
1599
+ // Nested lists should render correctly with path tracking
1588
1600
  expect(result).not.toBeNull();
1589
1601
  });
1590
1602
  });
@@ -13,7 +13,7 @@ import { processStructure } from "../templateEngine";
13
13
  import { NODE_TYPE } from "../../shared/constants";
14
14
  import { ErrorBoundary } from "../ErrorBoundary";
15
15
  import type { ComponentNode, StyleValue } from "../../shared/types";
16
- import { isComponentNode, extractNodeProperties, isSlotMarker, isEmbedNode, isLinkNode, isLocaleListNode, isCMSListNode, markAsSlotContent, evaluateNodeIf, isBooleanMapping } from "../../shared/nodeUtils";
16
+ import { isComponentNode, extractNodeProperties, isSlotMarker, isEmbedNode, isLinkNode, isLocaleListNode, isListNode, markAsSlotContent, evaluateNodeIf, isBooleanMapping } from "../../shared/nodeUtils";
17
17
  import { mergeNodeStyles } from "../../shared/styleNodeUtils";
18
18
  import { extractAttributesFromNode } from "../../shared/attributeNodeUtils";
19
19
  import { resolvePropsFromDefinition } from "../../shared/propResolver";
@@ -41,7 +41,7 @@ import {
41
41
  buildEmbed,
42
42
  buildLinkNode,
43
43
  buildLocaleList,
44
- buildCMSList,
44
+ buildList,
45
45
  buildLink,
46
46
  } from "./builders";
47
47
 
@@ -346,8 +346,10 @@ export class ComponentBuilder {
346
346
  return buildLocaleList(node, ctx, builderDeps);
347
347
  }
348
348
 
349
- if (nodeType === NODE_TYPE.CMS_LIST && isCMSListNode(node)) {
350
- return buildCMSList(node, children, ctx, builderDeps);
349
+ // Handle List nodes (unified - handles both prop and collection source types)
350
+ // isListNode() also matches legacy 'cms-list' type for migration
351
+ if (isListNode(node)) {
352
+ return buildList(node, children, ctx, builderDeps);
351
353
  }
352
354
 
353
355
  // Extract node properties
@@ -15,6 +15,7 @@ import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveEx
15
15
  import DOMPurify from "isomorphic-dompurify";
16
16
  import type { ElementRegistry } from "../../elementRegistry";
17
17
  import type { BuilderContext } from "./types";
18
+ import { hasItemTemplates, processItemTemplate } from "../../../shared/itemTemplateUtils";
18
19
 
19
20
  export interface EmbedBuilderDeps {
20
21
  elementRegistry: ElementRegistry;
@@ -49,8 +50,16 @@ export function buildEmbed(
49
50
  ): ReactElement {
50
51
  const { key, elementPath, parentComponentName, componentContext, componentRootPath, cmsItemIndexPath } = ctx;
51
52
 
53
+ // Process templates in html property before sanitization (matching SSR behavior)
54
+ let htmlContent = node.html;
55
+
56
+ // Process item template strings (for List context) - e.g., {{item.icon}}, {{feature.title}}
57
+ if (ctx.templateContext && hasItemTemplates(htmlContent)) {
58
+ htmlContent = processItemTemplate(htmlContent, ctx.templateContext);
59
+ }
60
+
52
61
  // Sanitize HTML with allowlist
53
- const sanitizedHtml = DOMPurify.sanitize(node.html, SANITIZE_CONFIG);
62
+ const sanitizedHtml = DOMPurify.sanitize(htmlContent, SANITIZE_CONFIG);
54
63
  const effectiveParentComponentName = deps.getEffectiveParentComponentName(componentContext, parentComponentName);
55
64
 
56
65
  // Extract attributes from node
@@ -5,7 +5,7 @@
5
5
  * - embedBuilder: Embed nodes (HTML content like SVGs)
6
6
  * - linkNodeBuilder: Link nodes (links as div in editor)
7
7
  * - localeListBuilder: Locale list nodes (language switchers)
8
- * - cmsListBuilder: CMS list nodes (collection item rendering)
8
+ * - listBuilder: Unified list nodes (prop and collection item rendering)
9
9
  * - linkBuilder: Link components with navigation/prefetch
10
10
  */
11
11
 
@@ -22,5 +22,9 @@ export type {
22
22
  export { buildEmbed, type EmbedBuilderDeps } from './embedBuilder';
23
23
  export { buildLinkNode, type LinkNodeBuilderDeps } from './linkNodeBuilder';
24
24
  export { buildLocaleList, type LocaleListBuilderDeps } from './localeListBuilder';
25
- export { buildCMSList, type CMSListBuilderDeps } from './cmsListBuilder';
25
+ export { buildList, type ListBuilderDeps } from './listBuilder';
26
26
  export { buildLink, type LinkBuilderDeps } from './linkBuilder';
27
+
28
+ // Legacy alias for backward compatibility during migration
29
+ export { buildList as buildCMSList } from './listBuilder';
30
+ export type { ListBuilderDeps as CMSListBuilderDeps } from './listBuilder';