meno-core 1.0.6 ā 1.0.8
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/build-static.ts +54 -3
- package/lib/client/core/ComponentBuilder.ts +19 -3
- package/lib/client/core/builders/cmsListBuilder.ts +39 -4
- package/lib/client/core/builders/embedBuilder.ts +38 -3
- package/lib/client/core/builders/localeListBuilder.ts +38 -3
- package/lib/client/core/builders/objectLinkBuilder.ts +38 -3
- package/lib/client/core/cmsTemplateProcessor.ts +13 -1
- package/lib/client/routing/Router.tsx +43 -3
- package/lib/client/templateEngine.ts +21 -6
- package/lib/server/fileWatcher.ts +34 -2
- package/lib/server/routes/pages.ts +16 -8
- package/lib/server/services/cmsService.test.ts +182 -0
- package/lib/server/services/cmsService.ts +57 -2
- package/lib/server/services/fileWatcherService.ts +4 -0
- package/lib/server/ssr/cmsSSRProcessor.ts +13 -1
- package/lib/server/ssr/htmlGenerator.ts +15 -1
- package/lib/server/ssr/ssrRenderer.ts +96 -13
- package/lib/server/websocketManager.ts +10 -0
- package/lib/shared/constants.ts +6 -0
- package/lib/shared/cssGeneration.test.ts +52 -0
- package/lib/shared/cssGeneration.ts +18 -0
- package/lib/shared/fontLoader.ts +1 -0
- package/lib/shared/responsiveScaling.test.ts +16 -3
- package/lib/shared/responsiveScaling.ts +4 -0
- package/lib/shared/types/api.ts +2 -1
- package/lib/shared/utilityClassConfig.ts +1 -0
- package/package.json +1 -1
package/build-static.ts
CHANGED
|
@@ -150,6 +150,37 @@ function getDisplayPath(
|
|
|
150
150
|
return slug === "" ? `/${locale}` : `/${locale}/${slug}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Generate robots.txt with sensible defaults
|
|
155
|
+
*/
|
|
156
|
+
async function generateRobotsTxt(siteUrl: string, distDir: string): Promise<void> {
|
|
157
|
+
const content = `User-agent: *
|
|
158
|
+
Allow: /
|
|
159
|
+
|
|
160
|
+
Sitemap: ${siteUrl}/sitemap.xml
|
|
161
|
+
`;
|
|
162
|
+
await writeFile(join(distDir, 'robots.txt'), content, 'utf-8');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate sitemap.xml from collected URLs
|
|
167
|
+
*/
|
|
168
|
+
async function generateSitemap(urls: string[], siteUrl: string, distDir: string): Promise<void> {
|
|
169
|
+
// Sort URLs for deterministic output
|
|
170
|
+
const sortedUrls = [...urls].sort();
|
|
171
|
+
|
|
172
|
+
const urlEntries = sortedUrls
|
|
173
|
+
.map(url => ` <url><loc>${siteUrl}${url}</loc></url>`)
|
|
174
|
+
.join('\n');
|
|
175
|
+
|
|
176
|
+
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
177
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
178
|
+
${urlEntries}
|
|
179
|
+
</urlset>
|
|
180
|
+
`;
|
|
181
|
+
await writeFile(join(distDir, 'sitemap.xml'), content, 'utf-8');
|
|
182
|
+
}
|
|
183
|
+
|
|
153
184
|
/**
|
|
154
185
|
* Clean dist directory, keeping only production files
|
|
155
186
|
*/
|
|
@@ -232,7 +263,8 @@ async function buildCMSTemplates(
|
|
|
232
263
|
i18nConfig: I18nConfig,
|
|
233
264
|
slugMappings: SlugMap[],
|
|
234
265
|
distDir: string,
|
|
235
|
-
cmsService: CMSService
|
|
266
|
+
cmsService: CMSService,
|
|
267
|
+
generatedUrls: Set<string>
|
|
236
268
|
): Promise<{ success: number; errors: number }> {
|
|
237
269
|
let successCount = 0;
|
|
238
270
|
let errorCount = 0;
|
|
@@ -313,6 +345,7 @@ async function buildCMSTemplates(
|
|
|
313
345
|
await writeFile(outputPath, finalHtml, 'utf-8');
|
|
314
346
|
|
|
315
347
|
const displayPath = locale === i18nConfig.defaultLocale ? itemPath : `/${locale}${itemPath}`;
|
|
348
|
+
generatedUrls.add(displayPath);
|
|
316
349
|
console.log(` ā
${displayPath}`);
|
|
317
350
|
successCount++;
|
|
318
351
|
}
|
|
@@ -333,7 +366,11 @@ async function buildStaticPages(): Promise<void> {
|
|
|
333
366
|
console.log("šļø Building static HTML files...\n");
|
|
334
367
|
|
|
335
368
|
// Load project config first
|
|
336
|
-
await loadProjectConfig();
|
|
369
|
+
const projectConfig = await loadProjectConfig();
|
|
370
|
+
const siteUrl = (projectConfig as { siteUrl?: string }).siteUrl?.replace(/\/$/, ''); // Remove trailing slash
|
|
371
|
+
|
|
372
|
+
// Track all generated URLs for sitemap
|
|
373
|
+
const generatedUrls = new Set<string>();
|
|
337
374
|
|
|
338
375
|
// Load i18n config for multi-locale build
|
|
339
376
|
const i18nConfig = await loadI18nConfig();
|
|
@@ -483,6 +520,7 @@ async function buildStaticPages(): Promise<void> {
|
|
|
483
520
|
|
|
484
521
|
await writeFile(outputPath, finalHtml, "utf-8");
|
|
485
522
|
|
|
523
|
+
generatedUrls.add(urlPath);
|
|
486
524
|
console.log(`ā
Built: ${urlPath} ā ${outputPath}`);
|
|
487
525
|
successCount++;
|
|
488
526
|
}
|
|
@@ -501,11 +539,21 @@ async function buildStaticPages(): Promise<void> {
|
|
|
501
539
|
i18nConfig,
|
|
502
540
|
slugMappings,
|
|
503
541
|
distDir,
|
|
504
|
-
cmsService
|
|
542
|
+
cmsService,
|
|
543
|
+
generatedUrls
|
|
505
544
|
);
|
|
506
545
|
successCount += cmsResult.success;
|
|
507
546
|
errorCount += cmsResult.errors;
|
|
508
547
|
|
|
548
|
+
// Generate SEO files (robots.txt and sitemap.xml)
|
|
549
|
+
if (siteUrl) {
|
|
550
|
+
await generateRobotsTxt(siteUrl, distDir);
|
|
551
|
+
await generateSitemap([...generatedUrls], siteUrl, distDir);
|
|
552
|
+
console.log(`\nš SEO files generated (robots.txt, sitemap.xml)`);
|
|
553
|
+
} else {
|
|
554
|
+
console.warn(`\nā ļø Skipping SEO files: siteUrl not configured in project.config.json`);
|
|
555
|
+
}
|
|
556
|
+
|
|
509
557
|
console.log("\n" + "=".repeat(50));
|
|
510
558
|
console.log(`⨠Build complete!`);
|
|
511
559
|
console.log(` ā
Success: ${successCount}`);
|
|
@@ -520,6 +568,9 @@ async function buildStaticPages(): Promise<void> {
|
|
|
520
568
|
if (existsSync(functionsDir)) {
|
|
521
569
|
console.log(` - functions/ (Cloudflare Pages Functions)`);
|
|
522
570
|
}
|
|
571
|
+
if (siteUrl) {
|
|
572
|
+
console.log(` - robots.txt, sitemap.xml (SEO)`);
|
|
573
|
+
}
|
|
523
574
|
console.log(` - No React, no client-router ā`);
|
|
524
575
|
}
|
|
525
576
|
|
|
@@ -26,7 +26,7 @@ import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, type V
|
|
|
26
26
|
import { DEFAULT_I18N_CONFIG, resolveI18nValue } from "../../shared/i18n";
|
|
27
27
|
import { getChildPath, pathToString } from "../../shared/pathArrayUtils";
|
|
28
28
|
import { responsiveStylesToClasses } from "../../shared/utilityClassMapper";
|
|
29
|
-
import { processCMSTemplate, processCMSPropsTemplate } from "./cmsTemplateProcessor";
|
|
29
|
+
import { processCMSTemplate, processCMSPropsTemplate, RAW_HTML_PREFIX } from "./cmsTemplateProcessor";
|
|
30
30
|
import type { PrefetchService } from "../services/PrefetchService";
|
|
31
31
|
import { generateElementClassName, type ElementClassContext } from "../../shared/elementClassName";
|
|
32
32
|
import type { InteractiveStyles } from "../../shared/types/styles";
|
|
@@ -318,8 +318,10 @@ export class ComponentBuilder {
|
|
|
318
318
|
|
|
319
319
|
/**
|
|
320
320
|
* Process text node with CMS and item templates
|
|
321
|
+
* Returns a ReactElement with dangerouslySetInnerHTML for raw HTML content (rich-text),
|
|
322
|
+
* or a plain string for regular text content.
|
|
321
323
|
*/
|
|
322
|
-
private processTextNode(text: string, ctx: BuilderContext): string {
|
|
324
|
+
private processTextNode(text: string, ctx: BuilderContext): string | ReactElement {
|
|
323
325
|
let result = text;
|
|
324
326
|
|
|
325
327
|
// Process CMS templates
|
|
@@ -338,6 +340,12 @@ export class ComponentBuilder {
|
|
|
338
340
|
result = processItemTemplate(result, effectiveTemplateContext, i18nResolver);
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
// Check for raw HTML marker (from rich-text fields) - render with dangerouslySetInnerHTML
|
|
344
|
+
if (result.startsWith(RAW_HTML_PREFIX)) {
|
|
345
|
+
const rawHtml = result.slice(RAW_HTML_PREFIX.length);
|
|
346
|
+
return h('span', { dangerouslySetInnerHTML: { __html: rawHtml } });
|
|
347
|
+
}
|
|
348
|
+
|
|
341
349
|
return result;
|
|
342
350
|
}
|
|
343
351
|
|
|
@@ -536,7 +544,15 @@ export class ComponentBuilder {
|
|
|
536
544
|
delete extractedAttributes.className;
|
|
537
545
|
}
|
|
538
546
|
|
|
539
|
-
|
|
547
|
+
// Convert boolean true to empty string for React compatibility
|
|
548
|
+
// React strips boolean true for non-standard attributes, but SSR outputs them as presence-only
|
|
549
|
+
// Using empty string makes React render the attribute (e.g., cmsrt="" matches [cmsrt] selector)
|
|
550
|
+
const normalizedAttributes: Record<string, unknown> = {};
|
|
551
|
+
for (const [key, value] of Object.entries(extractedAttributes)) {
|
|
552
|
+
normalizedAttributes[key] = value === true ? '' : value;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return { ...result, ...normalizedAttributes };
|
|
540
556
|
}
|
|
541
557
|
|
|
542
558
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { createElement as h } from "react";
|
|
7
7
|
import type { ReactElement } from "react";
|
|
8
8
|
import type { CMSListNode, CMSItem } from "../../../shared/types/cms";
|
|
9
|
-
import type { InteractiveStyles } from "../../../shared/types";
|
|
9
|
+
import type { InteractiveStyles, StyleObject, ResponsiveStyleObject } from "../../../shared/types";
|
|
10
10
|
import { singularize } from "../../../shared/types/cms";
|
|
11
11
|
import { buildTemplateContext, resolveItemsTemplate } from "../../../shared/itemTemplateUtils";
|
|
12
12
|
import { extractAttributesFromNode } from "../../../shared/attributeNodeUtils";
|
|
@@ -14,6 +14,7 @@ import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
|
14
14
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
15
15
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
16
16
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
17
|
+
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
17
18
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
18
19
|
import type { BuilderContext, BuildResult, BuildChildrenContext } from "./types";
|
|
19
20
|
|
|
@@ -60,7 +61,18 @@ export function buildCMSList(
|
|
|
60
61
|
'data-element-path': pathToString(elementPath),
|
|
61
62
|
'data-cms-list': 'true',
|
|
62
63
|
'data-collection': node.collection || '',
|
|
63
|
-
ref: (el: HTMLElement | null) =>
|
|
64
|
+
ref: (el: HTMLElement | null) => {
|
|
65
|
+
deps.elementRegistry.register(elementPath, el, effectiveParentComponentName, false);
|
|
66
|
+
// Apply CSS variables for interactive styles
|
|
67
|
+
if (el) {
|
|
68
|
+
const cssVariables = InteractiveStylesRegistry.getVariables(elementPath);
|
|
69
|
+
if (cssVariables && Object.keys(cssVariables).length > 0) {
|
|
70
|
+
for (const [varName, value] of Object.entries(cssVariables)) {
|
|
71
|
+
el.style.setProperty(varName, value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
64
76
|
};
|
|
65
77
|
|
|
66
78
|
// Start building className
|
|
@@ -97,9 +109,32 @@ export function buildCMSList(
|
|
|
97
109
|
// Prepend element class
|
|
98
110
|
classNames.unshift(elementClass);
|
|
99
111
|
|
|
100
|
-
// Register interactive styles
|
|
112
|
+
// Register interactive styles with mapping support
|
|
101
113
|
if (nodeInteractiveStyles && nodeInteractiveStyles.length > 0) {
|
|
102
|
-
|
|
114
|
+
if (ctx.componentResolvedProps && hasInteractiveStyleMappings(nodeInteractiveStyles)) {
|
|
115
|
+
const { resolvedStyles, mappings } = extractInteractiveStyleMappings(nodeInteractiveStyles);
|
|
116
|
+
const cssVariables = resolveExtractedMappings(mappings, ctx.componentResolvedProps);
|
|
117
|
+
InteractiveStylesRegistry.set(elementClass, resolvedStyles);
|
|
118
|
+
if (Object.keys(cssVariables).length > 0) {
|
|
119
|
+
InteractiveStylesRegistry.setVariables(elementPath, cssVariables);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
InteractiveStylesRegistry.set(elementClass, nodeInteractiveStyles);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Apply preview classes when previewProp is set and truthy
|
|
126
|
+
if (ctx.componentResolvedProps) {
|
|
127
|
+
const previewClasses: string[] = [];
|
|
128
|
+
for (const rule of nodeInteractiveStyles) {
|
|
129
|
+
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
130
|
+
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
131
|
+
previewClasses.push(...styleClasses);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (previewClasses.length > 0) {
|
|
135
|
+
classNames.push(...previewClasses);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
103
138
|
}
|
|
104
139
|
}
|
|
105
140
|
|
|
@@ -11,6 +11,7 @@ import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
|
11
11
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
12
12
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
13
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
14
|
+
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
14
15
|
import DOMPurify from "isomorphic-dompurify";
|
|
15
16
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
16
17
|
import type { BuilderContext } from "./types";
|
|
@@ -60,7 +61,18 @@ export function buildEmbed(
|
|
|
60
61
|
key,
|
|
61
62
|
'data-element-path': pathToString(elementPath),
|
|
62
63
|
'data-embed-node': 'true',
|
|
63
|
-
ref: (el: HTMLElement | null) =>
|
|
64
|
+
ref: (el: HTMLElement | null) => {
|
|
65
|
+
deps.elementRegistry.register(elementPath, el, effectiveParentComponentName, false);
|
|
66
|
+
// Apply CSS variables for interactive styles
|
|
67
|
+
if (el) {
|
|
68
|
+
const cssVariables = InteractiveStylesRegistry.getVariables(elementPath);
|
|
69
|
+
if (cssVariables && Object.keys(cssVariables).length > 0) {
|
|
70
|
+
for (const [varName, value] of Object.entries(cssVariables)) {
|
|
71
|
+
el.style.setProperty(varName, value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
64
76
|
dangerouslySetInnerHTML: { __html: sanitizedHtml }
|
|
65
77
|
};
|
|
66
78
|
|
|
@@ -103,9 +115,32 @@ export function buildEmbed(
|
|
|
103
115
|
// Prepend element class
|
|
104
116
|
classNames.unshift(elementClass);
|
|
105
117
|
|
|
106
|
-
// Register interactive styles
|
|
118
|
+
// Register interactive styles with mapping support
|
|
107
119
|
if (nodeInteractiveStyles && nodeInteractiveStyles.length > 0) {
|
|
108
|
-
|
|
120
|
+
if (ctx.componentResolvedProps && hasInteractiveStyleMappings(nodeInteractiveStyles)) {
|
|
121
|
+
const { resolvedStyles, mappings } = extractInteractiveStyleMappings(nodeInteractiveStyles);
|
|
122
|
+
const cssVariables = resolveExtractedMappings(mappings, ctx.componentResolvedProps);
|
|
123
|
+
InteractiveStylesRegistry.set(elementClass, resolvedStyles);
|
|
124
|
+
if (Object.keys(cssVariables).length > 0) {
|
|
125
|
+
InteractiveStylesRegistry.setVariables(elementPath, cssVariables);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
InteractiveStylesRegistry.set(elementClass, nodeInteractiveStyles);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Apply preview classes when previewProp is set and truthy
|
|
132
|
+
if (ctx.componentResolvedProps) {
|
|
133
|
+
const previewClasses: string[] = [];
|
|
134
|
+
for (const rule of nodeInteractiveStyles) {
|
|
135
|
+
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
136
|
+
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
137
|
+
previewClasses.push(...styleClasses);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (previewClasses.length > 0) {
|
|
141
|
+
classNames.push(...previewClasses);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
109
144
|
}
|
|
110
145
|
}
|
|
111
146
|
|
|
@@ -11,6 +11,7 @@ import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
|
11
11
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
12
12
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
13
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
14
|
+
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
14
15
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
15
16
|
import type { BuilderContext } from "./types";
|
|
16
17
|
|
|
@@ -49,7 +50,18 @@ export function buildLocaleList(
|
|
|
49
50
|
key,
|
|
50
51
|
'data-element-path': pathToString(elementPath),
|
|
51
52
|
'data-locale-list': 'true',
|
|
52
|
-
ref: (el: HTMLElement | null) =>
|
|
53
|
+
ref: (el: HTMLElement | null) => {
|
|
54
|
+
deps.elementRegistry.register(elementPath, el, effectiveParentComponentName, false);
|
|
55
|
+
// Apply CSS variables for interactive styles
|
|
56
|
+
if (el) {
|
|
57
|
+
const cssVariables = InteractiveStylesRegistry.getVariables(elementPath);
|
|
58
|
+
if (cssVariables && Object.keys(cssVariables).length > 0) {
|
|
59
|
+
for (const [varName, value] of Object.entries(cssVariables)) {
|
|
60
|
+
el.style.setProperty(varName, value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
53
65
|
};
|
|
54
66
|
|
|
55
67
|
// Add CMS item index path for elements inside CMS lists
|
|
@@ -91,9 +103,32 @@ export function buildLocaleList(
|
|
|
91
103
|
// Prepend element class
|
|
92
104
|
classNames.unshift(elementClass);
|
|
93
105
|
|
|
94
|
-
// Register interactive styles
|
|
106
|
+
// Register interactive styles with mapping support
|
|
95
107
|
if (nodeInteractiveStyles && nodeInteractiveStyles.length > 0) {
|
|
96
|
-
|
|
108
|
+
if (ctx.componentResolvedProps && hasInteractiveStyleMappings(nodeInteractiveStyles)) {
|
|
109
|
+
const { resolvedStyles, mappings } = extractInteractiveStyleMappings(nodeInteractiveStyles);
|
|
110
|
+
const cssVariables = resolveExtractedMappings(mappings, ctx.componentResolvedProps);
|
|
111
|
+
InteractiveStylesRegistry.set(elementClass, resolvedStyles);
|
|
112
|
+
if (Object.keys(cssVariables).length > 0) {
|
|
113
|
+
InteractiveStylesRegistry.setVariables(elementPath, cssVariables);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
InteractiveStylesRegistry.set(elementClass, nodeInteractiveStyles);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Apply preview classes when previewProp is set and truthy
|
|
120
|
+
if (ctx.componentResolvedProps) {
|
|
121
|
+
const previewClasses: string[] = [];
|
|
122
|
+
for (const rule of nodeInteractiveStyles) {
|
|
123
|
+
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
124
|
+
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
125
|
+
previewClasses.push(...styleClasses);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (previewClasses.length > 0) {
|
|
129
|
+
classNames.push(...previewClasses);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
97
132
|
}
|
|
98
133
|
}
|
|
99
134
|
|
|
@@ -11,6 +11,7 @@ import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
|
11
11
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
12
12
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
13
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
14
|
+
import { hasInteractiveStyleMappings, extractInteractiveStyleMappings, resolveExtractedMappings } from "../../../shared/interactiveStyleMappings";
|
|
14
15
|
import type { ElementRegistry } from "../../elementRegistry";
|
|
15
16
|
import type { BuilderContext, BuildResult, BuildChildrenContext } from "./types";
|
|
16
17
|
|
|
@@ -55,7 +56,18 @@ export function buildObjectLink(
|
|
|
55
56
|
key,
|
|
56
57
|
'data-element-path': pathToString(elementPath),
|
|
57
58
|
'data-object-link-node': 'true',
|
|
58
|
-
ref: (el: HTMLElement | null) =>
|
|
59
|
+
ref: (el: HTMLElement | null) => {
|
|
60
|
+
deps.elementRegistry.register(elementPath, el, effectiveParentComponentName, false);
|
|
61
|
+
// Apply CSS variables for interactive styles
|
|
62
|
+
if (el) {
|
|
63
|
+
const cssVariables = InteractiveStylesRegistry.getVariables(elementPath);
|
|
64
|
+
if (cssVariables && Object.keys(cssVariables).length > 0) {
|
|
65
|
+
for (const [varName, value] of Object.entries(cssVariables)) {
|
|
66
|
+
el.style.setProperty(varName, value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
59
71
|
};
|
|
60
72
|
|
|
61
73
|
// Add CMS item index path for elements inside CMS lists
|
|
@@ -97,9 +109,32 @@ export function buildObjectLink(
|
|
|
97
109
|
// Prepend element class
|
|
98
110
|
classNames.unshift(elementClass);
|
|
99
111
|
|
|
100
|
-
// Register interactive styles
|
|
112
|
+
// Register interactive styles with mapping support
|
|
101
113
|
if (nodeInteractiveStyles && nodeInteractiveStyles.length > 0) {
|
|
102
|
-
|
|
114
|
+
if (ctx.componentResolvedProps && hasInteractiveStyleMappings(nodeInteractiveStyles)) {
|
|
115
|
+
const { resolvedStyles, mappings } = extractInteractiveStyleMappings(nodeInteractiveStyles);
|
|
116
|
+
const cssVariables = resolveExtractedMappings(mappings, ctx.componentResolvedProps);
|
|
117
|
+
InteractiveStylesRegistry.set(elementClass, resolvedStyles);
|
|
118
|
+
if (Object.keys(cssVariables).length > 0) {
|
|
119
|
+
InteractiveStylesRegistry.setVariables(elementPath, cssVariables);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
InteractiveStylesRegistry.set(elementClass, nodeInteractiveStyles);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Apply preview classes when previewProp is set and truthy
|
|
126
|
+
if (ctx.componentResolvedProps) {
|
|
127
|
+
const previewClasses: string[] = [];
|
|
128
|
+
for (const rule of nodeInteractiveStyles) {
|
|
129
|
+
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
130
|
+
const styleClasses = responsiveStylesToClasses(rule.style as StyleObject | ResponsiveStyleObject);
|
|
131
|
+
previewClasses.push(...styleClasses);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (previewClasses.length > 0) {
|
|
135
|
+
classNames.push(...previewClasses);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
103
138
|
}
|
|
104
139
|
}
|
|
105
140
|
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
import type { I18nValue, I18nConfig } from '../../shared/types';
|
|
9
9
|
import { getI18nConfig } from '../i18nConfigService';
|
|
10
10
|
import { isRichTextMarker, richTextMarkerToHtml } from '../../shared/propResolver';
|
|
11
|
+
import { tiptapToHtml } from '../../shared/richtext/tiptapToHtml';
|
|
12
|
+
import { isTiptapDocument } from '../../shared/richtext/types';
|
|
13
|
+
import { RAW_HTML_PREFIX } from '../../shared/constants';
|
|
14
|
+
|
|
15
|
+
// Re-export for backward compatibility
|
|
16
|
+
export { RAW_HTML_PREFIX };
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Check if a value is an I18nValue object
|
|
@@ -86,8 +92,14 @@ export function processCMSTemplate(
|
|
|
86
92
|
return '';
|
|
87
93
|
}
|
|
88
94
|
// Handle rich-text markers - extract HTML content for interpolation
|
|
95
|
+
// Mark with RAW_HTML_PREFIX so renderer knows not to escape
|
|
89
96
|
if (typeof value === 'object' && '__richtext__' in value && typeof (value as unknown as { html: string }).html === 'string') {
|
|
90
|
-
return (value as unknown as { html: string }).html;
|
|
97
|
+
return RAW_HTML_PREFIX + (value as unknown as { html: string }).html;
|
|
98
|
+
}
|
|
99
|
+
// Handle raw TipTap documents (fallback if not preprocessed)
|
|
100
|
+
// Mark with RAW_HTML_PREFIX so renderer knows not to escape
|
|
101
|
+
if (isTiptapDocument(value)) {
|
|
102
|
+
return RAW_HTML_PREFIX + tiptapToHtml(value);
|
|
91
103
|
}
|
|
92
104
|
return String(value);
|
|
93
105
|
});
|
|
@@ -29,6 +29,18 @@ import { parseLocaleFromPath, setStoredLocale, DEFAULT_I18N_CONFIG } from "../..
|
|
|
29
29
|
import { fetchI18nConfig, setI18nConfig as setCachedI18nConfig } from "../i18nConfigService";
|
|
30
30
|
import { IFRAME_MESSAGE_TYPES } from "../../shared/constants";
|
|
31
31
|
|
|
32
|
+
/** SSR-serialized CMS context for client-side hydration */
|
|
33
|
+
interface SSRCMSContext {
|
|
34
|
+
item: Record<string, unknown>;
|
|
35
|
+
templatePath: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare global {
|
|
39
|
+
interface Window {
|
|
40
|
+
__MENO_CMS__?: SSRCMSContext;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/**
|
|
33
45
|
* Router component props
|
|
34
46
|
*/
|
|
@@ -123,6 +135,11 @@ export function Router(props: RouterProps = {}): ReactElement {
|
|
|
123
135
|
// Don't block rendering waiting for CMS context - render immediately and re-render when context arrives
|
|
124
136
|
const [awaitingCmsContext, setAwaitingCmsContext] = useState(false);
|
|
125
137
|
const [collectionItemsMap, setCollectionItemsMap] = useState<Record<string, CMSItem[]>>({});
|
|
138
|
+
// Store CMS template path for HMR reloads (so we don't lose it after initial load)
|
|
139
|
+
const [cmsTemplatePath, setCmsTemplatePath] = useState<string | null>(null);
|
|
140
|
+
|
|
141
|
+
// Track if initial mount used SSR CMS context (to skip redundant path-based load)
|
|
142
|
+
const ssrCmsHandledRef = useRef(false);
|
|
126
143
|
|
|
127
144
|
// Create RouteLoader instance
|
|
128
145
|
const routeLoader = useRef(new RouteLoader({
|
|
@@ -312,9 +329,11 @@ export function Router(props: RouterProps = {}): ReactElement {
|
|
|
312
329
|
}, [componentTree, currentPath, services]);
|
|
313
330
|
|
|
314
331
|
// Load components function using RouteLoader
|
|
332
|
+
// For CMS pages, use the stored template path instead of the URL path
|
|
315
333
|
const loadComponents = useCallback(async (path: string) => {
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
const pathToLoad = cmsTemplatePath || path;
|
|
335
|
+
await routeLoader.loadComponents(pathToLoad);
|
|
336
|
+
}, [cmsTemplatePath]);
|
|
318
337
|
|
|
319
338
|
// Handle navigation
|
|
320
339
|
useEffect(() => {
|
|
@@ -330,16 +349,37 @@ export function Router(props: RouterProps = {}): ReactElement {
|
|
|
330
349
|
}, [loadComponents]);
|
|
331
350
|
|
|
332
351
|
useEffect(() => {
|
|
352
|
+
// Check for SSR-serialized CMS context (from window.__MENO_CMS__)
|
|
353
|
+
// If present, use the template path instead of the CMS URL to avoid 404
|
|
354
|
+
const ssrCmsContext = window.__MENO_CMS__;
|
|
355
|
+
if (ssrCmsContext) {
|
|
356
|
+
// Set CMS context from SSR
|
|
357
|
+
setCmsContext(ssrCmsContext.item);
|
|
358
|
+
// Store template path for HMR reloads
|
|
359
|
+
setCmsTemplatePath(ssrCmsContext.templatePath);
|
|
360
|
+
// Mark that we're handling SSR CMS (to skip path-based load below)
|
|
361
|
+
ssrCmsHandledRef.current = true;
|
|
362
|
+
// Clear to prevent stale data on SPA navigation
|
|
363
|
+
delete window.__MENO_CMS__;
|
|
364
|
+
}
|
|
365
|
+
|
|
333
366
|
// Initial load
|
|
334
367
|
routeLoader.loadPages();
|
|
335
368
|
routeLoader.loadGlobalComponents().then(() => {
|
|
336
|
-
|
|
369
|
+
// Use template path for CMS pages, otherwise use the current URL path
|
|
370
|
+
const pathToLoad = ssrCmsContext ? ssrCmsContext.templatePath : currentPath;
|
|
371
|
+
loadComponents(pathToLoad);
|
|
337
372
|
});
|
|
338
373
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
339
374
|
}, []); // Only run on mount - currentPath handled by separate effect below
|
|
340
375
|
|
|
341
376
|
// Reload when path changes
|
|
342
377
|
useEffect(() => {
|
|
378
|
+
// Skip initial mount if SSR CMS context was handled (template already loading)
|
|
379
|
+
if (ssrCmsHandledRef.current) {
|
|
380
|
+
ssrCmsHandledRef.current = false;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
343
383
|
setShowNotFound(false); // Reset not found state when path changes
|
|
344
384
|
loadComponents(currentPath);
|
|
345
385
|
}, [currentPath, loadComponents]);
|
|
@@ -16,6 +16,7 @@ import { isRichTextMarker, richTextMarkerToHtml } from '../shared/propResolver';
|
|
|
16
16
|
import { isTiptapDocument, tiptapToHtml } from '../shared/richtext';
|
|
17
17
|
import { hasItemTemplates } from '../shared/itemTemplateUtils';
|
|
18
18
|
import { safeEvaluate } from '../shared/expressionEvaluator';
|
|
19
|
+
import { RAW_HTML_PREFIX } from '../shared/constants';
|
|
19
20
|
|
|
20
21
|
// Re-export for backward compatibility
|
|
21
22
|
export { isResponsiveStyle };
|
|
@@ -327,9 +328,10 @@ export function processStructure(
|
|
|
327
328
|
// Use evaluateTemplate to preserve type (objects, arrays, numbers)
|
|
328
329
|
if (/^\{\{.+\}\}$/.test(structure)) {
|
|
329
330
|
const result = evaluateTemplate(structure, evalContext);
|
|
330
|
-
// Check for rich-text marker -
|
|
331
|
+
// Check for rich-text marker - extract HTML content with RAW_HTML_PREFIX
|
|
332
|
+
// The prefix signals to ComponentBuilder.processTextNode to render as HTML
|
|
331
333
|
if (isRichTextMarker(result)) {
|
|
332
|
-
return
|
|
334
|
+
return RAW_HTML_PREFIX + richTextMarkerToHtml(result);
|
|
333
335
|
}
|
|
334
336
|
if (typeof result === 'string' || typeof result === 'number') {
|
|
335
337
|
return result;
|
|
@@ -391,11 +393,24 @@ export function processStructure(
|
|
|
391
393
|
return null;
|
|
392
394
|
}
|
|
393
395
|
|
|
394
|
-
//
|
|
396
|
+
// Check if this is a valid node structure or a plain object (like props)
|
|
395
397
|
const inputNode = structure as ComponentNode;
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
398
|
+
const hasValidNodeType = inputNode.type && isValidNodeType(inputNode.type);
|
|
399
|
+
|
|
400
|
+
// If no valid node type, treat as plain object and process values recursively
|
|
401
|
+
// This handles props objects like { text: "{{text}}", isMarginBottom: false }
|
|
402
|
+
if (!hasValidNodeType) {
|
|
403
|
+
const result: Record<string, unknown> = {};
|
|
404
|
+
for (const [key, value] of Object.entries(structure)) {
|
|
405
|
+
const processedValue = processStructure(value as any, context, viewportWidth, instanceChildren, preserveResponsiveStyles, depth + 1);
|
|
406
|
+
if (processedValue !== null && processedValue !== undefined) {
|
|
407
|
+
result[key] = processedValue;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return result as any;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const preservedType = inputNode.type;
|
|
399
414
|
|
|
400
415
|
// Create base processed object based on type
|
|
401
416
|
let processed: ComponentNode;
|
|
@@ -13,12 +13,14 @@ export interface FileWatchCallbacks {
|
|
|
13
13
|
onComponentChange?: () => Promise<void>;
|
|
14
14
|
onPageChange?: (pagePath: string) => Promise<void>;
|
|
15
15
|
onColorsChange?: () => Promise<void>;
|
|
16
|
+
onCMSChange?: (collection: string) => Promise<void>;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export class FileWatcher {
|
|
19
20
|
private componentsWatcher: FSWatcher | null = null;
|
|
20
21
|
private pagesWatcher: FSWatcher | null = null;
|
|
21
22
|
private colorsWatcher: FSWatcher | null = null;
|
|
23
|
+
private cmsWatcher: FSWatcher | null = null;
|
|
22
24
|
|
|
23
25
|
constructor(private callbacks: FileWatchCallbacks) {}
|
|
24
26
|
|
|
@@ -98,12 +100,37 @@ export class FileWatcher {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/**
|
|
101
|
-
* Start watching
|
|
103
|
+
* Start watching CMS directory
|
|
104
|
+
* Watches for changes in CMS content files (cms/{collection}/*.json)
|
|
105
|
+
*/
|
|
106
|
+
watchCMS(dirPath: string = projectPaths.cms()): void {
|
|
107
|
+
if (!existsSync(dirPath)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.cmsWatcher = watch(
|
|
112
|
+
dirPath,
|
|
113
|
+
{ recursive: true },
|
|
114
|
+
async (event, filename) => {
|
|
115
|
+
if (filename && filename.endsWith('.json')) {
|
|
116
|
+
// Extract collection from path: "blog/my-post.json" -> "blog"
|
|
117
|
+
const collection = filename.split('/')[0];
|
|
118
|
+
if (this.callbacks.onCMSChange) {
|
|
119
|
+
await this.callbacks.onCMSChange(collection);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start watching all directories
|
|
102
128
|
*/
|
|
103
129
|
watchAll(): void {
|
|
104
130
|
this.watchComponents();
|
|
105
131
|
this.watchPages();
|
|
106
132
|
this.watchColors();
|
|
133
|
+
this.watchCMS();
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
/**
|
|
@@ -124,13 +151,18 @@ export class FileWatcher {
|
|
|
124
151
|
this.colorsWatcher.close();
|
|
125
152
|
this.colorsWatcher = null;
|
|
126
153
|
}
|
|
154
|
+
|
|
155
|
+
if (this.cmsWatcher) {
|
|
156
|
+
this.cmsWatcher.close();
|
|
157
|
+
this.cmsWatcher = null;
|
|
158
|
+
}
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
/**
|
|
130
162
|
* Check if watchers are active
|
|
131
163
|
*/
|
|
132
164
|
isWatching(): boolean {
|
|
133
|
-
return this.componentsWatcher !== null || this.pagesWatcher !== null;
|
|
165
|
+
return this.componentsWatcher !== null || this.pagesWatcher !== null || this.cmsWatcher !== null;
|
|
134
166
|
}
|
|
135
167
|
}
|
|
136
168
|
|