meno-core 1.0.40 → 1.0.41

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 (56) hide show
  1. package/bin/cli.ts +33 -0
  2. package/build-astro.ts +3 -27
  3. package/dist/bin/cli.js +30 -2
  4. package/dist/bin/cli.js.map +2 -2
  5. package/dist/build-static.js +7 -7
  6. package/dist/chunks/{chunk-3NOZVNM4.js → chunk-EQOSDQS2.js} +4 -4
  7. package/dist/chunks/{chunk-V4SVSX3X.js → chunk-IBR2F4IL.js} +4 -5
  8. package/dist/chunks/{chunk-V4SVSX3X.js.map → chunk-IBR2F4IL.js.map} +2 -2
  9. package/dist/chunks/{chunk-OJ5SROQN.js → chunk-IGVQF5GY.js} +8 -6
  10. package/dist/chunks/chunk-IGVQF5GY.js.map +7 -0
  11. package/dist/chunks/{chunk-Z7SAOCDG.js → chunk-LBWIHPN7.js} +5 -2
  12. package/dist/chunks/{chunk-Z7SAOCDG.js.map → chunk-LBWIHPN7.js.map} +2 -2
  13. package/dist/chunks/{chunk-A6KWUEA6.js → chunk-MKB2J6AD.js} +9 -1
  14. package/dist/chunks/chunk-MKB2J6AD.js.map +7 -0
  15. package/dist/chunks/{chunk-LOJLO2EY.js → chunk-S2HXJTAF.js} +1 -1
  16. package/dist/chunks/chunk-S2HXJTAF.js.map +7 -0
  17. package/dist/chunks/{chunk-GKICS7CF.js → chunk-SK3TLNUP.js} +120 -107
  18. package/dist/chunks/chunk-SK3TLNUP.js.map +7 -0
  19. package/dist/chunks/{chunk-MOCRENNU.js → chunk-SNUROC7E.js} +6 -6
  20. package/dist/chunks/{configService-TXBNUBBL.js → configService-MICL4S2L.js} +2 -2
  21. package/dist/chunks/{constants-L75FR445.js → constants-ZEU4TZCA.js} +2 -2
  22. package/dist/entries/server-router.js +7 -7
  23. package/dist/lib/client/index.js +9 -4
  24. package/dist/lib/client/index.js.map +2 -2
  25. package/dist/lib/server/index.js +76 -2966
  26. package/dist/lib/server/index.js.map +4 -4
  27. package/dist/lib/shared/index.js +3 -3
  28. package/dist/lib/test-utils/index.js +1 -1
  29. package/lib/client/core/ComponentBuilder.ts +1 -1
  30. package/lib/client/routing/Router.tsx +6 -0
  31. package/lib/client/templateEngine.test.ts +178 -0
  32. package/lib/client/templateEngine.ts +1 -2
  33. package/lib/server/astro/cmsPageEmitter.ts +3 -0
  34. package/lib/server/astro/componentEmitter.ts +60 -12
  35. package/lib/server/astro/nodeToAstro.test.ts +1101 -0
  36. package/lib/server/astro/nodeToAstro.ts +43 -4
  37. package/lib/server/astro/pageEmitter.ts +4 -0
  38. package/lib/server/index.ts +18 -4
  39. package/lib/server/services/configService.ts +12 -0
  40. package/lib/server/ssr/htmlGenerator.ts +0 -5
  41. package/lib/server/ssr/imageMetadata.ts +3 -3
  42. package/lib/server/ssr/ssrRenderer.ts +48 -19
  43. package/lib/shared/constants.ts +2 -0
  44. package/lib/shared/types/components.ts +8 -4
  45. package/lib/shared/validation/propValidator.ts +2 -1
  46. package/lib/shared/validation/schemas.ts +3 -1
  47. package/package.json +1 -1
  48. package/templates/index-router.html +0 -5
  49. package/dist/chunks/chunk-A6KWUEA6.js.map +0 -7
  50. package/dist/chunks/chunk-GKICS7CF.js.map +0 -7
  51. package/dist/chunks/chunk-LOJLO2EY.js.map +0 -7
  52. package/dist/chunks/chunk-OJ5SROQN.js.map +0 -7
  53. /package/dist/chunks/{chunk-3NOZVNM4.js.map → chunk-EQOSDQS2.js.map} +0 -0
  54. /package/dist/chunks/{chunk-MOCRENNU.js.map → chunk-SNUROC7E.js.map} +0 -0
  55. /package/dist/chunks/{configService-TXBNUBBL.js.map → configService-MICL4S2L.js.map} +0 -0
  56. /package/dist/chunks/{constants-L75FR445.js.map → constants-ZEU4TZCA.js.map} +0 -0
@@ -23,7 +23,7 @@ import {
23
23
  isValidIdentifier,
24
24
  resolvePaletteColor,
25
25
  resolveSafePath
26
- } from "../../chunks/chunk-LOJLO2EY.js";
26
+ } from "../../chunks/chunk-S2HXJTAF.js";
27
27
  import {
28
28
  CSS_PROPERTIES,
29
29
  CSS_PROPERTIES_DEFINITION,
@@ -201,7 +201,7 @@ import {
201
201
  validatePath,
202
202
  validatePropDefinition,
203
203
  validateStructuredComponentDefinition
204
- } from "../../chunks/chunk-OJ5SROQN.js";
204
+ } from "../../chunks/chunk-IGVQF5GY.js";
205
205
  import {
206
206
  DEFAULT_BREAKPOINTS,
207
207
  DEFAULT_I18N_CONFIG,
@@ -255,7 +255,7 @@ import {
255
255
  TREE_SCROLL_DELAY_MS,
256
256
  WEBSOCKET_STATES,
257
257
  init_constants
258
- } from "../../chunks/chunk-Z7SAOCDG.js";
258
+ } from "../../chunks/chunk-LBWIHPN7.js";
259
259
  import "../../chunks/chunk-KSBZ2L7C.js";
260
260
 
261
261
  // lib/shared/index.ts
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  NODE_TYPE,
3
3
  init_constants
4
- } from "../../chunks/chunk-Z7SAOCDG.js";
4
+ } from "../../chunks/chunk-LBWIHPN7.js";
5
5
  import "../../chunks/chunk-KSBZ2L7C.js";
6
6
 
7
7
  // lib/test-utils/mocks.ts
@@ -637,7 +637,7 @@ export class ComponentBuilder {
637
637
  if (utilityClasses.length > 0) {
638
638
  const existingClassName = (props.className || '') as string;
639
639
  const classArray = existingClassName ? existingClassName.split(/\s+/) : [];
640
- return { ...props, className: [...classArray, ...utilityClasses].filter(Boolean).join(' ') };
640
+ return { ...props, className: [...classArray, ...utilityClasses].filter(Boolean).join(' '), style: processedStyle };
641
641
  }
642
642
  }
643
643
 
@@ -255,6 +255,12 @@ export function Router(props: RouterProps = {}): ReactElement {
255
255
  if (typeof window === 'undefined') return;
256
256
 
257
257
  const handleMessage = (event: MessageEvent) => {
258
+ if (event.data?.type === IFRAME_MESSAGE_TYPES.CSS_VARIABLE_UPDATE) {
259
+ const { name, value } = event.data as { name: string; value: string };
260
+ document.documentElement.style.setProperty(name, value);
261
+ return;
262
+ }
263
+
258
264
  if (event.data?.type === IFRAME_MESSAGE_TYPES.INTERACTIVE_CSS_UPDATE) {
259
265
  const css = event.data.css as string;
260
266
  const styleId = 'interactive-styles';
@@ -807,3 +807,181 @@ describe("Template Engine - normalizeStyle", () => {
807
807
  expect(result).toBeNull();
808
808
  });
809
809
  });
810
+
811
+ // ==========================================================================
812
+ // Targeted tests for `as any` code paths in processStructure
813
+ // These exercise specific type-cast paths to ensure safety before refactoring
814
+ // ==========================================================================
815
+
816
+ describe("Template Engine - processStructure as-any paths", () => {
817
+ const baseContext: TemplateContext = {
818
+ props: { text: "Hello", size: "lg", variant: "primary" },
819
+ componentDef: {} as StructuredComponentDefinition,
820
+ };
821
+
822
+ describe("Boolean preservation (line 429)", () => {
823
+ test("should preserve false boolean values in structure", () => {
824
+ // processStructure should return false as-is, not convert to null
825
+ const result = processStructure(false as any, baseContext);
826
+ expect(result).toBe(false);
827
+ });
828
+
829
+ test("should preserve true boolean values in structure", () => {
830
+ const result = processStructure(true as any, baseContext);
831
+ expect(result).toBe(true);
832
+ });
833
+ });
834
+
835
+ describe("Object template evaluation returning objects (line 454)", () => {
836
+ test("should return object result from full template expression", () => {
837
+ const context: TemplateContext = {
838
+ props: { link: { href: "/about", target: "_blank" } },
839
+ componentDef: {} as StructuredComponentDefinition,
840
+ };
841
+ // Full template {{link}} should return the object as-is
842
+ const result = processStructure("{{link}}", context);
843
+ expect(result).toEqual({ href: "/about", target: "_blank" });
844
+ });
845
+ });
846
+
847
+ describe("Slot default content via 'default' property (lines 483-486, 516-517)", () => {
848
+ test("should render slot default array when no instance children", () => {
849
+ // Slot markers use type: "slot" (NODE_TYPE.SLOT)
850
+ const structure = [
851
+ { type: "node", tag: "div", children: [
852
+ { type: "slot", default: [{ type: "node", tag: "span", children: "fallback" }] }
853
+ ]}
854
+ ] as unknown as ComponentNode[];
855
+ const result = processStructure(structure, baseContext);
856
+ expect(Array.isArray(result)).toBe(true);
857
+ const root = (result as any[])[0];
858
+ expect(root.tag).toBe("div");
859
+ // The slot default should have been rendered
860
+ expect(root.children.length).toBe(1);
861
+ expect(root.children[0].tag).toBe("span");
862
+ });
863
+
864
+ test("should render slot default string when no instance children", () => {
865
+ const structure = [
866
+ { type: "node", tag: "div", children: [
867
+ { type: "slot", default: "Default text" }
868
+ ]}
869
+ ] as unknown as ComponentNode[];
870
+ const result = processStructure(structure, baseContext);
871
+ const root = (result as any[])[0];
872
+ expect(root.children[0]).toBe("Default text");
873
+ });
874
+
875
+ test("should use instance children over slot default", () => {
876
+ const structure = [
877
+ { type: "node", tag: "div", children: [
878
+ { type: "slot", default: [{ type: "node", tag: "span", children: "fallback" }] }
879
+ ]}
880
+ ] as unknown as ComponentNode[];
881
+ const instanceChildren = [
882
+ { type: "node", tag: "b", children: "override" }
883
+ ] as unknown as ComponentNode[];
884
+ const result = processStructure(structure, baseContext, undefined, instanceChildren);
885
+ const root = (result as any[])[0];
886
+ expect(root.children[0].tag).toBe("b");
887
+ });
888
+
889
+ test("should handle slot marker as standalone object with default", () => {
890
+ const slotMarker = {
891
+ type: "slot",
892
+ default: [{ type: "node", tag: "div", children: "content" }],
893
+ };
894
+ const result = processStructure(slotMarker as any, baseContext);
895
+ expect(result).toBeDefined();
896
+ });
897
+ });
898
+
899
+ describe("Plain object processing (lines 538, 543)", () => {
900
+ test("should process plain objects recursively and resolve templates", () => {
901
+ // An object without a valid node type gets treated as a plain props object
902
+ const structure = { label: "{{text}}", visible: true } as any;
903
+ const result = processStructure(structure, baseContext);
904
+ expect(result).toBeDefined();
905
+ expect((result as any).label).toBe("Hello");
906
+ expect((result as any).visible).toBe(true);
907
+ });
908
+
909
+ test("should strip null/undefined values from plain objects", () => {
910
+ const context: TemplateContext = {
911
+ props: { defined: "value" },
912
+ componentDef: {} as StructuredComponentDefinition,
913
+ };
914
+ const structure = { a: "{{defined}}", b: "{{missing}}" } as any;
915
+ const result = processStructure(structure, context);
916
+ expect((result as any).a).toBe("value");
917
+ // Missing props resolve to empty string, not null
918
+ });
919
+ });
920
+
921
+ describe("Embed node creation (line 562)", () => {
922
+ test("should process embed node with html content", () => {
923
+ const node: ComponentNode = {
924
+ type: "embed" as any,
925
+ html: "<script>alert('test')</script>",
926
+ } as any;
927
+ const result = processStructure(node, baseContext);
928
+ expect(result).toBeDefined();
929
+ expect((result as any).type).toBe("embed");
930
+ expect((result as any).html).toBe("<script>alert('test')</script>");
931
+ });
932
+ });
933
+
934
+ describe("Link node creation (line 569)", () => {
935
+ test("should process link node with href", () => {
936
+ const node = {
937
+ type: "link",
938
+ href: "/about",
939
+ children: ["About us"],
940
+ } as any;
941
+ const result = processStructure(node, baseContext);
942
+ expect(result).toBeDefined();
943
+ expect((result as any).type).toBe("link");
944
+ });
945
+
946
+ test("should process link node with template href", () => {
947
+ const context: TemplateContext = {
948
+ props: { url: "/contact" },
949
+ componentDef: {} as StructuredComponentDefinition,
950
+ };
951
+ const node = {
952
+ type: "link",
953
+ href: "{{url}}",
954
+ children: ["Contact"],
955
+ } as any;
956
+ const result = processStructure(node, context);
957
+ expect(result).toBeDefined();
958
+ expect((result as any).type).toBe("link");
959
+ });
960
+ });
961
+
962
+ describe("Locale-list node creation (line 574)", () => {
963
+ test("should process locale-list node", () => {
964
+ const node = {
965
+ type: "locale-list",
966
+ style: { display: "flex" },
967
+ } as any;
968
+ const result = processStructure(node, baseContext);
969
+ expect(result).toBeDefined();
970
+ expect((result as any).type).toBe("locale-list");
971
+ });
972
+ });
973
+
974
+ describe("List node with legacy cms-list type (line 575)", () => {
975
+ test("should process list node", () => {
976
+ const node = {
977
+ type: "list",
978
+ source: "items",
979
+ sourceType: "prop",
980
+ children: [{ type: "html" as const, tag: "div", children: [] }],
981
+ } as any;
982
+ const result = processStructure(node, baseContext);
983
+ expect(result).toBeDefined();
984
+ expect((result as any).type).toBe("list");
985
+ });
986
+ });
987
+ });
@@ -814,8 +814,7 @@ export function processStructure(
814
814
  if (isResponsiveStyle(resolvedStyle)) {
815
815
  // Apply responsive styles directly to node.style or props.style
816
816
  if (isComponentNode(processed)) {
817
- processed.props = processed.props || {};
818
- processed.props.style = resolvedStyle as ResponsiveStyleObject;
817
+ (processed as any).style = resolvedStyle as ResponsiveStyleObject;
819
818
  } else if (isHtmlNode(processed) || isEmbedNode(processed) || isLocaleListNode(processed) || isLinkNode(processed) || isListNode(processed)) {
820
819
  processed.style = resolvedStyle as ResponsiveStyleObject;
821
820
  }
@@ -54,6 +54,8 @@ export interface CMSPageEmitOptions {
54
54
  isMultiLocale: boolean;
55
55
  /** Slug mappings for translating internal link hrefs */
56
56
  slugMappings?: SlugMap[];
57
+ /** Image format: 'webp' uses plain <img>, 'avif' uses <picture> */
58
+ imageFormat?: 'webp' | 'avif';
57
59
  }
58
60
 
59
61
  // ---------------------------------------------------------------------------
@@ -295,6 +297,7 @@ export function emitCMSPage(options: CMSPageEmitOptions): string {
295
297
  cmsWrapFn: wrapFn,
296
298
  slugMappings,
297
299
  i18nDefaultLocale: i18nConfig.defaultLocale,
300
+ imageFormat: options.imageFormat,
298
301
  };
299
302
 
300
303
  // Emit the template body
@@ -76,6 +76,47 @@ function formatDefault(def: PropDefinition): string | null {
76
76
  return JSON.stringify(val);
77
77
  }
78
78
 
79
+ /**
80
+ * Merge the `className` variable onto the root element's class attribute.
81
+ * Handles: class="existing", class:list={[...]}, class={expr}, or no class at all.
82
+ */
83
+ function mergeClassNameOntoRoot(template: string): string {
84
+ // Find the first opening tag (skip whitespace/newlines)
85
+ const tagMatch = template.match(/^(\s*<)(\w[\w-]*)([\s\S]*?)(\/?>)/);
86
+ if (!tagMatch) return template;
87
+
88
+ const [fullMatch, prefix, tagName, attrs, close] = tagMatch;
89
+
90
+ // Check for existing class:list
91
+ const classListMatch = attrs.match(/\s+class:list=\{(\[[\s\S]*?\])\}/);
92
+ if (classListMatch) {
93
+ const existingList = classListMatch[1];
94
+ // Append className to the array
95
+ const newList = existingList.replace(/\]$/, `, className]`);
96
+ const newAttrs = attrs.replace(classListMatch[0], ` class:list={${newList}}`);
97
+ return prefix + tagName + newAttrs + close + template.slice(fullMatch.length);
98
+ }
99
+
100
+ // Check for existing class="..." (static)
101
+ const classStaticMatch = attrs.match(/\s+class="([^"]*)"/);
102
+ if (classStaticMatch) {
103
+ const existing = classStaticMatch[1];
104
+ const newAttrs = attrs.replace(classStaticMatch[0], ` class:list={["${existing}", className]}`);
105
+ return prefix + tagName + newAttrs + close + template.slice(fullMatch.length);
106
+ }
107
+
108
+ // Check for existing class={expr} (dynamic)
109
+ const classDynMatch = attrs.match(/\s+class=\{([^}]+)\}/);
110
+ if (classDynMatch) {
111
+ const expr = classDynMatch[1];
112
+ const newAttrs = attrs.replace(classDynMatch[0], ` class:list={[${expr}, className]}`);
113
+ return prefix + tagName + newAttrs + close + template.slice(fullMatch.length);
114
+ }
115
+
116
+ // No existing class — add class={className}
117
+ return prefix + tagName + ` class={className}` + attrs + close + template.slice(fullMatch.length);
118
+ }
119
+
79
120
  // ---------------------------------------------------------------------------
80
121
  // Main emitter
81
122
  // ---------------------------------------------------------------------------
@@ -115,9 +156,12 @@ export function emitAstroComponent(
115
156
  };
116
157
 
117
158
  // Emit the template body
118
- const templateBody = nodeToAstro(structure, ctx);
159
+ let templateBody = nodeToAstro(structure, ctx);
119
160
 
120
- // Build frontmatter
161
+ // Merge instance className onto the root element (for acceptsStyles support)
162
+ templateBody = mergeClassNameOntoRoot(templateBody);
163
+
164
+ // Build frontmatter (includes class prop for instance style support)
121
165
  const frontmatter = buildFrontmatter(name, propDefs, ctx.imports, ctx.dynamicTags, ctx.needsI18nResolver ? defaultLocale : undefined);
122
166
 
123
167
  // Build style/script sections
@@ -150,7 +194,8 @@ function buildFrontmatter(
150
194
 
151
195
  const propEntries = Object.entries(propDefs);
152
196
 
153
- if (propEntries.length > 0) {
197
+ // Always generate Props interface and destructuring (at minimum for class prop)
198
+ {
154
199
  // Interface
155
200
  lines.push('interface Props {');
156
201
  for (const [propName, propDef] of propEntries) {
@@ -159,6 +204,8 @@ function buildFrontmatter(
159
204
  const optional = 'default' in propDef && propDef.default !== undefined;
160
205
  lines.push(` ${propName}${optional ? '?' : ''}: ${tsType};`);
161
206
  }
207
+ // Always include class prop for instance style support
208
+ lines.push(' class?: string;');
162
209
  lines.push('}');
163
210
  lines.push('');
164
211
 
@@ -176,16 +223,17 @@ function buildFrontmatter(
176
223
  }
177
224
  }
178
225
 
179
- if (destructParts.length > 0) {
180
- if (destructParts.length <= 3 && destructParts.join(', ').length < 80) {
181
- lines.push(`const { ${destructParts.join(', ')} } = Astro.props;`);
182
- } else {
183
- lines.push('const {');
184
- for (const part of destructParts) {
185
- lines.push(` ${part},`);
186
- }
187
- lines.push('} = Astro.props;');
226
+ // Always include class prop (renamed to className to avoid reserved word)
227
+ destructParts.push('class: className = ""');
228
+
229
+ if (destructParts.length <= 3 && destructParts.join(', ').length < 80) {
230
+ lines.push(`const { ${destructParts.join(', ')} } = Astro.props;`);
231
+ } else {
232
+ lines.push('const {');
233
+ for (const part of destructParts) {
234
+ lines.push(` ${part},`);
188
235
  }
236
+ lines.push('} = Astro.props;');
189
237
  }
190
238
  }
191
239