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
|
@@ -12,6 +12,7 @@ import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
|
12
12
|
import { evaluateTemplate, processStructure, isResponsiveStyle, isHtmlMapping, resolveHtmlMapping } from '../../client/templateEngine';
|
|
13
13
|
import { resolvePropsFromDefinition, isRichTextMarker, richTextMarkerToHtml } from '../../shared/propResolver';
|
|
14
14
|
import { loadBreakpointConfig, loadI18nConfig } from '../jsonLoader';
|
|
15
|
+
import { configService } from '../services/configService';
|
|
15
16
|
import type { I18nConfig } from '../../shared/types/components';
|
|
16
17
|
import { extractLocaleFromPath, DEFAULT_I18N_CONFIG, resolveI18nValue, buildLocalizedPath, isI18nValue } from '../../shared/i18n';
|
|
17
18
|
import { NODE_TYPE } from '../../shared/constants';
|
|
@@ -116,6 +117,10 @@ interface SSRContext {
|
|
|
116
117
|
neededCollections?: Set<string>;
|
|
117
118
|
/** When true, draft items are filtered out from CMS lists */
|
|
118
119
|
isProductionBuild?: boolean;
|
|
120
|
+
/** Collector for SSR fallback HTML of complex nodes (list, locale-list) keyed by element path */
|
|
121
|
+
ssrFallbackCollector?: Map<string, string>;
|
|
122
|
+
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
|
|
123
|
+
imageFormat?: 'webp' | 'avif';
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
/**
|
|
@@ -364,15 +369,17 @@ export async function buildComponentHTML(
|
|
|
364
369
|
cmsContext?: CMSContext,
|
|
365
370
|
cmsService?: CMSService,
|
|
366
371
|
isProductionBuild?: boolean
|
|
367
|
-
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string> }> {
|
|
372
|
+
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string> }> {
|
|
368
373
|
// Create map to collect interactive styles during render
|
|
369
374
|
const interactiveStylesMap = new Map<string, InteractiveStyles>();
|
|
370
375
|
// Create array to collect high-priority images for preloading
|
|
371
376
|
const preloadImages: PreloadImage[] = [];
|
|
372
377
|
// Create set to track collections that need client-side data injection
|
|
373
378
|
const neededCollections = new Set<string>();
|
|
379
|
+
// Create map to collect SSR fallback HTML for complex nodes (list, locale-list)
|
|
380
|
+
const ssrFallbackCollector = new Map<string, string>();
|
|
374
381
|
|
|
375
|
-
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections };
|
|
382
|
+
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
376
383
|
|
|
377
384
|
// Register components for this render
|
|
378
385
|
ssrComponentRegistry.merge(globalComponents);
|
|
@@ -403,11 +410,13 @@ export async function buildComponentHTML(
|
|
|
403
410
|
preloadImages, // Collect high-priority images for preloading
|
|
404
411
|
neededCollections, // Track collections that need client-side data
|
|
405
412
|
isProductionBuild,
|
|
413
|
+
ssrFallbackCollector, // Collect SSR fallback HTML for complex nodes
|
|
414
|
+
imageFormat: configService.getImageFormat(),
|
|
406
415
|
};
|
|
407
416
|
|
|
408
417
|
const html = await renderNode(node, ctx);
|
|
409
418
|
|
|
410
|
-
return { html, interactiveStylesMap, preloadImages, neededCollections };
|
|
419
|
+
return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
/**
|
|
@@ -581,7 +590,14 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
|
|
|
581
590
|
}
|
|
582
591
|
|
|
583
592
|
// List is a pure repeater - no container element, just children + optional template
|
|
584
|
-
|
|
593
|
+
const listResult = childrenHTML + templateHtml;
|
|
594
|
+
|
|
595
|
+
// Store SSR fallback for Astro export (list nodes can't be expressed as static Astro components)
|
|
596
|
+
if (ctx.ssrFallbackCollector && ctx.elementPath) {
|
|
597
|
+
ctx.ssrFallbackCollector.set(ctx.elementPath.join('.'), listResult);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return listResult;
|
|
585
601
|
}
|
|
586
602
|
|
|
587
603
|
/**
|
|
@@ -788,7 +804,7 @@ async function renderNode(
|
|
|
788
804
|
// Check for raw HTML marker (from rich-text fields) - don't escape
|
|
789
805
|
if (text.startsWith(RAW_HTML_PREFIX)) {
|
|
790
806
|
let rawHtml = text.slice(RAW_HTML_PREFIX.length);
|
|
791
|
-
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap);
|
|
807
|
+
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
|
|
792
808
|
rawHtml = await expandRichTextComponents(rawHtml, ctx);
|
|
793
809
|
rawHtml = localizeRichTextLinks(rawHtml, ctx);
|
|
794
810
|
return rawHtml;
|
|
@@ -808,7 +824,7 @@ async function renderNode(
|
|
|
808
824
|
// Check for raw HTML marker (from rich-text fields) - don't escape
|
|
809
825
|
if (text.startsWith(RAW_HTML_PREFIX)) {
|
|
810
826
|
let rawHtml = text.slice(RAW_HTML_PREFIX.length);
|
|
811
|
-
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap);
|
|
827
|
+
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
|
|
812
828
|
rawHtml = await expandRichTextComponents(rawHtml, ctx);
|
|
813
829
|
rawHtml = localizeRichTextLinks(rawHtml, ctx);
|
|
814
830
|
return rawHtml;
|
|
@@ -875,12 +891,12 @@ async function renderNode(
|
|
|
875
891
|
// Sanitize HTML with allowlist for SVG, rich-text formatting, and common elements (same as client)
|
|
876
892
|
const purify = getDOMPurify();
|
|
877
893
|
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'],
|
|
894
|
+
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'],
|
|
895
|
+
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
896
|
KEEP_CONTENT: true
|
|
881
897
|
}) : htmlContent;
|
|
882
898
|
const optimizedHtml = ctx.imageMetadataMap
|
|
883
|
-
? rewriteRichTextImages(sanitizedHtml, ctx.imageMetadataMap)
|
|
899
|
+
? rewriteRichTextImages(sanitizedHtml, ctx.imageMetadataMap, ctx.imageFormat)
|
|
884
900
|
: sanitizedHtml;
|
|
885
901
|
|
|
886
902
|
// Extract attributes from node
|
|
@@ -1215,27 +1231,52 @@ async function renderComponent(
|
|
|
1215
1231
|
// Merge instance style overrides, className, and attributes
|
|
1216
1232
|
// processedStructure is typically an HTML node (the component's root element)
|
|
1217
1233
|
// Handle both component nodes and HTML nodes
|
|
1218
|
-
const rootNode = processedStructure as ComponentNode & { props?: Record<string, unknown> };
|
|
1234
|
+
const rootNode = processedStructure as ComponentNode & { props?: Record<string, unknown>; style?: any; attributes?: Record<string, string | number | boolean> };
|
|
1219
1235
|
if (isComponentNode(rootNode) || isHtmlNode(rootNode)) {
|
|
1220
1236
|
if (!rootNode.props) {
|
|
1221
1237
|
rootNode.props = {};
|
|
1222
1238
|
}
|
|
1223
1239
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1240
|
+
// For HTML root nodes, merge styles into node.style (where the renderer reads them)
|
|
1241
|
+
if (isHtmlNode(rootNode)) {
|
|
1242
|
+
if (propsWithStyleAndAttrs.style) {
|
|
1243
|
+
const existingStyle = rootNode.style;
|
|
1244
|
+
if (existingStyle && typeof existingStyle === 'object') {
|
|
1245
|
+
rootNode.style = {
|
|
1246
|
+
...(existingStyle as Record<string, any>),
|
|
1247
|
+
...(propsWithStyleAndAttrs.style as Record<string, any>)
|
|
1248
|
+
};
|
|
1249
|
+
} else {
|
|
1250
|
+
rootNode.style = propsWithStyleAndAttrs.style as any;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
} else {
|
|
1254
|
+
// For component root nodes, merge into props.style
|
|
1255
|
+
if (rootNode.props.style && typeof rootNode.props.style === 'object') {
|
|
1256
|
+
rootNode.props.style = {
|
|
1257
|
+
...(rootNode.props.style as Record<string, unknown>),
|
|
1258
|
+
...(propsWithStyleAndAttrs.style as Record<string, unknown> || {})
|
|
1259
|
+
};
|
|
1260
|
+
} else if (propsWithStyleAndAttrs.style) {
|
|
1261
|
+
rootNode.props.style = propsWithStyleAndAttrs.style;
|
|
1262
|
+
}
|
|
1231
1263
|
}
|
|
1232
1264
|
|
|
1233
1265
|
// Merge className from component instance (includes responsive utility classes)
|
|
1234
1266
|
if (propsWithStyleAndAttrs.className) {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1267
|
+
if (isHtmlNode(rootNode)) {
|
|
1268
|
+
// For HTML root nodes, merge into attributes where the renderer reads them
|
|
1269
|
+
if (!rootNode.attributes) rootNode.attributes = {};
|
|
1270
|
+
const existingClass = (rootNode.attributes.class || '') as string;
|
|
1271
|
+
rootNode.attributes.class = existingClass
|
|
1272
|
+
? `${existingClass} ${propsWithStyleAndAttrs.className}`
|
|
1273
|
+
: propsWithStyleAndAttrs.className as string;
|
|
1274
|
+
} else {
|
|
1275
|
+
const existingClassName = rootNode.props.className || '';
|
|
1276
|
+
rootNode.props.className = existingClassName
|
|
1277
|
+
? `${existingClassName} ${propsWithStyleAndAttrs.className}`
|
|
1278
|
+
: propsWithStyleAndAttrs.className;
|
|
1279
|
+
}
|
|
1239
1280
|
}
|
|
1240
1281
|
|
|
1241
1282
|
// Merge attributes into props
|
|
@@ -1428,8 +1469,8 @@ function renderImageElement(
|
|
|
1428
1469
|
|
|
1429
1470
|
// Collect high-priority images for preloading in head
|
|
1430
1471
|
if (fetchpriority === 'high' && metadata && ctx.preloadImages) {
|
|
1431
|
-
// Prefer AVIF, fallback to WebP
|
|
1432
|
-
if (metadata.avifSrcset) {
|
|
1472
|
+
// Prefer AVIF when format allows, fallback to WebP
|
|
1473
|
+
if (metadata.avifSrcset && ctx.imageFormat !== 'webp') {
|
|
1433
1474
|
ctx.preloadImages.push({
|
|
1434
1475
|
srcset: metadata.avifSrcset,
|
|
1435
1476
|
type: 'image/avif',
|
|
@@ -1460,9 +1501,9 @@ function renderImageElement(
|
|
|
1460
1501
|
blurStyle = ` style="background-image: url(${escapeHtml(metadata.blurHash)}); background-size: cover;" onload="this.style.backgroundImage=''"`;
|
|
1461
1502
|
}
|
|
1462
1503
|
|
|
1463
|
-
// Render as <picture> element if AVIF is available
|
|
1504
|
+
// Render as <picture> element if AVIF is available and imageFormat is 'avif'
|
|
1464
1505
|
// Layout classes go on <picture> (block container), image-specific classes on <img>
|
|
1465
|
-
if (metadata?.avifSrcset) {
|
|
1506
|
+
if (metadata?.avifSrcset && ctx.imageFormat !== 'webp') {
|
|
1466
1507
|
// Image-specific class prefixes that should stay on <img>
|
|
1467
1508
|
const imgClassPrefixes = [
|
|
1468
1509
|
'objf-', 'objp-', 'flt-', 'tm-', 'bs-',
|
|
@@ -1639,7 +1680,14 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
|
|
|
1639
1680
|
const nodeAttributes = extractAttributesFromNode(node as any);
|
|
1640
1681
|
const attrsStr = buildAttributes(nodeAttributes);
|
|
1641
1682
|
|
|
1642
|
-
|
|
1683
|
+
const localeListResult = `<div data-locale-list="true"${containerClassAttr}${localeListStyleAttr}${attrsStr}>${linksHTML}</div>`;
|
|
1684
|
+
|
|
1685
|
+
// Store SSR fallback for Astro export (locale-list nodes can't be expressed as static Astro components)
|
|
1686
|
+
if (ctx.ssrFallbackCollector && ctx.elementPath) {
|
|
1687
|
+
ctx.ssrFallbackCollector.set(ctx.elementPath.join('.'), localeListResult);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
return localeListResult;
|
|
1643
1691
|
}
|
|
1644
1692
|
// If context is missing, return empty div
|
|
1645
1693
|
return '<div data-locale-list="true"></div>';
|
|
@@ -1659,7 +1707,7 @@ export async function renderPageSSR(
|
|
|
1659
1707
|
cmsContext?: CMSContext,
|
|
1660
1708
|
cmsService?: CMSService,
|
|
1661
1709
|
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> }> {
|
|
1710
|
+
): 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
1711
|
// Extract page content
|
|
1664
1712
|
const rootNode = pageData?.root || undefined;
|
|
1665
1713
|
if (!rootNode) {
|
|
@@ -1695,9 +1743,9 @@ export async function renderPageSSR(
|
|
|
1695
1743
|
|
|
1696
1744
|
// Render the component tree to HTML with i18n and CMS support
|
|
1697
1745
|
// Also collect interactive styles, preload images, and needed collections during render
|
|
1698
|
-
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections } = rootNode
|
|
1746
|
+
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector } = rootNode
|
|
1699
1747
|
? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild)
|
|
1700
|
-
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>() };
|
|
1748
|
+
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>() };
|
|
1701
1749
|
|
|
1702
1750
|
// Collect JavaScript and CSS from all components
|
|
1703
1751
|
const javascript = await collectComponentJavaScript(globalComponents, pageComponents);
|
|
@@ -1728,5 +1776,6 @@ export async function renderPageSSR(
|
|
|
1728
1776
|
interactiveStylesMap,
|
|
1729
1777
|
preloadImages,
|
|
1730
1778
|
neededCollections,
|
|
1779
|
+
ssrFallbackCollector,
|
|
1731
1780
|
};
|
|
1732
1781
|
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webflow Export Build Orchestrator
|
|
3
|
+
* Loads the project, renders all pages, and converts to Webflow payload.
|
|
4
|
+
* Mirrors the pattern of build-astro.ts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync } from 'fs';
|
|
8
|
+
import { readFile } from 'fs/promises';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import {
|
|
11
|
+
loadJSONFile,
|
|
12
|
+
loadComponentDirectory,
|
|
13
|
+
mapPageNameToPath,
|
|
14
|
+
parseJSON,
|
|
15
|
+
loadI18nConfig,
|
|
16
|
+
loadBreakpointConfig,
|
|
17
|
+
loadResponsiveScalesConfig,
|
|
18
|
+
} from '../jsonLoader';
|
|
19
|
+
import { renderPageSSR } from '../ssr/ssrRenderer';
|
|
20
|
+
import { projectPaths } from '../projectContext';
|
|
21
|
+
import { loadProjectConfig } from '../../shared/fontLoader';
|
|
22
|
+
import { FileSystemCMSProvider } from '../providers/fileSystemCMSProvider';
|
|
23
|
+
import { CMSService } from '../services/cmsService';
|
|
24
|
+
import { isI18nValue, resolveI18nValue } from '../../shared/i18n';
|
|
25
|
+
import { configService } from '../services/configService';
|
|
26
|
+
import { colorService } from '../services/ColorService';
|
|
27
|
+
import { variableService } from '../services/VariableService';
|
|
28
|
+
import { migrateTemplatesDirectory } from '../migrateTemplates';
|
|
29
|
+
import type {
|
|
30
|
+
ComponentDefinition,
|
|
31
|
+
JSONPage,
|
|
32
|
+
CMSSchema,
|
|
33
|
+
CMSItem,
|
|
34
|
+
I18nConfig,
|
|
35
|
+
} from '../../shared/types';
|
|
36
|
+
import { isItemDraftForLocale } from '../../shared/types';
|
|
37
|
+
import type { SlugMap } from '../../shared/slugTranslator';
|
|
38
|
+
import type {
|
|
39
|
+
WebflowExportPayload,
|
|
40
|
+
WebflowPage,
|
|
41
|
+
WebflowStyleClass,
|
|
42
|
+
WebflowCMSCollection,
|
|
43
|
+
WebflowCMSField,
|
|
44
|
+
WebflowAssetRef,
|
|
45
|
+
} from './types';
|
|
46
|
+
import { mapCMSFieldType } from './types';
|
|
47
|
+
import { nodeToWebflow, type WebflowEmitContext } from './nodeToWebflow';
|
|
48
|
+
import { generateThemeColorVariablesCSS, generateVariablesCSS } from '../cssGenerator';
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Helpers
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function scanJSONFiles(dir: string, prefix: string = ''): string[] {
|
|
55
|
+
const results: string[] = [];
|
|
56
|
+
if (!existsSync(dir)) return results;
|
|
57
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
60
|
+
results.push(prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
61
|
+
} else if (entry.isDirectory()) {
|
|
62
|
+
results.push(...scanJSONFiles(join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isCMSPage(pageData: JSONPage): boolean {
|
|
69
|
+
return pageData.meta?.source === 'cms' && !!pageData.meta?.cms;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildCMSItemPath(
|
|
73
|
+
urlPattern: string,
|
|
74
|
+
item: CMSItem,
|
|
75
|
+
slugField: string,
|
|
76
|
+
locale: string,
|
|
77
|
+
i18nConfig: I18nConfig
|
|
78
|
+
): string {
|
|
79
|
+
let slug = item[slugField] ?? item._slug ?? item._id;
|
|
80
|
+
if (isI18nValue(slug)) {
|
|
81
|
+
slug = resolveI18nValue(slug, locale, i18nConfig) as string;
|
|
82
|
+
}
|
|
83
|
+
return urlPattern.replace('{{slug}}', String(slug));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function scanAssets(projectRoot: string): WebflowAssetRef[] {
|
|
87
|
+
const assets: WebflowAssetRef[] = [];
|
|
88
|
+
const assetDirs: Array<{ dir: string; type: WebflowAssetRef['type'] }> = [
|
|
89
|
+
{ dir: 'images', type: 'image' },
|
|
90
|
+
{ dir: 'fonts', type: 'font' },
|
|
91
|
+
{ dir: 'videos', type: 'video' },
|
|
92
|
+
{ dir: 'assets', type: 'file' },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const { dir, type } of assetDirs) {
|
|
96
|
+
const fullDir = join(projectRoot, dir);
|
|
97
|
+
if (!existsSync(fullDir)) continue;
|
|
98
|
+
const files = scanJSONFiles(fullDir).map(f => f.replace('.json', '')); // scanJSONFiles works for any extension
|
|
99
|
+
// Re-scan properly for all file types
|
|
100
|
+
const allFiles = scanAllFiles(fullDir);
|
|
101
|
+
for (const file of allFiles) {
|
|
102
|
+
assets.push({
|
|
103
|
+
localPath: `${dir}/${file}`,
|
|
104
|
+
type,
|
|
105
|
+
fileName: file.split('/').pop()!,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return assets;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scanAllFiles(dir: string, prefix: string = ''): string[] {
|
|
114
|
+
const results: string[] = [];
|
|
115
|
+
if (!existsSync(dir)) return results;
|
|
116
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
119
|
+
if (entry.isFile()) {
|
|
120
|
+
results.push(relativePath);
|
|
121
|
+
} else if (entry.isDirectory()) {
|
|
122
|
+
results.push(...scanAllFiles(join(dir, entry.name), relativePath));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract CSS variables from theme and variables config
|
|
130
|
+
*/
|
|
131
|
+
function extractCSSVariables(
|
|
132
|
+
themeColorCSS: string,
|
|
133
|
+
variablesCSS: string
|
|
134
|
+
): Record<string, string> {
|
|
135
|
+
const vars: Record<string, string> = {};
|
|
136
|
+
const regex = /--([\w-]+)\s*:\s*([^;]+)/g;
|
|
137
|
+
|
|
138
|
+
for (const css of [themeColorCSS, variablesCSS]) {
|
|
139
|
+
let match;
|
|
140
|
+
while ((match = regex.exec(css)) !== null) {
|
|
141
|
+
vars[`--${match[1]}`] = match[2].trim();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return vars;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Main export
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export async function buildWebflowPayload(
|
|
153
|
+
projectRoot?: string
|
|
154
|
+
): Promise<WebflowExportPayload> {
|
|
155
|
+
// 1. Setup: load project configuration
|
|
156
|
+
configService.reset();
|
|
157
|
+
|
|
158
|
+
const projectConfig = await loadProjectConfig();
|
|
159
|
+
const siteUrl = (projectConfig as { siteUrl?: string }).siteUrl?.replace(/\/$/, '') || '';
|
|
160
|
+
const i18nConfig = await loadI18nConfig();
|
|
161
|
+
|
|
162
|
+
await migrateTemplatesDirectory();
|
|
163
|
+
|
|
164
|
+
const { components } = await loadComponentDirectory(projectPaths.components());
|
|
165
|
+
const globalComponents: Record<string, ComponentDefinition> = {};
|
|
166
|
+
components.forEach((value, key) => { globalComponents[key] = value; });
|
|
167
|
+
|
|
168
|
+
const cmsProvider = new FileSystemCMSProvider(projectPaths.templates(), projectPaths.cms());
|
|
169
|
+
const cmsService = new CMSService(cmsProvider);
|
|
170
|
+
await cmsService.initialize();
|
|
171
|
+
|
|
172
|
+
const themeConfig = await colorService.loadThemeConfig();
|
|
173
|
+
const variablesConfig = await variableService.loadConfig();
|
|
174
|
+
const breakpoints = await loadBreakpointConfig();
|
|
175
|
+
const responsiveScales = await loadResponsiveScalesConfig();
|
|
176
|
+
|
|
177
|
+
await configService.load();
|
|
178
|
+
|
|
179
|
+
// 2. Scan pages
|
|
180
|
+
const pagesDir = projectPaths.pages();
|
|
181
|
+
if (!existsSync(pagesDir)) {
|
|
182
|
+
return emptyPayload();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const pageFiles = scanJSONFiles(pagesDir);
|
|
186
|
+
if (pageFiles.length === 0) {
|
|
187
|
+
return emptyPayload();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Collect slug mappings
|
|
191
|
+
const slugMappings: SlugMap[] = [];
|
|
192
|
+
for (const file of pageFiles) {
|
|
193
|
+
const pageName = file.replace('.json', '');
|
|
194
|
+
const basePath = mapPageNameToPath(pageName);
|
|
195
|
+
const pageContent = await loadJSONFile(join(pagesDir, file));
|
|
196
|
+
if (!pageContent) continue;
|
|
197
|
+
try {
|
|
198
|
+
const pageData = parseJSON<JSONPage>(pageContent);
|
|
199
|
+
if (pageData.meta?.slugs) {
|
|
200
|
+
const pageId = basePath === '/' ? 'index' : basePath.substring(1);
|
|
201
|
+
slugMappings.push({ pageId, slugs: pageData.meta.slugs });
|
|
202
|
+
}
|
|
203
|
+
} catch { /* ignore */ }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 3. Render and convert pages
|
|
207
|
+
const allPages: WebflowPage[] = [];
|
|
208
|
+
const allStyleClasses = new Map<string, WebflowStyleClass>();
|
|
209
|
+
|
|
210
|
+
// Regular pages
|
|
211
|
+
for (const file of pageFiles) {
|
|
212
|
+
const pageName = file.replace('.json', '');
|
|
213
|
+
const basePath = mapPageNameToPath(pageName);
|
|
214
|
+
const pageContent = await loadJSONFile(join(pagesDir, file));
|
|
215
|
+
if (!pageContent) continue;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const pageData = parseJSON<JSONPage>(pageContent);
|
|
219
|
+
if (pageData.meta?.draft === true) continue;
|
|
220
|
+
|
|
221
|
+
const slugs = pageData.meta?.slugs;
|
|
222
|
+
|
|
223
|
+
for (const localeConfig of i18nConfig.locales) {
|
|
224
|
+
const locale = localeConfig.code;
|
|
225
|
+
const isDefault = locale === i18nConfig.defaultLocale;
|
|
226
|
+
|
|
227
|
+
let slug: string;
|
|
228
|
+
if (slugs && slugs[locale]) {
|
|
229
|
+
slug = slugs[locale];
|
|
230
|
+
} else if (basePath === '/') {
|
|
231
|
+
slug = '';
|
|
232
|
+
} else {
|
|
233
|
+
slug = basePath.substring(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const urlPath = isDefault
|
|
237
|
+
? (slug === '' ? '/' : `/${slug}`)
|
|
238
|
+
: (slug === '' ? `/${locale}` : `/${locale}/${slug}`);
|
|
239
|
+
|
|
240
|
+
// Render via SSR to resolve all props
|
|
241
|
+
const result = await renderPageSSR(
|
|
242
|
+
pageData,
|
|
243
|
+
globalComponents,
|
|
244
|
+
urlPath,
|
|
245
|
+
siteUrl,
|
|
246
|
+
locale,
|
|
247
|
+
i18nConfig,
|
|
248
|
+
slugMappings,
|
|
249
|
+
undefined,
|
|
250
|
+
cmsService,
|
|
251
|
+
true
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Convert node tree to Webflow elements
|
|
255
|
+
const ctx: WebflowEmitContext = {
|
|
256
|
+
globalComponents,
|
|
257
|
+
elementPath: [0],
|
|
258
|
+
fileType: 'page',
|
|
259
|
+
fileName: pageName,
|
|
260
|
+
breakpoints,
|
|
261
|
+
styleClasses: allStyleClasses,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const body = pageData.root || (pageData as any).node;
|
|
265
|
+
const elements = body ? nodeToWebflow(body, ctx) : [];
|
|
266
|
+
|
|
267
|
+
allPages.push({
|
|
268
|
+
title: result.title,
|
|
269
|
+
slug: slug || 'index',
|
|
270
|
+
metaDescription: typeof pageData.meta?.description === 'string' ? pageData.meta.description : undefined,
|
|
271
|
+
elements,
|
|
272
|
+
locale,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} catch (error: any) {
|
|
276
|
+
console.error(`Error processing ${basePath}:`, error?.message);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// CMS template pages
|
|
281
|
+
const templatesDir = projectPaths.templates();
|
|
282
|
+
const cmsCollections: WebflowCMSCollection[] = [];
|
|
283
|
+
|
|
284
|
+
if (existsSync(templatesDir)) {
|
|
285
|
+
const templateFiles = readdirSync(templatesDir).filter(f => f.endsWith('.json'));
|
|
286
|
+
|
|
287
|
+
for (const file of templateFiles) {
|
|
288
|
+
const templateContent = await loadJSONFile(join(templatesDir, file));
|
|
289
|
+
if (!templateContent) continue;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const pageData = parseJSON<JSONPage>(templateContent);
|
|
293
|
+
if (pageData.meta?.draft === true) continue;
|
|
294
|
+
if (!isCMSPage(pageData)) continue;
|
|
295
|
+
|
|
296
|
+
const cmsSchema = pageData.meta!.cms as CMSSchema;
|
|
297
|
+
const items = await cmsService.queryItems({ collection: cmsSchema.id });
|
|
298
|
+
|
|
299
|
+
// Build Webflow CMS collection
|
|
300
|
+
const fields: WebflowCMSField[] = [];
|
|
301
|
+
if (cmsSchema.fields) {
|
|
302
|
+
for (const [fieldName, fieldDef] of Object.entries(cmsSchema.fields)) {
|
|
303
|
+
fields.push({
|
|
304
|
+
name: fieldDef.label || fieldName,
|
|
305
|
+
slug: fieldName,
|
|
306
|
+
type: mapCMSFieldType(fieldDef.type),
|
|
307
|
+
required: fieldDef.required,
|
|
308
|
+
options: fieldDef.options,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Resolve i18n values in items for default locale
|
|
314
|
+
const resolvedItems: Record<string, unknown>[] = [];
|
|
315
|
+
for (const item of items) {
|
|
316
|
+
const resolved: Record<string, unknown> = {};
|
|
317
|
+
for (const [key, value] of Object.entries(item)) {
|
|
318
|
+
if (isI18nValue(value)) {
|
|
319
|
+
resolved[key] = resolveI18nValue(value, i18nConfig.defaultLocale, i18nConfig);
|
|
320
|
+
} else {
|
|
321
|
+
resolved[key] = value;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
resolvedItems.push(resolved);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
cmsCollections.push({
|
|
328
|
+
name: cmsSchema.id,
|
|
329
|
+
slug: cmsSchema.id,
|
|
330
|
+
urlPattern: cmsSchema.urlPattern,
|
|
331
|
+
fields,
|
|
332
|
+
items: resolvedItems,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Render CMS item pages
|
|
336
|
+
for (const item of items) {
|
|
337
|
+
for (const localeConfig of i18nConfig.locales) {
|
|
338
|
+
const locale = localeConfig.code;
|
|
339
|
+
if (isItemDraftForLocale(item, locale)) continue;
|
|
340
|
+
|
|
341
|
+
const itemPath = buildCMSItemPath(cmsSchema.urlPattern, item, cmsSchema.slugField, locale, i18nConfig);
|
|
342
|
+
const itemWithUrl: CMSItem = { ...item, _url: itemPath };
|
|
343
|
+
|
|
344
|
+
const result = await renderPageSSR(
|
|
345
|
+
pageData,
|
|
346
|
+
globalComponents,
|
|
347
|
+
itemPath,
|
|
348
|
+
siteUrl,
|
|
349
|
+
locale,
|
|
350
|
+
i18nConfig,
|
|
351
|
+
slugMappings,
|
|
352
|
+
{ cms: itemWithUrl },
|
|
353
|
+
cmsService,
|
|
354
|
+
true
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const ctx: WebflowEmitContext = {
|
|
358
|
+
globalComponents,
|
|
359
|
+
elementPath: [0],
|
|
360
|
+
fileType: 'page',
|
|
361
|
+
fileName: file.replace('.json', ''),
|
|
362
|
+
breakpoints,
|
|
363
|
+
styleClasses: allStyleClasses,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const body = pageData.root || (pageData as any).node;
|
|
367
|
+
// Pass CMS item data as props so {{cms.field}} templates resolve
|
|
368
|
+
const cmsProps = { cms: itemWithUrl };
|
|
369
|
+
const elements = body ? nodeToWebflow(body, ctx, cmsProps) : [];
|
|
370
|
+
|
|
371
|
+
const slug = itemPath.startsWith('/') ? itemPath.substring(1) : itemPath;
|
|
372
|
+
allPages.push({
|
|
373
|
+
title: result.title,
|
|
374
|
+
slug,
|
|
375
|
+
elements,
|
|
376
|
+
locale,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} catch (error: any) {
|
|
381
|
+
console.error(`Error processing template ${file}:`, error?.message);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 4. Collect CSS variables
|
|
387
|
+
const themeColorCSS = generateThemeColorVariablesCSS(themeConfig);
|
|
388
|
+
const variablesCSS = generateVariablesCSS(variablesConfig, breakpoints, responsiveScales);
|
|
389
|
+
const cssVariables = extractCSSVariables(themeColorCSS, variablesCSS);
|
|
390
|
+
|
|
391
|
+
// 5. Scan assets
|
|
392
|
+
const assets = scanAssets(projectPaths.project);
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
version: 1,
|
|
396
|
+
exportedAt: new Date().toISOString(),
|
|
397
|
+
pages: allPages,
|
|
398
|
+
styles: Array.from(allStyleClasses.values()),
|
|
399
|
+
cms: cmsCollections,
|
|
400
|
+
assets,
|
|
401
|
+
cssVariables: Object.keys(cssVariables).length > 0 ? cssVariables : undefined,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function emptyPayload(): WebflowExportPayload {
|
|
406
|
+
return {
|
|
407
|
+
version: 1,
|
|
408
|
+
exportedAt: new Date().toISOString(),
|
|
409
|
+
pages: [],
|
|
410
|
+
styles: [],
|
|
411
|
+
cms: [],
|
|
412
|
+
assets: [],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webflow Export Module
|
|
3
|
+
* Re-exports the main build function and types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { buildWebflowPayload } from './buildWebflow';
|
|
7
|
+
export { nodeToWebflow } from './nodeToWebflow';
|
|
8
|
+
export { mapStylesToWebflow } from './styleMapper';
|
|
9
|
+
export type {
|
|
10
|
+
WebflowExportPayload,
|
|
11
|
+
WebflowPage,
|
|
12
|
+
WebflowElement,
|
|
13
|
+
WebflowStyleClass,
|
|
14
|
+
WebflowCMSCollection,
|
|
15
|
+
WebflowCMSField,
|
|
16
|
+
WebflowAssetRef,
|
|
17
|
+
WebflowBreakpoint,
|
|
18
|
+
WebflowPseudoState,
|
|
19
|
+
WebflowFieldType,
|
|
20
|
+
CSSProperties,
|
|
21
|
+
} from './types';
|
|
22
|
+
export { mapCMSFieldType } from './types';
|