meno-core 1.0.39 → 1.0.40
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-astro.ts +195 -68
- package/dist/bin/cli.js +1 -1
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-WK5XLASY.js → chunk-3NOZVNM4.js} +3 -3
- package/dist/chunks/{chunk-W6HDII4T.js → chunk-GKICS7CF.js} +27 -14
- package/dist/chunks/chunk-GKICS7CF.js.map +7 -0
- package/dist/chunks/{chunk-P3FX5HJM.js → chunk-LOJLO2EY.js} +1 -1
- package/dist/chunks/chunk-LOJLO2EY.js.map +7 -0
- package/dist/chunks/{chunk-HNAS6BSS.js → chunk-MOCRENNU.js} +55 -5
- package/dist/chunks/{chunk-HNAS6BSS.js.map → chunk-MOCRENNU.js.map} +3 -3
- package/dist/chunks/{chunk-NV25WXCA.js → chunk-OJ5SROQN.js} +5 -3
- package/dist/chunks/chunk-OJ5SROQN.js.map +7 -0
- package/dist/chunks/{chunk-AIXKUVNG.js → chunk-V4SVSX3X.js} +3 -3
- package/dist/chunks/{chunk-KULPBDC7.js → chunk-Z7SAOCDG.js} +5 -2
- package/dist/chunks/{chunk-KULPBDC7.js.map → chunk-Z7SAOCDG.js.map} +2 -2
- package/dist/chunks/{constants-5CRJRQNR.js → constants-L75FR445.js} +2 -2
- package/dist/entries/server-router.js +6 -6
- package/dist/lib/client/index.js +5 -5
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +2007 -197
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +3 -3
- package/dist/lib/test-utils/index.js +1 -1
- package/lib/client/core/builders/embedBuilder.ts +2 -2
- package/lib/server/astro/cmsPageEmitter.ts +417 -0
- package/lib/server/astro/componentEmitter.ts +90 -5
- package/lib/server/astro/nodeToAstro.ts +830 -37
- package/lib/server/astro/pageEmitter.ts +39 -3
- package/lib/server/astro/tailwindMapper.ts +69 -8
- package/lib/server/astro/templateTransformer.ts +107 -0
- package/lib/server/index.ts +9 -0
- package/lib/server/routes/api/components.ts +62 -0
- package/lib/server/routes/api/core-routes.ts +8 -0
- package/lib/server/ssr/ssrRenderer.ts +30 -10
- package/lib/server/webflow/buildWebflow.ts +415 -0
- package/lib/server/webflow/index.ts +22 -0
- package/lib/server/webflow/nodeToWebflow.ts +423 -0
- package/lib/server/webflow/styleMapper.ts +241 -0
- package/lib/server/webflow/types.ts +196 -0
- package/lib/shared/constants.ts +2 -0
- package/lib/shared/types/components.ts +1 -0
- package/lib/shared/validation/schemas.ts +1 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-NV25WXCA.js.map +0 -7
- package/dist/chunks/chunk-P3FX5HJM.js.map +0 -7
- package/dist/chunks/chunk-W6HDII4T.js.map +0 -7
- /package/dist/chunks/{chunk-WK5XLASY.js.map → chunk-3NOZVNM4.js.map} +0 -0
- /package/dist/chunks/{chunk-AIXKUVNG.js.map → chunk-V4SVSX3X.js.map} +0 -0
- /package/dist/chunks/{constants-5CRJRQNR.js.map → constants-L75FR445.js.map} +0 -0
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* Generates .astro page files that import and compose components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { JSONPage, ComponentDefinition } from '../../shared/types';
|
|
6
|
+
import type { JSONPage, ComponentDefinition, I18nConfig } from '../../shared/types';
|
|
7
7
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
8
8
|
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
9
9
|
import { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
|
|
10
|
+
import type { ImageMetadataMap } from '../ssr/imageMetadata';
|
|
11
|
+
import type { SlugMap } from '../../shared/slugTranslator';
|
|
10
12
|
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
12
14
|
// Types
|
|
@@ -41,6 +43,14 @@ export interface PageEmitOptions {
|
|
|
41
43
|
pageName: string;
|
|
42
44
|
/** Breakpoint config for responsive Tailwind classes */
|
|
43
45
|
breakpoints?: BreakpointConfig;
|
|
46
|
+
/** Image metadata map for responsive image generation */
|
|
47
|
+
imageMetadataMap?: ImageMetadataMap;
|
|
48
|
+
/** I18n config for locale list rendering */
|
|
49
|
+
i18nConfig?: I18nConfig;
|
|
50
|
+
/** Locale→slug map for current page (for locale list links) */
|
|
51
|
+
currentPageSlugMap?: Record<string, string>;
|
|
52
|
+
/** Slug mappings for translating internal link hrefs */
|
|
53
|
+
slugMappings?: SlugMap[];
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
// ---------------------------------------------------------------------------
|
|
@@ -88,6 +98,10 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
88
98
|
ssrFallbacks,
|
|
89
99
|
pageName,
|
|
90
100
|
breakpoints: breakpointsOpt,
|
|
101
|
+
imageMetadataMap,
|
|
102
|
+
i18nConfig,
|
|
103
|
+
currentPageSlugMap,
|
|
104
|
+
slugMappings,
|
|
91
105
|
} = options;
|
|
92
106
|
|
|
93
107
|
const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
|
|
@@ -110,6 +124,14 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
110
124
|
fileType: 'page',
|
|
111
125
|
fileName: pageName,
|
|
112
126
|
breakpoints,
|
|
127
|
+
imageMetadataMap,
|
|
128
|
+
locale,
|
|
129
|
+
i18nConfig,
|
|
130
|
+
currentPageSlugMap,
|
|
131
|
+
frontmatterLines: [],
|
|
132
|
+
astroImports: new Set<string>(),
|
|
133
|
+
slugMappings,
|
|
134
|
+
i18nDefaultLocale: i18nConfig?.defaultLocale,
|
|
113
135
|
};
|
|
114
136
|
|
|
115
137
|
// Emit the template body
|
|
@@ -117,6 +139,13 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
117
139
|
|
|
118
140
|
// Build frontmatter with imports
|
|
119
141
|
const importLines: string[] = [];
|
|
142
|
+
|
|
143
|
+
// Add Astro API imports (e.g., getCollection) if collected during emission
|
|
144
|
+
if (ctx.astroImports && ctx.astroImports.size > 0) {
|
|
145
|
+
const astroImports = Array.from(ctx.astroImports);
|
|
146
|
+
importLines.push(`import { ${astroImports.join(', ')} } from 'astro:content';`);
|
|
147
|
+
}
|
|
148
|
+
|
|
120
149
|
importLines.push(`import BaseLayout from '${layoutImportPath}';`);
|
|
121
150
|
|
|
122
151
|
// Sort component imports alphabetically
|
|
@@ -137,8 +166,13 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
137
166
|
const escapedMeta = escapeTemplateLiteral(meta);
|
|
138
167
|
const escapedFontPreloads = escapeTemplateLiteral(fontPreloads);
|
|
139
168
|
|
|
169
|
+
// Collect frontmatter lines from list emission (e.g., getCollection calls)
|
|
170
|
+
const extraFrontmatter = ctx.frontmatterLines && ctx.frontmatterLines.length > 0
|
|
171
|
+
? '\n' + ctx.frontmatterLines.join('\n')
|
|
172
|
+
: '';
|
|
173
|
+
|
|
140
174
|
return `---
|
|
141
|
-
${importLines.join('\n')}
|
|
175
|
+
${importLines.join('\n')}${extraFrontmatter}
|
|
142
176
|
---
|
|
143
177
|
<BaseLayout
|
|
144
178
|
title="${escapeJSX(title)}"
|
|
@@ -149,7 +183,9 @@ ${importLines.join('\n')}
|
|
|
149
183
|
fontPreloads={\`${escapedFontPreloads}\`}
|
|
150
184
|
libraryTags={${libraryTagsLiteral}}
|
|
151
185
|
>
|
|
152
|
-
|
|
186
|
+
<div id="root">
|
|
187
|
+
${templateBody} </div>
|
|
188
|
+
</BaseLayout>
|
|
153
189
|
`;
|
|
154
190
|
}
|
|
155
191
|
|
|
@@ -263,6 +263,10 @@ const singleValueMatches: Record<string, string> = {
|
|
|
263
263
|
'right:0': 'right-0',
|
|
264
264
|
'bottom:0': 'bottom-0',
|
|
265
265
|
'left:0': 'left-0',
|
|
266
|
+
'outline:none': '[outline:none]',
|
|
267
|
+
'background:none': '[background:none]',
|
|
268
|
+
'background:transparent': '[background:transparent]',
|
|
269
|
+
'backgroundColor:transparent': 'bg-transparent',
|
|
266
270
|
};
|
|
267
271
|
|
|
268
272
|
// ---------------------------------------------------------------------------
|
|
@@ -331,7 +335,7 @@ const arbitraryPrefixMap: Record<string, string> = {
|
|
|
331
335
|
backdropFilter: 'backdrop',
|
|
332
336
|
transform: '[transform]',
|
|
333
337
|
transformOrigin: 'origin',
|
|
334
|
-
transition: 'transition',
|
|
338
|
+
transition: '[transition]',
|
|
335
339
|
mixBlendMode: 'mix-blend',
|
|
336
340
|
clipPath: '[clip-path]',
|
|
337
341
|
|
|
@@ -408,16 +412,44 @@ export function propertyToTailwind(
|
|
|
408
412
|
}
|
|
409
413
|
|
|
410
414
|
// Color variable handling:
|
|
411
|
-
// "var(--text)" → "text-[var(--text)]"
|
|
412
|
-
// Bare color name like "text" → "text-[var(--text)]"
|
|
415
|
+
// "var(--text)" → "text-[color:var(--text)]" (type hint avoids ambiguity with font-size)
|
|
416
|
+
// Bare color name like "text" → "text-[color:var(--text)]"
|
|
413
417
|
if (property === 'color' || property === 'backgroundColor' || property === 'borderColor') {
|
|
414
418
|
const prefix = property === 'color' ? 'text' : property === 'backgroundColor' ? 'bg' : 'border';
|
|
415
419
|
if (strValue.includes('var(')) {
|
|
416
|
-
return `${prefix}-[
|
|
420
|
+
return `${prefix}-[color:${strValue}]`;
|
|
417
421
|
}
|
|
418
422
|
// Bare name (not a hex, rgb, or number-starting value)
|
|
419
423
|
if (!strValue.match(/^[#\d]/) && !strValue.includes('rgb') && !strValue.includes('hsl')) {
|
|
420
|
-
return `${prefix}-[var(--${strValue})]`;
|
|
424
|
+
return `${prefix}-[color:var(--${strValue})]`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Color properties with actual color values need type hints to avoid ambiguity
|
|
429
|
+
if (property === 'borderColor' || property === 'color') {
|
|
430
|
+
const prefix = property === 'color' ? 'text' : 'border';
|
|
431
|
+
const sanitized = strValue.replace(/\s+/g, '_');
|
|
432
|
+
return `${prefix}-[color:${sanitized}]`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Border shorthand (e.g., "1px solid", "2px solid var(--border)") needs arbitrary property syntax
|
|
436
|
+
// because Tailwind's border-[...] only accepts width values, not compound shorthands
|
|
437
|
+
if (property === 'border' || property === 'borderTop' || property === 'borderRight' ||
|
|
438
|
+
property === 'borderBottom' || property === 'borderLeft') {
|
|
439
|
+
if (strValue.includes('solid') || strValue.includes('dashed') || strValue.includes('dotted') || strValue.includes('none')) {
|
|
440
|
+
const cssProp = property.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
441
|
+
const sanitized = strValue.replace(/\s+/g, '_');
|
|
442
|
+
return `[${cssProp}:${sanitized}]`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// `background` is a CSS shorthand — Tailwind's bg-[...] maps to background-color, not background.
|
|
447
|
+
// For non-color values (none, transparent, gradients, etc.), use arbitrary property syntax.
|
|
448
|
+
if (property === 'background') {
|
|
449
|
+
const isSimpleColor = /^(#[0-9a-fA-F]{3,8}|rgb|hsl|var\()/.test(strValue);
|
|
450
|
+
if (!isSimpleColor) {
|
|
451
|
+
const sanitized = strValue.replace(/\s+/g, '_');
|
|
452
|
+
return `[background:${sanitized}]`;
|
|
421
453
|
}
|
|
422
454
|
}
|
|
423
455
|
|
|
@@ -439,9 +471,16 @@ export function propertyToTailwind(
|
|
|
439
471
|
// Standard arbitrary value: prefix-[value]
|
|
440
472
|
const sanitized = strValue.replace(/\s+/g, '_');
|
|
441
473
|
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
474
|
+
// Disambiguate properties that share a Tailwind prefix with another CSS property.
|
|
475
|
+
// Without type hints, Tailwind guesses wrong (e.g., text-[var(--x)] → color instead of font-size).
|
|
476
|
+
if (property === 'fontSize') {
|
|
477
|
+
return `text-[length:${sanitized}]`;
|
|
478
|
+
}
|
|
479
|
+
if (property === 'fontFamily') {
|
|
480
|
+
return `font-[family-name:${sanitized}]`;
|
|
481
|
+
}
|
|
482
|
+
if (property === 'fontWeight') {
|
|
483
|
+
return `font-[number:${sanitized}]`;
|
|
445
484
|
}
|
|
446
485
|
|
|
447
486
|
return `${twPrefix}-[${sanitized}]`;
|
|
@@ -459,6 +498,16 @@ export function stylesToTailwind(
|
|
|
459
498
|
const classes: string[] = [];
|
|
460
499
|
const dynamicStyles: Record<string, string> = {};
|
|
461
500
|
|
|
501
|
+
// When borderColor is present alongside a border shorthand, Tailwind's CSS
|
|
502
|
+
// ordering can cause the shorthand's arbitrary property `[border:...]` to
|
|
503
|
+
// override the longhand `border-[color:...]`. To avoid this, decompose
|
|
504
|
+
// border shorthands into width + style only (dropping the color part) so
|
|
505
|
+
// borderColor can take effect without conflicts.
|
|
506
|
+
const hasBorderColor = 'borderColor' in style && !isStyleMapping(style.borderColor);
|
|
507
|
+
const borderShorthands = new Set([
|
|
508
|
+
'border', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft',
|
|
509
|
+
]);
|
|
510
|
+
|
|
462
511
|
for (const [prop, value] of Object.entries(style)) {
|
|
463
512
|
if (isStyleMapping(value)) continue;
|
|
464
513
|
|
|
@@ -471,6 +520,18 @@ export function stylesToTailwind(
|
|
|
471
520
|
continue;
|
|
472
521
|
}
|
|
473
522
|
|
|
523
|
+
// Decompose border shorthands when borderColor is separately specified
|
|
524
|
+
if (hasBorderColor && borderShorthands.has(prop)) {
|
|
525
|
+
const parts = strValue.split(/\s+/);
|
|
526
|
+
// Parse width and style from shorthand (e.g. "1px solid #ccc" → "1px", "solid")
|
|
527
|
+
const width = parts.find(p => /^\d/.test(p) || p === '0');
|
|
528
|
+
const borderStyle = parts.find(p => /^(solid|dashed|dotted|double|groove|ridge|inset|outset|none|hidden)$/.test(p));
|
|
529
|
+
const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
530
|
+
if (width) classes.push(`[${cssProp}-width:${width}]`);
|
|
531
|
+
if (borderStyle) classes.push(`[${cssProp}-style:${borderStyle}]`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
474
535
|
const twClass = propertyToTailwind(prop, value);
|
|
475
536
|
if (twClass) {
|
|
476
537
|
classes.push(twClass);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS Template Expression Transformer for Astro Export
|
|
3
|
+
* Converts {{cms.field}} and {{item.field}} template expressions to Astro expressions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// CMS template pattern (from cmsSSRProcessor.ts)
|
|
7
|
+
const CMS_TEMPLATE_PATTERN = /\{\{cms\.([^}]+)\}\}/g;
|
|
8
|
+
// Item template pattern for list iteration
|
|
9
|
+
const ITEM_TEMPLATE_PATTERN = /\{\{([^}]+)\}\}/g;
|
|
10
|
+
|
|
11
|
+
export function isTemplateExpression(text: string): boolean {
|
|
12
|
+
return /\{\{.+?\}\}/.test(text);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function transformCMSTemplate(
|
|
16
|
+
text: string,
|
|
17
|
+
binding: string = 'entry',
|
|
18
|
+
richTextFields?: Set<string>,
|
|
19
|
+
wrapFn?: string
|
|
20
|
+
): string {
|
|
21
|
+
const w = (expr: string) => wrapFn ? `${wrapFn}(${expr})` : expr;
|
|
22
|
+
|
|
23
|
+
// Check if entire text is a single {{cms.field}} expression
|
|
24
|
+
const fullMatch = text.match(/^\{\{cms\.([^}]+)\}\}$/);
|
|
25
|
+
if (fullMatch) {
|
|
26
|
+
const fieldPath = fullMatch[1].trim();
|
|
27
|
+
const topLevelField = fieldPath.split('.')[0];
|
|
28
|
+
|
|
29
|
+
// Rich-text fields use Fragment set:html
|
|
30
|
+
if (richTextFields?.has(topLevelField)) {
|
|
31
|
+
return `<Fragment set:html={${w(`${binding}.data.${fieldPath}`)}} />`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return `{${w(`${binding}.data.${fieldPath}`)}}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Mixed content: "Hello {{cms.name}}" → {`Hello ${entry.data.name}`}
|
|
38
|
+
if (CMS_TEMPLATE_PATTERN.test(text)) {
|
|
39
|
+
CMS_TEMPLATE_PATTERN.lastIndex = 0;
|
|
40
|
+
const replaced = text.replace(CMS_TEMPLATE_PATTERN, (_, fieldPath) => {
|
|
41
|
+
return `\${${w(`${binding}.data.${fieldPath.trim()}`)}}`;
|
|
42
|
+
});
|
|
43
|
+
return `{\`${replaced}\`}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Replace legacy itemIndex/itemFirst/itemLast meta-variables with
|
|
51
|
+
* the actual .map() callback index parameter name.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Rewrite the default `item.` prefix to the actual .map() parameter name
|
|
55
|
+
* when itemAs differs from 'item' (e.g., itemAs='link' → item.text → link.text).
|
|
56
|
+
*/
|
|
57
|
+
export function rewriteItemVar(expr: string, itemVar: string): string {
|
|
58
|
+
if (itemVar === 'item') return expr;
|
|
59
|
+
return expr.replace(/\bitem\./g, `${itemVar}.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function replaceItemMetaVars(expr: string, indexVar: string, sourceVar?: string, itemVar?: string): string {
|
|
63
|
+
const lastExpr = sourceVar
|
|
64
|
+
? `(${indexVar} === ${sourceVar}.length - 1)`
|
|
65
|
+
: `false /* itemLast not supported */`;
|
|
66
|
+
let result = expr
|
|
67
|
+
.replace(/\bitemIndex\b/g, indexVar)
|
|
68
|
+
.replace(/\bitemFirst\b/g, `(${indexVar} === 0)`)
|
|
69
|
+
.replace(/\bitemLast\b/g, lastExpr);
|
|
70
|
+
if (itemVar) result = rewriteItemVar(result, itemVar);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function transformItemTemplate(
|
|
75
|
+
text: string,
|
|
76
|
+
itemVar: string = 'item',
|
|
77
|
+
indexVar?: string,
|
|
78
|
+
sourceVar?: string
|
|
79
|
+
): string {
|
|
80
|
+
// Check if entire text is a single {{expression}}
|
|
81
|
+
const fullMatch = text.match(/^\{\{(.+)\}\}$/);
|
|
82
|
+
if (fullMatch) {
|
|
83
|
+
let expr = fullMatch[1].trim();
|
|
84
|
+
expr = rewriteItemVar(expr, itemVar);
|
|
85
|
+
if (indexVar) expr = replaceItemMetaVars(expr, indexVar, sourceVar);
|
|
86
|
+
// If it contains a dot, it's item.field
|
|
87
|
+
if (expr.startsWith(`${itemVar}.`)) {
|
|
88
|
+
return `{${expr}}`;
|
|
89
|
+
}
|
|
90
|
+
// Bare expression like {{features}} - keep as prop reference
|
|
91
|
+
return `{${expr}}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Mixed content: replace each {{expr}}
|
|
95
|
+
if (ITEM_TEMPLATE_PATTERN.test(text)) {
|
|
96
|
+
ITEM_TEMPLATE_PATTERN.lastIndex = 0;
|
|
97
|
+
const replaced = text.replace(ITEM_TEMPLATE_PATTERN, (_, expr) => {
|
|
98
|
+
let trimmed = expr.trim();
|
|
99
|
+
trimmed = rewriteItemVar(trimmed, itemVar);
|
|
100
|
+
if (indexVar) trimmed = replaceItemMetaVars(trimmed, indexVar, sourceVar);
|
|
101
|
+
return `\${${trimmed}}`;
|
|
102
|
+
});
|
|
103
|
+
return `{\`${replaced}\`}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return text;
|
|
107
|
+
}
|
package/lib/server/index.ts
CHANGED
|
@@ -65,6 +65,15 @@ export { buildStaticPages } from '../../build-static';
|
|
|
65
65
|
// Astro export
|
|
66
66
|
export { buildAstroProject } from '../../build-astro';
|
|
67
67
|
|
|
68
|
+
// Webflow export
|
|
69
|
+
export { buildWebflowPayload } from './webflow';
|
|
70
|
+
export type {
|
|
71
|
+
WebflowExportPayload,
|
|
72
|
+
WebflowPage,
|
|
73
|
+
WebflowElement,
|
|
74
|
+
WebflowStyleClass,
|
|
75
|
+
} from './webflow';
|
|
76
|
+
|
|
68
77
|
// Utilities
|
|
69
78
|
export * from './utils';
|
|
70
79
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ComponentService } from '../../services/componentService';
|
|
7
|
+
import type { PageService } from '../../services/pageService';
|
|
8
|
+
import type { ComponentNode } from '../../../shared/types';
|
|
7
9
|
import { createCorsHeaders } from '../../middleware/cors';
|
|
8
10
|
import { jsonResponse } from './shared';
|
|
9
11
|
import { formatJsonErrorMessage } from '../../../shared/jsonRepair';
|
|
@@ -96,6 +98,66 @@ export async function handleComponentJavaScriptRoute(
|
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Handle component usage API endpoint - GET /api/component-usage
|
|
103
|
+
* Returns how many times each component is used across all pages and other components
|
|
104
|
+
*/
|
|
105
|
+
export function handleComponentUsageRoute(
|
|
106
|
+
pageService: PageService,
|
|
107
|
+
componentService: ComponentService
|
|
108
|
+
): Response {
|
|
109
|
+
const usageMap = new Map<string, { count: number; files: Map<string, { name: string; path: string; type: 'page' | 'component' }> }>();
|
|
110
|
+
|
|
111
|
+
function walkTree(node: ComponentNode | undefined | null, fileName: string, filePath: string, fileType: 'page' | 'component') {
|
|
112
|
+
if (!node || typeof node !== 'object') return;
|
|
113
|
+
if (node.type === 'component' && 'component' in node && (node as any).component) {
|
|
114
|
+
const compName = (node as any).component as string;
|
|
115
|
+
if (!usageMap.has(compName)) {
|
|
116
|
+
usageMap.set(compName, { count: 0, files: new Map() });
|
|
117
|
+
}
|
|
118
|
+
const entry = usageMap.get(compName)!;
|
|
119
|
+
entry.count++;
|
|
120
|
+
entry.files.set(filePath, { name: fileName, path: filePath, type: fileType });
|
|
121
|
+
}
|
|
122
|
+
if ('children' in node && Array.isArray((node as any).children)) {
|
|
123
|
+
for (const child of (node as any).children) {
|
|
124
|
+
if (child && typeof child === 'object') {
|
|
125
|
+
walkTree(child as ComponentNode, fileName, filePath, fileType);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Walk all pages
|
|
132
|
+
const pagePaths = pageService.getAllPagePaths();
|
|
133
|
+
for (const path of pagePaths) {
|
|
134
|
+
const pageData = pageService.getPageData(path);
|
|
135
|
+
if (pageData && 'root' in pageData && pageData.root) {
|
|
136
|
+
const name = path === '/' ? 'index' : path.replace(/^\//, '').replace(/\//g, '-');
|
|
137
|
+
walkTree(pageData.root, name, path, 'page');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Walk all component structures
|
|
142
|
+
const allComponents = componentService.getAllComponents();
|
|
143
|
+
for (const [compName, compDef] of Object.entries(allComponents)) {
|
|
144
|
+
if (compDef.component?.structure) {
|
|
145
|
+
walkTree(compDef.component.structure, compName, compName, 'component');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Convert to plain object
|
|
150
|
+
const result: Record<string, { count: number; files: Array<{ name: string; path: string; type: 'page' | 'component' }> }> = {};
|
|
151
|
+
for (const [name, data] of usageMap) {
|
|
152
|
+
result[name] = {
|
|
153
|
+
count: data.count,
|
|
154
|
+
files: Array.from(data.files.values()),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return jsonResponse(result);
|
|
159
|
+
}
|
|
160
|
+
|
|
99
161
|
// Note: Write handlers (handleSaveComponentRoute, handleSaveComponentJavaScriptRoute,
|
|
100
162
|
// handleSaveComponentCSSRoute, handleComponentCategoryRoute) moved to @meno/studio
|
|
101
163
|
|
|
@@ -201,6 +201,14 @@ export async function handleCoreApiRoutes(
|
|
|
201
201
|
);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Component usage API route (read-only)
|
|
205
|
+
if (url.pathname === API_ROUTES.COMPONENT_USAGE && req.method === 'GET') {
|
|
206
|
+
return await handleRouteError(
|
|
207
|
+
() => Promise.resolve(componentsRoutes.handleComponentUsageRoute(pageService, componentService)),
|
|
208
|
+
'Failed to fetch component usage'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
// Slug mappings API route (read-only)
|
|
205
213
|
if (url.pathname === '/api/slug-mappings' && req.method === 'GET') {
|
|
206
214
|
return await handleRouteError(
|
|
@@ -116,6 +116,8 @@ interface SSRContext {
|
|
|
116
116
|
neededCollections?: Set<string>;
|
|
117
117
|
/** When true, draft items are filtered out from CMS lists */
|
|
118
118
|
isProductionBuild?: boolean;
|
|
119
|
+
/** Collector for SSR fallback HTML of complex nodes (list, locale-list) keyed by element path */
|
|
120
|
+
ssrFallbackCollector?: Map<string, string>;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/**
|
|
@@ -364,15 +366,17 @@ export async function buildComponentHTML(
|
|
|
364
366
|
cmsContext?: CMSContext,
|
|
365
367
|
cmsService?: CMSService,
|
|
366
368
|
isProductionBuild?: boolean
|
|
367
|
-
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string> }> {
|
|
369
|
+
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string> }> {
|
|
368
370
|
// Create map to collect interactive styles during render
|
|
369
371
|
const interactiveStylesMap = new Map<string, InteractiveStyles>();
|
|
370
372
|
// Create array to collect high-priority images for preloading
|
|
371
373
|
const preloadImages: PreloadImage[] = [];
|
|
372
374
|
// Create set to track collections that need client-side data injection
|
|
373
375
|
const neededCollections = new Set<string>();
|
|
376
|
+
// Create map to collect SSR fallback HTML for complex nodes (list, locale-list)
|
|
377
|
+
const ssrFallbackCollector = new Map<string, string>();
|
|
374
378
|
|
|
375
|
-
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections };
|
|
379
|
+
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
376
380
|
|
|
377
381
|
// Register components for this render
|
|
378
382
|
ssrComponentRegistry.merge(globalComponents);
|
|
@@ -403,11 +407,12 @@ export async function buildComponentHTML(
|
|
|
403
407
|
preloadImages, // Collect high-priority images for preloading
|
|
404
408
|
neededCollections, // Track collections that need client-side data
|
|
405
409
|
isProductionBuild,
|
|
410
|
+
ssrFallbackCollector, // Collect SSR fallback HTML for complex nodes
|
|
406
411
|
};
|
|
407
412
|
|
|
408
413
|
const html = await renderNode(node, ctx);
|
|
409
414
|
|
|
410
|
-
return { html, interactiveStylesMap, preloadImages, neededCollections };
|
|
415
|
+
return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
411
416
|
}
|
|
412
417
|
|
|
413
418
|
/**
|
|
@@ -581,7 +586,14 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
|
|
|
581
586
|
}
|
|
582
587
|
|
|
583
588
|
// List is a pure repeater - no container element, just children + optional template
|
|
584
|
-
|
|
589
|
+
const listResult = childrenHTML + templateHtml;
|
|
590
|
+
|
|
591
|
+
// Store SSR fallback for Astro export (list nodes can't be expressed as static Astro components)
|
|
592
|
+
if (ctx.ssrFallbackCollector && ctx.elementPath) {
|
|
593
|
+
ctx.ssrFallbackCollector.set(ctx.elementPath.join('.'), listResult);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return listResult;
|
|
585
597
|
}
|
|
586
598
|
|
|
587
599
|
/**
|
|
@@ -875,8 +887,8 @@ async function renderNode(
|
|
|
875
887
|
// Sanitize HTML with allowlist for SVG, rich-text formatting, and common elements (same as client)
|
|
876
888
|
const purify = getDOMPurify();
|
|
877
889
|
const sanitizedHtml = purify ? purify.sanitize(htmlContent, {
|
|
878
|
-
ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas', 'b', 'i', 'u', 'strong', 'em', 'sub', 'sup', 'mark', 's', 'small', 'del', 'ins', 'q', 'abbr', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
|
879
|
-
ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'stroke-opacity', 'font-size', 'font-family', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity', 'frameborder', 'allowfullscreen', 'allow', 'title'],
|
|
890
|
+
ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas', 'b', 'i', 'u', 'strong', 'em', 'sub', 'sup', 'mark', 's', 'small', 'del', 'ins', 'q', 'abbr', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'style', 'animate', 'animateTransform', 'animateMotion', 'set'],
|
|
891
|
+
ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'stroke-opacity', 'font-size', 'font-family', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity', 'frameborder', 'allowfullscreen', 'allow', 'title', 'attributeName', 'values', 'dur', 'begin', 'end', 'repeatCount', 'repeatDur', 'keyTimes', 'keySplines', 'calcMode', 'from', 'to', 'by', 'additive', 'accumulate', 'type', 'rotate', 'keyPoints', 'path'],
|
|
880
892
|
KEEP_CONTENT: true
|
|
881
893
|
}) : htmlContent;
|
|
882
894
|
const optimizedHtml = ctx.imageMetadataMap
|
|
@@ -1639,7 +1651,14 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
|
|
|
1639
1651
|
const nodeAttributes = extractAttributesFromNode(node as any);
|
|
1640
1652
|
const attrsStr = buildAttributes(nodeAttributes);
|
|
1641
1653
|
|
|
1642
|
-
|
|
1654
|
+
const localeListResult = `<div data-locale-list="true"${containerClassAttr}${localeListStyleAttr}${attrsStr}>${linksHTML}</div>`;
|
|
1655
|
+
|
|
1656
|
+
// Store SSR fallback for Astro export (locale-list nodes can't be expressed as static Astro components)
|
|
1657
|
+
if (ctx.ssrFallbackCollector && ctx.elementPath) {
|
|
1658
|
+
ctx.ssrFallbackCollector.set(ctx.elementPath.join('.'), localeListResult);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
return localeListResult;
|
|
1643
1662
|
}
|
|
1644
1663
|
// If context is missing, return empty div
|
|
1645
1664
|
return '<div data-locale-list="true"></div>';
|
|
@@ -1659,7 +1678,7 @@ export async function renderPageSSR(
|
|
|
1659
1678
|
cmsContext?: CMSContext,
|
|
1660
1679
|
cmsService?: CMSService,
|
|
1661
1680
|
isProductionBuild?: boolean
|
|
1662
|
-
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string> }> {
|
|
1681
|
+
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string> }> {
|
|
1663
1682
|
// Extract page content
|
|
1664
1683
|
const rootNode = pageData?.root || undefined;
|
|
1665
1684
|
if (!rootNode) {
|
|
@@ -1695,9 +1714,9 @@ export async function renderPageSSR(
|
|
|
1695
1714
|
|
|
1696
1715
|
// Render the component tree to HTML with i18n and CMS support
|
|
1697
1716
|
// Also collect interactive styles, preload images, and needed collections during render
|
|
1698
|
-
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections } = rootNode
|
|
1717
|
+
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector } = rootNode
|
|
1699
1718
|
? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild)
|
|
1700
|
-
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>() };
|
|
1719
|
+
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>() };
|
|
1701
1720
|
|
|
1702
1721
|
// Collect JavaScript and CSS from all components
|
|
1703
1722
|
const javascript = await collectComponentJavaScript(globalComponents, pageComponents);
|
|
@@ -1728,5 +1747,6 @@ export async function renderPageSSR(
|
|
|
1728
1747
|
interactiveStylesMap,
|
|
1729
1748
|
preloadImages,
|
|
1730
1749
|
neededCollections,
|
|
1750
|
+
ssrFallbackCollector,
|
|
1731
1751
|
};
|
|
1732
1752
|
}
|