meno-core 1.0.46 → 1.0.48

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 (68) hide show
  1. package/build-astro.ts +13 -77
  2. package/dist/build-static.js +7 -7
  3. package/dist/chunks/{chunk-FED5MME6.js → chunk-3FHJUHAS.js} +5 -4
  4. package/dist/chunks/chunk-3FHJUHAS.js.map +7 -0
  5. package/dist/chunks/{chunk-XSWR3QLI.js → chunk-B2RTLDXY.js} +130 -130
  6. package/dist/chunks/chunk-B2RTLDXY.js.map +7 -0
  7. package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-BJRKEPMP.js} +2 -2
  8. package/dist/chunks/{chunk-ORN7S4AP.js → chunk-D5E3OKSL.js} +5 -5
  9. package/dist/chunks/{chunk-KITQJYZV.js → chunk-EK4KESLU.js} +33 -3
  10. package/dist/chunks/chunk-EK4KESLU.js.map +7 -0
  11. package/dist/chunks/{chunk-C6U5T5S5.js → chunk-NKUV77SR.js} +43 -10
  12. package/dist/chunks/chunk-NKUV77SR.js.map +7 -0
  13. package/dist/chunks/{chunk-ZWYDT3QJ.js → chunk-NP76N4HQ.js} +4 -4
  14. package/dist/chunks/{chunk-ZWYDT3QJ.js.map → chunk-NP76N4HQ.js.map} +2 -2
  15. package/dist/chunks/{chunk-IF3RATBY.js → chunk-RQSTH2BS.js} +2 -2
  16. package/dist/chunks/{chunk-77ZB6353.js → chunk-TPQ7APVQ.js} +42 -86
  17. package/dist/chunks/chunk-TPQ7APVQ.js.map +7 -0
  18. package/dist/chunks/{configService-DYCUEURL.js → configService-IGJEC3MC.js} +3 -3
  19. package/dist/entries/server-router.js +8 -8
  20. package/dist/lib/client/index.js +16 -4
  21. package/dist/lib/client/index.js.map +2 -2
  22. package/dist/lib/server/index.js +1803 -383
  23. package/dist/lib/server/index.js.map +4 -4
  24. package/dist/lib/shared/index.js +8 -4
  25. package/dist/lib/shared/index.js.map +1 -1
  26. package/lib/client/core/ComponentBuilder.test.ts +38 -0
  27. package/lib/client/core/ComponentBuilder.ts +25 -1
  28. package/lib/server/astro/astroEmitHelpers.ts +5 -0
  29. package/lib/server/astro/cmsPageEmitter.ts +15 -2
  30. package/lib/server/astro/componentEmitter.ts +13 -5
  31. package/lib/server/astro/nodeToAstro.ts +23 -9
  32. package/lib/server/astro/pageEmitter.ts +15 -2
  33. package/lib/server/index.ts +1 -1
  34. package/lib/server/jsonLoader.test.ts +0 -17
  35. package/lib/server/jsonLoader.ts +0 -81
  36. package/lib/server/routes/api/variables.ts +4 -2
  37. package/lib/server/ssr/htmlGenerator.test.ts +4 -4
  38. package/lib/server/ssr/htmlGenerator.ts +7 -2
  39. package/lib/server/ssr/imageMetadata.ts +15 -9
  40. package/lib/server/ssr/ssrRenderer.test.ts +103 -1
  41. package/lib/server/ssr/ssrRenderer.ts +41 -35
  42. package/lib/server/utils/jsonLineMapper.test.ts +53 -1
  43. package/lib/server/utils/jsonLineMapper.ts +43 -3
  44. package/lib/server/webflow/buildWebflow.ts +343 -123
  45. package/lib/server/webflow/index.ts +1 -0
  46. package/lib/server/webflow/nodeToWebflow.test.ts +3170 -0
  47. package/lib/server/webflow/nodeToWebflow.ts +2141 -129
  48. package/lib/server/webflow/styleMapper.test.ts +389 -0
  49. package/lib/server/webflow/styleMapper.ts +517 -63
  50. package/lib/server/webflow/templateWrapper.ts +49 -0
  51. package/lib/server/webflow/types.ts +218 -18
  52. package/lib/shared/elementClassName.test.ts +15 -0
  53. package/lib/shared/elementClassName.ts +7 -3
  54. package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
  55. package/lib/shared/styleNodeUtils.test.ts +47 -1
  56. package/lib/shared/styleNodeUtils.ts +7 -7
  57. package/lib/shared/types/index.ts +1 -0
  58. package/lib/shared/types/variables.ts +37 -0
  59. package/package.json +1 -1
  60. package/dist/chunks/chunk-77ZB6353.js.map +0 -7
  61. package/dist/chunks/chunk-C6U5T5S5.js.map +0 -7
  62. package/dist/chunks/chunk-FED5MME6.js.map +0 -7
  63. package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
  64. package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
  65. /package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-BJRKEPMP.js.map} +0 -0
  66. /package/dist/chunks/{chunk-ORN7S4AP.js.map → chunk-D5E3OKSL.js.map} +0 -0
  67. /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-RQSTH2BS.js.map} +0 -0
  68. /package/dist/chunks/{configService-DYCUEURL.js.map → configService-IGJEC3MC.js.map} +0 -0
@@ -20,6 +20,7 @@ import { isComponentNode, isHtmlNode, isLinkNode, isEmbedNode, isLocaleListNode,
20
20
  import type { ListNode } from '../../shared/registry/nodeTypes/ListNodeType';
21
21
  import type { CMSService } from '../services/cmsService';
22
22
  import { extractAttributesFromNode, skipEmptyTemplateAttributes } from '../../shared/attributeNodeUtils';
23
+ import { mergeNodeStyles } from '../../shared/styleNodeUtils';
23
24
  import { SSRRegistry } from '../../shared/registry/SSRRegistry';
24
25
  import { mergeResponsiveStyles } from '../../shared/responsiveStyleUtils';
25
26
  // Lazy-loaded: jsdom (used by isomorphic-dompurify on the server) can't be bundled by esbuild
@@ -242,6 +243,14 @@ function evaluateIfCondition(node: ComponentNode, ctx: SSRContext): boolean {
242
243
  resolved = processCMSTemplate(resolved, ctx.cmsContext.cms, ctx.locale, ctx.i18nConfig);
243
244
  }
244
245
 
246
+ // Unresolved template (e.g. {{prop}} where the prop is undefined and
247
+ // processStructure preserved the original template per evaluateTemplate's
248
+ // backward-compat fallback) → treat as falsy so missing context means
249
+ // "don't render", matching `if: false` and unset BooleanMapping values.
250
+ if (resolved.includes('{{') && resolved.includes('}}')) {
251
+ return false;
252
+ }
253
+
245
254
  // Evaluate truthiness - false, 0, empty string are falsy
246
255
  return Boolean(resolved) && resolved !== 'false' && resolved !== '0' && resolved !== '';
247
256
  }
@@ -1112,13 +1121,25 @@ async function renderNode(
1112
1121
  // Convert styles to utility classes instead of inline styles
1113
1122
  let utilityClasses: string[] = [];
1114
1123
  let resolvedStyle: StyleObject = {};
1124
+ // For custom component instances, defer style→class conversion: forward the
1125
+ // style object so renderComponent can deep-merge it into the structure root,
1126
+ // then let the recursive renderNode call generate classes from the merged
1127
+ // result. This matches the client (ComponentBuilder.mergeNodeStyles) so that
1128
+ // an instance override of e.g. marginBottom actually replaces the structure's
1129
+ // marginBottom instead of fighting it as a same-specificity utility class.
1130
+ const isCustomComponentNode = nodeType === NODE_TYPE.COMPONENT && isComponentNode(node) && ssrComponentRegistry.has(node.component);
1131
+ let deferredComponentStyle: StyleObject | ResponsiveStyleObject | undefined;
1115
1132
 
1116
1133
  if (nodeStyle) {
1117
- // Validate that all styles can generate utility classes (build-time warnings)
1118
- validateStyleCoverage(nodeStyle, `Node: ${nodeType || 'unknown'}`);
1134
+ if (isCustomComponentNode) {
1135
+ deferredComponentStyle = nodeStyle;
1136
+ } else {
1137
+ // Validate that all styles can generate utility classes (build-time warnings)
1138
+ validateStyleCoverage(nodeStyle, `Node: ${nodeType || 'unknown'}`);
1119
1139
 
1120
- // Convert style object to utility class names (process templates in style values)
1121
- utilityClasses = processStyleToClasses(nodeStyle, ctx);
1140
+ // Convert style object to utility class names (process templates in style values)
1141
+ utilityClasses = processStyleToClasses(nodeStyle, ctx);
1142
+ }
1122
1143
  } else if (nodeProps.style) {
1123
1144
  // If no node.style but props have style, keep it for backward compatibility
1124
1145
  if (isResponsiveStyle(nodeProps.style) && breakpoints && viewportWidth) {
@@ -1164,7 +1185,9 @@ async function renderNode(
1164
1185
  ...nodeProps,
1165
1186
  ...nodeAttributesWithoutClass,
1166
1187
  ...(mergedClassName ? { className: mergedClassName } : {}),
1167
- ...(Object.keys(resolvedStyle).length > 0 ? { style: resolvedStyle } : {})
1188
+ ...(deferredComponentStyle
1189
+ ? { style: deferredComponentStyle }
1190
+ : Object.keys(resolvedStyle).length > 0 ? { style: resolvedStyle } : {})
1168
1191
  };
1169
1192
 
1170
1193
  // Check if this is a custom component
@@ -1243,42 +1266,25 @@ async function renderComponent(
1243
1266
 
1244
1267
  // Merge instance style overrides, className, and attributes
1245
1268
  // processedStructure is typically an HTML node (the component's root element)
1246
- // Handle both component nodes and HTML nodes
1269
+ // Handle component nodes, HTML nodes, and Link nodes (Link and HTML share top-level style/attributes)
1247
1270
  const rootNode = processedStructure as ComponentNode & { props?: Record<string, unknown>; style?: any; attributes?: Record<string, string | number | boolean> };
1248
- if (isComponentNode(rootNode) || isHtmlNode(rootNode)) {
1271
+ if (isComponentNode(rootNode) || isHtmlNode(rootNode) || isLinkNode(rootNode)) {
1249
1272
  if (!rootNode.props) {
1250
1273
  rootNode.props = {};
1251
1274
  }
1252
1275
 
1253
- // For HTML root nodes, merge styles into node.style (where the renderer reads them)
1254
- if (isHtmlNode(rootNode)) {
1255
- if (propsWithStyleAndAttrs.style) {
1256
- const existingStyle = rootNode.style;
1257
- if (existingStyle && typeof existingStyle === 'object') {
1258
- rootNode.style = {
1259
- ...(existingStyle as Record<string, any>),
1260
- ...(propsWithStyleAndAttrs.style as Record<string, any>)
1261
- };
1262
- } else {
1263
- rootNode.style = propsWithStyleAndAttrs.style as StyleObject | ResponsiveStyleObject;
1264
- }
1265
- }
1266
- } else {
1267
- // For component root nodes, merge into props.style
1268
- if (rootNode.props.style && typeof rootNode.props.style === 'object') {
1269
- rootNode.props.style = {
1270
- ...(rootNode.props.style as Record<string, unknown>),
1271
- ...(propsWithStyleAndAttrs.style as Record<string, unknown> || {})
1272
- };
1273
- } else if (propsWithStyleAndAttrs.style) {
1274
- rootNode.props.style = propsWithStyleAndAttrs.style;
1275
- }
1276
+ // Deep-merge instance styles into the structure root (per-breakpoint for
1277
+ // responsive styles), so an instance override of a property actually
1278
+ // replaces the structure's value for that property instead of producing a
1279
+ // competing utility class on the same element.
1280
+ if (propsWithStyleAndAttrs.style) {
1281
+ mergeNodeStyles(rootNode, propsWithStyleAndAttrs.style as StyleObject | ResponsiveStyleObject);
1276
1282
  }
1277
1283
 
1278
1284
  // Merge className from component instance (includes responsive utility classes)
1279
1285
  if (propsWithStyleAndAttrs.className) {
1280
- if (isHtmlNode(rootNode)) {
1281
- // For HTML root nodes, merge into attributes where the renderer reads them
1286
+ if (isHtmlNode(rootNode) || isLinkNode(rootNode)) {
1287
+ // For HTML and Link root nodes, merge into attributes where the renderer reads them
1282
1288
  if (!rootNode.attributes) rootNode.attributes = {};
1283
1289
  const existingClass = (rootNode.attributes.class || '') as string;
1284
1290
  rootNode.attributes.class = existingClass
@@ -1293,11 +1299,11 @@ async function renderComponent(
1293
1299
  }
1294
1300
 
1295
1301
  // Merge attributes into props
1296
- Object.assign(rootNode.props, nodeAttributes);
1302
+ Object.assign(rootNode.props as Record<string, unknown>, nodeAttributes);
1297
1303
 
1298
- // Forward nodeAttributes to rootNode.attributes for HTML root nodes
1304
+ // Forward nodeAttributes to rootNode.attributes for HTML/Link root nodes
1299
1305
  // so attributes from outer components (e.g. data-component) reach the final HTML element
1300
- if (isHtmlNode(rootNode) && Object.keys(nodeAttributes).length > 0) {
1306
+ if ((isHtmlNode(rootNode) || isLinkNode(rootNode)) && Object.keys(nodeAttributes).length > 0) {
1301
1307
  if (!rootNode.attributes) {
1302
1308
  rootNode.attributes = {};
1303
1309
  }
@@ -70,7 +70,27 @@ describe("jsonLineMapper", () => {
70
70
  }`;
71
71
 
72
72
  const lineMap = buildLineMap(json);
73
- expect(lineMap.size).toBe(0);
73
+ // Only the root entry (empty-string key) is recorded; no children.
74
+ expect(lineMap.size).toBe(1);
75
+ expect(lineMap.get("")).toBeDefined();
76
+ });
77
+
78
+ test("records the root's own line range under the empty-string key", () => {
79
+ const json = `{
80
+ "root": {
81
+ "type": "node",
82
+ "children": [
83
+ {
84
+ "type": "node",
85
+ "tag": "div"
86
+ }
87
+ ]
88
+ }
89
+ }`;
90
+
91
+ const lineMap = buildLineMap(json);
92
+ // Root value starts at line 2 and closes at line 10.
93
+ expect(lineMap.get("")).toEqual({ startLine: 2, endLine: 10 });
74
94
  });
75
95
 
76
96
  test("should handle JSON without root", () => {
@@ -83,6 +103,38 @@ describe("jsonLineMapper", () => {
83
103
  const lineMap = buildLineMap(json);
84
104
  expect(lineMap.size).toBe(0);
85
105
  });
106
+
107
+ test("tracks component format via component.structure", () => {
108
+ const json = `{
109
+ "component": {
110
+ "interface": {
111
+ "size": { "type": "text" }
112
+ },
113
+ "structure": {
114
+ "type": "node",
115
+ "tag": "h1",
116
+ "children": [
117
+ {
118
+ "type": "node",
119
+ "tag": "span"
120
+ },
121
+ {
122
+ "type": "component",
123
+ "component": "Icon"
124
+ }
125
+ ]
126
+ }
127
+ }
128
+ }`;
129
+
130
+ const lineMap = buildLineMap(json);
131
+ // Structure spans from its opening `{` (line 6) to its closing `}` (line 19).
132
+ expect(lineMap.get("")).toEqual({ startLine: 6, endLine: 19 });
133
+ // First child: the <span>.
134
+ expect(lineMap.get("0")?.startLine).toBe(10);
135
+ // Second child: the Icon component instance.
136
+ expect(lineMap.get("1")?.startLine).toBe(14);
137
+ });
86
138
  });
87
139
 
88
140
  describe("lineMapToObject", () => {
@@ -86,6 +86,16 @@ export function buildLineMap(jsonText: string): LineMap {
86
86
  return { start: startPos, end: pos };
87
87
  }
88
88
 
89
+ function recordTrackedRoot(path: number[]): void {
90
+ // Parse the root element of the tracked structure and record its own line range
91
+ // under the empty-string key, so callers can resolve the root selection itself.
92
+ const { start, end } = parseValue(path, true);
93
+ lineMap.set('', {
94
+ startLine: charToLine[start],
95
+ endLine: charToLine[end - 1] || charToLine[start],
96
+ });
97
+ }
98
+
89
99
  function parseObject(path: number[], trackChildren: boolean): void {
90
100
  pos++; // skip {
91
101
  skipWhitespace();
@@ -98,10 +108,13 @@ export function buildLineMap(jsonText: string): LineMap {
98
108
  skipWhitespace();
99
109
 
100
110
  if (key === 'root' && path.length === 0) {
101
- // Track root object - children inside will be tracked
102
- parseValue(path, true);
111
+ // Page format: track from `root`.
112
+ recordTrackedRoot(path);
113
+ } else if (key === 'component' && path.length === 0) {
114
+ // Component format: descend into the component object looking for `structure`.
115
+ parseComponentWrapper(path);
103
116
  } else if (key === 'children' && trackChildren) {
104
- // This is a children array - track each child with indices
117
+ // Children array track each element with indices
105
118
  parseValue(path, true);
106
119
  } else {
107
120
  // Regular property - don't track individual items
@@ -115,6 +128,33 @@ export function buildLineMap(jsonText: string): LineMap {
115
128
  pos++; // skip }
116
129
  }
117
130
 
131
+ function parseComponentWrapper(path: number[]): void {
132
+ // We expect an object value for the `component` key. If it isn't one, just skip it.
133
+ if (jsonText[pos] !== '{') {
134
+ parseValue(path, false);
135
+ return;
136
+ }
137
+ pos++; // skip {
138
+ skipWhitespace();
139
+ while (pos < jsonText.length && jsonText[pos] !== '}') {
140
+ skipWhitespace();
141
+ const key = parseString();
142
+ skipWhitespace();
143
+ pos++; // skip :
144
+ skipWhitespace();
145
+ if (key === 'structure') {
146
+ // The component's structure is the root of what we track — same as `root` for pages.
147
+ recordTrackedRoot(path);
148
+ } else {
149
+ parseValue(path, false);
150
+ }
151
+ skipWhitespace();
152
+ if (jsonText[pos] === ',') pos++;
153
+ skipWhitespace();
154
+ }
155
+ pos++; // skip }
156
+ }
157
+
118
158
  function parseArray(path: number[], trackChildren: boolean): void {
119
159
  pos++; // skip [
120
160
  skipWhitespace();