meno-core 1.0.39 → 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.
- package/bin/cli.ts +33 -0
- package/build-astro.ts +172 -69
- package/dist/bin/cli.js +30 -2
- package/dist/bin/cli.js.map +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-WK5XLASY.js → chunk-EQOSDQS2.js} +4 -4
- package/dist/chunks/{chunk-AIXKUVNG.js → chunk-IBR2F4IL.js} +4 -5
- package/dist/chunks/{chunk-AIXKUVNG.js.map → chunk-IBR2F4IL.js.map} +2 -2
- package/dist/chunks/{chunk-NV25WXCA.js → chunk-IGVQF5GY.js} +11 -7
- package/dist/chunks/chunk-IGVQF5GY.js.map +7 -0
- package/dist/chunks/{chunk-KULPBDC7.js → chunk-LBWIHPN7.js} +9 -3
- package/dist/chunks/chunk-LBWIHPN7.js.map +7 -0
- package/dist/chunks/{chunk-A6KWUEA6.js → chunk-MKB2J6AD.js} +9 -1
- package/dist/chunks/chunk-MKB2J6AD.js.map +7 -0
- package/dist/chunks/{chunk-P3FX5HJM.js → chunk-S2HXJTAF.js} +1 -1
- package/dist/chunks/chunk-S2HXJTAF.js.map +7 -0
- package/dist/chunks/{chunk-W6HDII4T.js → chunk-SK3TLNUP.js} +140 -114
- package/dist/chunks/chunk-SK3TLNUP.js.map +7 -0
- package/dist/chunks/{chunk-HNAS6BSS.js → chunk-SNUROC7E.js} +56 -6
- package/dist/chunks/{chunk-HNAS6BSS.js.map → chunk-SNUROC7E.js.map} +3 -3
- package/dist/chunks/{configService-TXBNUBBL.js → configService-MICL4S2L.js} +2 -2
- package/dist/chunks/{constants-5CRJRQNR.js → constants-ZEU4TZCA.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +11 -6
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +507 -1587
- 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/ComponentBuilder.ts +1 -1
- package/lib/client/core/builders/embedBuilder.ts +2 -2
- package/lib/client/routing/Router.tsx +6 -0
- package/lib/client/templateEngine.test.ts +178 -0
- package/lib/client/templateEngine.ts +1 -2
- package/lib/server/astro/cmsPageEmitter.ts +420 -0
- package/lib/server/astro/componentEmitter.ts +150 -17
- package/lib/server/astro/nodeToAstro.test.ts +1101 -0
- package/lib/server/astro/nodeToAstro.ts +869 -37
- package/lib/server/astro/pageEmitter.ts +43 -3
- package/lib/server/astro/tailwindMapper.ts +69 -8
- package/lib/server/astro/templateTransformer.ts +107 -0
- package/lib/server/index.ts +26 -3
- package/lib/server/routes/api/components.ts +62 -0
- package/lib/server/routes/api/core-routes.ts +8 -0
- package/lib/server/services/configService.ts +12 -0
- package/lib/server/ssr/htmlGenerator.ts +0 -5
- package/lib/server/ssr/imageMetadata.ts +3 -3
- package/lib/server/ssr/ssrRenderer.ts +78 -29
- 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 +4 -0
- package/lib/shared/types/components.ts +9 -4
- package/lib/shared/validation/propValidator.ts +2 -1
- package/lib/shared/validation/schemas.ts +4 -1
- package/package.json +1 -1
- package/templates/index-router.html +0 -5
- package/dist/chunks/chunk-A6KWUEA6.js.map +0 -7
- package/dist/chunks/chunk-KULPBDC7.js.map +0 -7
- 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-EQOSDQS2.js.map} +0 -0
- /package/dist/chunks/{configService-TXBNUBBL.js.map → configService-MICL4S2L.js.map} +0 -0
- /package/dist/chunks/{constants-5CRJRQNR.js.map → constants-ZEU4TZCA.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,16 @@ 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[];
|
|
54
|
+
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> */
|
|
55
|
+
imageFormat?: 'webp' | 'avif';
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
// ---------------------------------------------------------------------------
|
|
@@ -88,6 +100,11 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
88
100
|
ssrFallbacks,
|
|
89
101
|
pageName,
|
|
90
102
|
breakpoints: breakpointsOpt,
|
|
103
|
+
imageMetadataMap,
|
|
104
|
+
i18nConfig,
|
|
105
|
+
currentPageSlugMap,
|
|
106
|
+
slugMappings,
|
|
107
|
+
imageFormat,
|
|
91
108
|
} = options;
|
|
92
109
|
|
|
93
110
|
const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
|
|
@@ -110,6 +127,15 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
110
127
|
fileType: 'page',
|
|
111
128
|
fileName: pageName,
|
|
112
129
|
breakpoints,
|
|
130
|
+
imageMetadataMap,
|
|
131
|
+
locale,
|
|
132
|
+
i18nConfig,
|
|
133
|
+
currentPageSlugMap,
|
|
134
|
+
frontmatterLines: [],
|
|
135
|
+
astroImports: new Set<string>(),
|
|
136
|
+
slugMappings,
|
|
137
|
+
i18nDefaultLocale: i18nConfig?.defaultLocale,
|
|
138
|
+
imageFormat,
|
|
113
139
|
};
|
|
114
140
|
|
|
115
141
|
// Emit the template body
|
|
@@ -117,6 +143,13 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
117
143
|
|
|
118
144
|
// Build frontmatter with imports
|
|
119
145
|
const importLines: string[] = [];
|
|
146
|
+
|
|
147
|
+
// Add Astro API imports (e.g., getCollection) if collected during emission
|
|
148
|
+
if (ctx.astroImports && ctx.astroImports.size > 0) {
|
|
149
|
+
const astroImports = Array.from(ctx.astroImports);
|
|
150
|
+
importLines.push(`import { ${astroImports.join(', ')} } from 'astro:content';`);
|
|
151
|
+
}
|
|
152
|
+
|
|
120
153
|
importLines.push(`import BaseLayout from '${layoutImportPath}';`);
|
|
121
154
|
|
|
122
155
|
// Sort component imports alphabetically
|
|
@@ -137,8 +170,13 @@ export function emitAstroPage(options: PageEmitOptions): string {
|
|
|
137
170
|
const escapedMeta = escapeTemplateLiteral(meta);
|
|
138
171
|
const escapedFontPreloads = escapeTemplateLiteral(fontPreloads);
|
|
139
172
|
|
|
173
|
+
// Collect frontmatter lines from list emission (e.g., getCollection calls)
|
|
174
|
+
const extraFrontmatter = ctx.frontmatterLines && ctx.frontmatterLines.length > 0
|
|
175
|
+
? '\n' + ctx.frontmatterLines.join('\n')
|
|
176
|
+
: '';
|
|
177
|
+
|
|
140
178
|
return `---
|
|
141
|
-
${importLines.join('\n')}
|
|
179
|
+
${importLines.join('\n')}${extraFrontmatter}
|
|
142
180
|
---
|
|
143
181
|
<BaseLayout
|
|
144
182
|
title="${escapeJSX(title)}"
|
|
@@ -149,7 +187,9 @@ ${importLines.join('\n')}
|
|
|
149
187
|
fontPreloads={\`${escapedFontPreloads}\`}
|
|
150
188
|
libraryTags={${libraryTagsLiteral}}
|
|
151
189
|
>
|
|
152
|
-
|
|
190
|
+
<div id="root">
|
|
191
|
+
${templateBody} </div>
|
|
192
|
+
</BaseLayout>
|
|
153
193
|
`;
|
|
154
194
|
}
|
|
155
195
|
|
|
@@ -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
|
@@ -17,7 +17,7 @@ export { generateBuildErrorPage, type BuildError, type BuildErrorsData } from '.
|
|
|
17
17
|
export { PageService } from './services/pageService';
|
|
18
18
|
export { ComponentService, type ComponentInfo } from './services/componentService';
|
|
19
19
|
export { CMSService, type ReferenceLocation } from './services/cmsService';
|
|
20
|
-
export { configService, ConfigService } from './services/configService';
|
|
20
|
+
export { configService, ConfigService, type ImageFormat } from './services/configService';
|
|
21
21
|
export { ColorService, colorService } from './services/ColorService';
|
|
22
22
|
export { VariableService, variableService } from './services/VariableService';
|
|
23
23
|
export { EnumService, enumService } from './services/EnumService';
|
|
@@ -62,8 +62,31 @@ export { migrateTemplatesDirectory } from './migrateTemplates';
|
|
|
62
62
|
// Static build
|
|
63
63
|
export { buildStaticPages } from '../../build-static';
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
export {
|
|
65
|
+
// Webflow export
|
|
66
|
+
export { buildWebflowPayload } from './webflow';
|
|
67
|
+
export type {
|
|
68
|
+
WebflowExportPayload,
|
|
69
|
+
WebflowPage,
|
|
70
|
+
WebflowElement,
|
|
71
|
+
WebflowStyleClass,
|
|
72
|
+
} from './webflow';
|
|
73
|
+
|
|
74
|
+
// JSON loaders
|
|
75
|
+
export {
|
|
76
|
+
loadJSONFile,
|
|
77
|
+
loadComponentDirectory,
|
|
78
|
+
mapPageNameToPath,
|
|
79
|
+
parseJSON,
|
|
80
|
+
loadI18nConfig,
|
|
81
|
+
loadBreakpointConfig,
|
|
82
|
+
loadResponsiveScalesConfig,
|
|
83
|
+
} from './jsonLoader';
|
|
84
|
+
|
|
85
|
+
// CSS generation
|
|
86
|
+
export { generateThemeColorVariablesCSS, generateVariablesCSS } from './cssGenerator';
|
|
87
|
+
|
|
88
|
+
// Font utilities
|
|
89
|
+
export { generateFontCSS, generateFontPreloadTags } from '../shared/fontLoader';
|
|
67
90
|
|
|
68
91
|
// Utilities
|
|
69
92
|
export * from './utils';
|
|
@@ -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(
|
|
@@ -28,6 +28,8 @@ export interface IconsConfig {
|
|
|
28
28
|
/**
|
|
29
29
|
* Raw project config structure from project.config.json
|
|
30
30
|
*/
|
|
31
|
+
export type ImageFormat = 'webp' | 'avif';
|
|
32
|
+
|
|
31
33
|
interface RawProjectConfig {
|
|
32
34
|
breakpoints?: BreakpointConfigInput;
|
|
33
35
|
responsiveScales?: Partial<ResponsiveScales>;
|
|
@@ -36,6 +38,7 @@ interface RawProjectConfig {
|
|
|
36
38
|
libraries?: LibrariesConfig;
|
|
37
39
|
csp?: CSPConfig;
|
|
38
40
|
baseComponent?: string;
|
|
41
|
+
imageFormat?: ImageFormat;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -253,6 +256,15 @@ export class ConfigService {
|
|
|
253
256
|
return this.config.baseComponent;
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Get image format setting
|
|
261
|
+
* Returns 'webp' (default) or 'avif'
|
|
262
|
+
*/
|
|
263
|
+
getImageFormat(): ImageFormat {
|
|
264
|
+
if (this.config?.imageFormat === 'avif') return 'avif';
|
|
265
|
+
return 'webp';
|
|
266
|
+
}
|
|
267
|
+
|
|
256
268
|
/**
|
|
257
269
|
* Get raw config value by key (for extension)
|
|
258
270
|
*/
|
|
@@ -135,7 +135,7 @@ export function extractImgSrc(imgTag: string): string | null {
|
|
|
135
135
|
* Only rewrites images that exist in the metadata map (project images with generated variants).
|
|
136
136
|
* External/unrecognized images are left unchanged.
|
|
137
137
|
*/
|
|
138
|
-
export function rewriteRichTextImages(html: string, metadataMap: ImageMetadataMap): string {
|
|
138
|
+
export function rewriteRichTextImages(html: string, metadataMap: ImageMetadataMap, imageFormat?: 'webp' | 'avif'): string {
|
|
139
139
|
return html.replace(/<img\b([^>]*)\/?>/gi, (fullMatch, innerAttrs: string) => {
|
|
140
140
|
const src = extractImgSrc(fullMatch);
|
|
141
141
|
if (!src) return fullMatch;
|
|
@@ -158,8 +158,8 @@ export function rewriteRichTextImages(html: string, metadataMap: ImageMetadataMa
|
|
|
158
158
|
|
|
159
159
|
const sizes = DEFAULT_SIZES;
|
|
160
160
|
|
|
161
|
-
// Render as <picture> with AVIF + WebP sources when AVIF is available
|
|
162
|
-
if (metadata.avifSrcset) {
|
|
161
|
+
// Render as <picture> with AVIF + WebP sources when AVIF is available and format allows
|
|
162
|
+
if (metadata.avifSrcset && imageFormat !== 'webp') {
|
|
163
163
|
return `<picture>` +
|
|
164
164
|
`<source type="image/avif" srcset="${escapeHtml(metadata.avifSrcset)}" sizes="${escapeHtml(sizes)}" />` +
|
|
165
165
|
`<source type="image/webp" srcset="${escapeHtml(metadata.srcset)}" sizes="${escapeHtml(sizes)}" />` +
|