meno-core 1.0.47 → 1.0.49
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 +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
- package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
- package/dist/chunks/{chunk-XSWR3QLI.js → chunk-AZQYF6KE.js} +261 -130
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-47UNLQUU.js → chunk-CHD5UCFF.js} +57 -12
- package/dist/chunks/chunk-CHD5UCFF.js.map +7 -0
- package/dist/chunks/{chunk-FGUZOYJX.js → chunk-EQYDSPBB.js} +435 -131
- package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
- package/dist/chunks/{chunk-IF3RATBY.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-KITQJYZV.js → chunk-J23ZX5AP.js} +40 -4
- package/dist/chunks/chunk-J23ZX5AP.js.map +7 -0
- package/dist/chunks/{chunk-LJFB5EBT.js → chunk-JER5NQVM.js} +5 -5
- package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-KPU2XHOS.js} +5 -2
- package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-KPU2XHOS.js.map} +2 -2
- package/dist/chunks/{chunk-BCLGRZ3U.js → chunk-LKAGAQ3M.js} +2 -2
- package/dist/chunks/{chunk-FED5MME6.js → chunk-S2CX6HFM.js} +262 -26
- package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
- package/dist/chunks/{configService-DYCUEURL.js → configService-CCA6AIDI.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +64 -20
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +1737 -296
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +50 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +17 -0
- package/lib/client/core/ComponentBuilder.ts +25 -1
- package/lib/client/core/builders/embedBuilder.ts +15 -2
- package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
- package/lib/client/core/builders/localeListBuilder.ts +17 -6
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- package/lib/server/index.ts +1 -1
- package/lib/server/jsonLoader.test.ts +0 -17
- package/lib/server/jsonLoader.ts +0 -81
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
- package/lib/server/routes/api/variables.ts +4 -2
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +5 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +114 -2
- package/lib/server/ssr/htmlGenerator.ts +53 -6
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +362 -1
- package/lib/server/ssr/ssrRenderer.ts +216 -72
- package/lib/server/utils/jsonLineMapper.test.ts +53 -1
- package/lib/server/utils/jsonLineMapper.ts +43 -3
- package/lib/server/webflow/buildWebflow.ts +343 -123
- package/lib/server/webflow/index.ts +1 -0
- package/lib/server/webflow/nodeToWebflow.test.ts +3170 -0
- package/lib/server/webflow/nodeToWebflow.ts +2141 -129
- package/lib/server/webflow/styleMapper.test.ts +389 -0
- package/lib/server/webflow/styleMapper.ts +517 -63
- package/lib/server/webflow/templateWrapper.ts +49 -0
- package/lib/server/webflow/types.ts +218 -18
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +247 -1
- package/lib/shared/cssProperties.ts +196 -6
- package/lib/shared/elementClassName.test.ts +15 -0
- package/lib/shared/elementClassName.ts +7 -3
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/cms.ts +28 -3
- package/lib/shared/types/index.ts +2 -0
- package/lib/shared/types/variables.ts +37 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.ts +15 -1
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-47UNLQUU.js.map +0 -7
- package/dist/chunks/chunk-FED5MME6.js.map +0 -7
- package/dist/chunks/chunk-FGUZOYJX.js.map +0 -7
- package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
- /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{chunk-LJFB5EBT.js.map → chunk-JER5NQVM.js.map} +0 -0
- /package/dist/chunks/{chunk-BCLGRZ3U.js.map → chunk-LKAGAQ3M.js.map} +0 -0
- /package/dist/chunks/{configService-DYCUEURL.js.map → configService-CCA6AIDI.js.map} +0 -0
|
@@ -86,6 +86,16 @@ export function buildLineMap(jsonText: string): LineMap {
|
|
|
86
86
|
return { start: startPos, end: pos };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function recordTrackedRoot(path: number[]): void {
|
|
90
|
+
// Parse the root element of the tracked structure and record its own line range
|
|
91
|
+
// under the empty-string key, so callers can resolve the root selection itself.
|
|
92
|
+
const { start, end } = parseValue(path, true);
|
|
93
|
+
lineMap.set('', {
|
|
94
|
+
startLine: charToLine[start],
|
|
95
|
+
endLine: charToLine[end - 1] || charToLine[start],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
function parseObject(path: number[], trackChildren: boolean): void {
|
|
90
100
|
pos++; // skip {
|
|
91
101
|
skipWhitespace();
|
|
@@ -98,10 +108,13 @@ export function buildLineMap(jsonText: string): LineMap {
|
|
|
98
108
|
skipWhitespace();
|
|
99
109
|
|
|
100
110
|
if (key === 'root' && path.length === 0) {
|
|
101
|
-
//
|
|
102
|
-
|
|
111
|
+
// Page format: track from `root`.
|
|
112
|
+
recordTrackedRoot(path);
|
|
113
|
+
} else if (key === 'component' && path.length === 0) {
|
|
114
|
+
// Component format: descend into the component object looking for `structure`.
|
|
115
|
+
parseComponentWrapper(path);
|
|
103
116
|
} else if (key === 'children' && trackChildren) {
|
|
104
|
-
//
|
|
117
|
+
// Children array — track each element with indices
|
|
105
118
|
parseValue(path, true);
|
|
106
119
|
} else {
|
|
107
120
|
// Regular property - don't track individual items
|
|
@@ -115,6 +128,33 @@ export function buildLineMap(jsonText: string): LineMap {
|
|
|
115
128
|
pos++; // skip }
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
function parseComponentWrapper(path: number[]): void {
|
|
132
|
+
// We expect an object value for the `component` key. If it isn't one, just skip it.
|
|
133
|
+
if (jsonText[pos] !== '{') {
|
|
134
|
+
parseValue(path, false);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
pos++; // skip {
|
|
138
|
+
skipWhitespace();
|
|
139
|
+
while (pos < jsonText.length && jsonText[pos] !== '}') {
|
|
140
|
+
skipWhitespace();
|
|
141
|
+
const key = parseString();
|
|
142
|
+
skipWhitespace();
|
|
143
|
+
pos++; // skip :
|
|
144
|
+
skipWhitespace();
|
|
145
|
+
if (key === 'structure') {
|
|
146
|
+
// The component's structure is the root of what we track — same as `root` for pages.
|
|
147
|
+
recordTrackedRoot(path);
|
|
148
|
+
} else {
|
|
149
|
+
parseValue(path, false);
|
|
150
|
+
}
|
|
151
|
+
skipWhitespace();
|
|
152
|
+
if (jsonText[pos] === ',') pos++;
|
|
153
|
+
skipWhitespace();
|
|
154
|
+
}
|
|
155
|
+
pos++; // skip }
|
|
156
|
+
}
|
|
157
|
+
|
|
118
158
|
function parseArray(path: number[], trackChildren: boolean): void {
|
|
119
159
|
pos++; // skip [
|
|
120
160
|
skipWhitespace();
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
parseJSON,
|
|
15
15
|
loadI18nConfig,
|
|
16
16
|
loadBreakpointConfig,
|
|
17
|
-
loadResponsiveScalesConfig,
|
|
18
17
|
} from '../jsonLoader';
|
|
19
18
|
import { renderPageSSR } from '../ssr/ssrRenderer';
|
|
20
19
|
import { projectPaths } from '../projectContext';
|
|
@@ -22,6 +21,8 @@ import { loadProjectConfig } from '../../shared/fontLoader';
|
|
|
22
21
|
import { FileSystemCMSProvider } from '../providers/fileSystemCMSProvider';
|
|
23
22
|
import { CMSService } from '../services/cmsService';
|
|
24
23
|
import { isI18nValue, resolveI18nValue } from '../../shared/i18n';
|
|
24
|
+
import { RAW_HTML_PREFIX } from '../../shared/constants';
|
|
25
|
+
import { extractPageMeta, type PageMeta } from '../ssr/metaTagGenerator';
|
|
25
26
|
import { configService } from '../services/configService';
|
|
26
27
|
import { colorService } from '../services/ColorService';
|
|
27
28
|
import { variableService } from '../services/VariableService';
|
|
@@ -32,20 +33,27 @@ import type {
|
|
|
32
33
|
CMSSchema,
|
|
33
34
|
CMSItem,
|
|
34
35
|
I18nConfig,
|
|
36
|
+
ThemeConfig,
|
|
35
37
|
} from '../../shared/types';
|
|
36
|
-
import {
|
|
38
|
+
import { resolvePaletteColor } from '../../shared/types/colors';
|
|
37
39
|
import type { SlugMap } from '../../shared/slugTranslator';
|
|
38
40
|
import type {
|
|
39
41
|
WebflowExportPayload,
|
|
40
42
|
WebflowPage,
|
|
41
43
|
WebflowStyleClass,
|
|
42
|
-
WebflowCMSCollection,
|
|
43
|
-
WebflowCMSField,
|
|
44
44
|
WebflowAssetRef,
|
|
45
|
+
WebflowScript,
|
|
46
|
+
WebflowComponentDef,
|
|
45
47
|
} from './types';
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
48
|
+
import { nodeToWebflow, normalizeListChildren, type WebflowEmitContext } from './nodeToWebflow';
|
|
49
|
+
import { isWebflowHandledRule } from './styleMapper';
|
|
50
|
+
import { generateInteractiveCSS } from '../../shared/cssGeneration';
|
|
51
|
+
import { generateVariablesCSS } from '../cssGenerator';
|
|
52
|
+
import type { InteractiveStyles } from '../../shared/types/styles';
|
|
53
|
+
import { resolveVariableValueAtBreakpoint } from '../../shared/responsiveScaling';
|
|
54
|
+
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
55
|
+
import type { VariablesConfig } from '../../shared/types';
|
|
56
|
+
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
49
57
|
|
|
50
58
|
// ---------------------------------------------------------------------------
|
|
51
59
|
// Helpers
|
|
@@ -69,18 +77,73 @@ function isCMSPage(pageData: JSONPage): boolean {
|
|
|
69
77
|
return pageData.meta?.source === 'cms' && !!pageData.meta?.cms;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Flatten a CMS item for the Webflow emit path: resolve i18n values to the
|
|
82
|
+
* current locale and unwrap rich-text markers (`{ __richtext__, html }`) into
|
|
83
|
+
* `RAW_HTML_PREFIX + html` so `nodeToWebflow`'s `resolveTemplate` can flatten
|
|
84
|
+
* inline HTML to plain text. Mirrors what SSR's `processCMSTemplate`
|
|
85
|
+
* (`cmsSSRProcessor.ts`) does inline at every interpolation site — but applied
|
|
86
|
+
* once up front so the simpler Webflow `resolveTemplate` (which only does
|
|
87
|
+
* `String(value)`) renders meaningful text instead of `[object Object]` /
|
|
88
|
+
* empty strings.
|
|
89
|
+
*/
|
|
90
|
+
function flattenCMSItemForLocale(
|
|
74
91
|
item: CMSItem,
|
|
75
|
-
slugField: string,
|
|
76
92
|
locale: string,
|
|
77
93
|
i18nConfig: I18nConfig
|
|
78
|
-
): string {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
): Record<string, unknown> {
|
|
95
|
+
const flatten = (value: unknown): unknown => {
|
|
96
|
+
if (value === null || value === undefined) return value;
|
|
97
|
+
if (isI18nValue(value)) {
|
|
98
|
+
return flatten(resolveI18nValue(value, locale, i18nConfig));
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'object' && '__richtext__' in (value as object)) {
|
|
101
|
+
const html = (value as { html?: unknown }).html;
|
|
102
|
+
if (typeof html === 'string') return RAW_HTML_PREFIX + html;
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
};
|
|
106
|
+
const out: Record<string, unknown> = {};
|
|
107
|
+
for (const [key, value] of Object.entries(item)) {
|
|
108
|
+
out[key] = flatten(value);
|
|
82
109
|
}
|
|
83
|
-
return
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a page-meta value (which may be an i18n object) to a string for the
|
|
115
|
+
* given locale, or undefined when the field is empty.
|
|
116
|
+
*/
|
|
117
|
+
function resolveMetaString(
|
|
118
|
+
value: unknown,
|
|
119
|
+
locale: string,
|
|
120
|
+
i18nConfig: I18nConfig
|
|
121
|
+
): string | undefined {
|
|
122
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
123
|
+
const resolved = isI18nValue(value)
|
|
124
|
+
? resolveI18nValue(value, locale, i18nConfig)
|
|
125
|
+
: value;
|
|
126
|
+
if (resolved === undefined || resolved === null || resolved === '') return undefined;
|
|
127
|
+
return String(resolved);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Lift the structured meta from a page (title, description, OG fields,
|
|
132
|
+
* keywords) into the per-locale fields of a `WebflowPage`. SSR's `result.title`
|
|
133
|
+
* is preserved as the canonical title; the rest comes from `extractPageMeta`.
|
|
134
|
+
*/
|
|
135
|
+
function buildPageMetaForLocale(
|
|
136
|
+
meta: PageMeta,
|
|
137
|
+
locale: string,
|
|
138
|
+
i18nConfig: I18nConfig
|
|
139
|
+
): Pick<WebflowPage, 'description' | 'keywords' | 'ogTitle' | 'ogDescription' | 'ogImage'> {
|
|
140
|
+
return {
|
|
141
|
+
description: resolveMetaString(meta.description, locale, i18nConfig),
|
|
142
|
+
keywords: resolveMetaString(meta.keywords, locale, i18nConfig),
|
|
143
|
+
ogTitle: resolveMetaString(meta.ogTitle, locale, i18nConfig),
|
|
144
|
+
ogDescription: resolveMetaString(meta.ogDescription, locale, i18nConfig),
|
|
145
|
+
ogImage: resolveMetaString(meta.ogImage, locale, i18nConfig),
|
|
146
|
+
};
|
|
84
147
|
}
|
|
85
148
|
|
|
86
149
|
function scanAssets(projectRoot: string): WebflowAssetRef[] {
|
|
@@ -126,23 +189,59 @@ function scanAllFiles(dir: string, prefix: string = ''): string[] {
|
|
|
126
189
|
}
|
|
127
190
|
|
|
128
191
|
/**
|
|
129
|
-
*
|
|
192
|
+
* Build per-theme `--var` maps so the walker can resolve `var(--bg)` against
|
|
193
|
+
* whatever theme an element actually inherits. Each theme key holds its full
|
|
194
|
+
* resolved palette (text, bg, border, …); `defaultThemeName` is what we fall
|
|
195
|
+
* back to when an element has no `theme` ancestor.
|
|
130
196
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
while ((match = regex.exec(css)) !== null) {
|
|
141
|
-
vars[`--${match[1]}`] = match[2].trim();
|
|
197
|
+
function buildThemeVarMaps(
|
|
198
|
+
themeConfig: ThemeConfig
|
|
199
|
+
): { byTheme: Record<string, Record<string, string>>; defaultTheme: string } {
|
|
200
|
+
const palette = themeConfig.palette;
|
|
201
|
+
const byTheme: Record<string, Record<string, string>> = {};
|
|
202
|
+
for (const [themeName, theme] of Object.entries(themeConfig.themes)) {
|
|
203
|
+
const vars: Record<string, string> = {};
|
|
204
|
+
for (const [name, value] of Object.entries(theme.colors)) {
|
|
205
|
+
vars[`--${name}`] = resolvePaletteColor(value, palette);
|
|
142
206
|
}
|
|
207
|
+
byTheme[themeName] = vars;
|
|
143
208
|
}
|
|
209
|
+
return { byTheme, defaultTheme: themeConfig.default };
|
|
210
|
+
}
|
|
144
211
|
|
|
145
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Build a per-breakpoint `--var → value` map directly from the variables
|
|
214
|
+
* config. Each breakpoint key (`base`, `tablet`, `mobile`, …) holds the full
|
|
215
|
+
* resolved variable map at that breakpoint, with per-variable `scales`
|
|
216
|
+
* overrides and global category scaling already applied
|
|
217
|
+
* (`resolveVariableValueAtBreakpoint` mirrors `generateVariablesCSS` and the
|
|
218
|
+
* runtime CSS resolver).
|
|
219
|
+
*
|
|
220
|
+
* The walker uses these maps to inline `var(--x)` references in element
|
|
221
|
+
* styles per-breakpoint, so the Webflow class system receives concrete
|
|
222
|
+
* scaled values at every tier instead of a single flat base value.
|
|
223
|
+
*/
|
|
224
|
+
function buildProjectVarMaps(
|
|
225
|
+
variablesConfig: VariablesConfig,
|
|
226
|
+
breakpoints: BreakpointConfig,
|
|
227
|
+
responsiveScales: ResponsiveScales | undefined
|
|
228
|
+
): Record<string, Record<string, string>> {
|
|
229
|
+
const out: Record<string, Record<string, string>> = { base: {} };
|
|
230
|
+
for (const bpName of Object.keys(breakpoints)) out[bpName] = {};
|
|
231
|
+
|
|
232
|
+
for (const variable of variablesConfig.variables) {
|
|
233
|
+
if (!variable.cssVar) continue;
|
|
234
|
+
out.base[variable.cssVar] = variable.value;
|
|
235
|
+
for (const bpName of Object.keys(breakpoints)) {
|
|
236
|
+
const resolved = resolveVariableValueAtBreakpoint(
|
|
237
|
+
{ value: variable.value, type: variable.type, scales: variable.scales },
|
|
238
|
+
bpName,
|
|
239
|
+
responsiveScales
|
|
240
|
+
);
|
|
241
|
+
out[bpName][variable.cssVar] = resolved;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
146
245
|
}
|
|
147
246
|
|
|
148
247
|
// ---------------------------------------------------------------------------
|
|
@@ -150,8 +249,12 @@ function extractCSSVariables(
|
|
|
150
249
|
// ---------------------------------------------------------------------------
|
|
151
250
|
|
|
152
251
|
export async function buildWebflowPayload(
|
|
153
|
-
|
|
252
|
+
options?: { bindCollectionLists?: boolean; promotedComponentNames?: string[]; locale?: string }
|
|
154
253
|
): Promise<WebflowExportPayload> {
|
|
254
|
+
const bindCollectionLists = options?.bindCollectionLists === true;
|
|
255
|
+
const promotedComponentNames = Array.isArray(options?.promotedComponentNames)
|
|
256
|
+
? new Set(options!.promotedComponentNames.filter((n): n is string => typeof n === 'string' && n.length > 0))
|
|
257
|
+
: undefined;
|
|
155
258
|
// 1. Setup: load project configuration
|
|
156
259
|
configService.reset();
|
|
157
260
|
|
|
@@ -159,6 +262,17 @@ export async function buildWebflowPayload(
|
|
|
159
262
|
const siteUrl = (projectConfig as { siteUrl?: string }).siteUrl?.replace(/\/$/, '') || '';
|
|
160
263
|
const i18nConfig = await loadI18nConfig();
|
|
161
264
|
|
|
265
|
+
// Pick which locale to render. Multi-locale projects export one locale at a
|
|
266
|
+
// time so the Webflow site mirrors a single language; the extension surfaces
|
|
267
|
+
// a picker that defaults to `i18nConfig.defaultLocale`. An invalid/missing
|
|
268
|
+
// request falls back to the default locale (silent — the picker is the
|
|
269
|
+
// source of truth here, not the URL).
|
|
270
|
+
const requestedLocale = options?.locale;
|
|
271
|
+
const selectedLocale = (requestedLocale && i18nConfig.locales.some(l => l.code === requestedLocale))
|
|
272
|
+
? requestedLocale
|
|
273
|
+
: i18nConfig.defaultLocale;
|
|
274
|
+
const localesToBuild = i18nConfig.locales.filter(l => l.code === selectedLocale);
|
|
275
|
+
|
|
162
276
|
await migrateTemplatesDirectory();
|
|
163
277
|
|
|
164
278
|
const { components } = await loadComponentDirectory(projectPaths.components());
|
|
@@ -172,9 +286,9 @@ export async function buildWebflowPayload(
|
|
|
172
286
|
const themeConfig = await colorService.loadThemeConfig();
|
|
173
287
|
const variablesConfig = await variableService.loadConfig();
|
|
174
288
|
const breakpoints = await loadBreakpointConfig();
|
|
175
|
-
const responsiveScales = await loadResponsiveScalesConfig();
|
|
176
289
|
|
|
177
290
|
await configService.load();
|
|
291
|
+
const responsiveScales = configService.getResponsiveScales();
|
|
178
292
|
|
|
179
293
|
// 2. Scan pages
|
|
180
294
|
const pagesDir = projectPaths.pages();
|
|
@@ -206,6 +320,24 @@ export async function buildWebflowPayload(
|
|
|
206
320
|
// 3. Render and convert pages
|
|
207
321
|
const allPages: WebflowPage[] = [];
|
|
208
322
|
const allStyleClasses = new Map<string, WebflowStyleClass>();
|
|
323
|
+
// elementClass → interactiveStyles, populated during the node walk and
|
|
324
|
+
// drained at the end into raw CSS (see step 6 below).
|
|
325
|
+
const allInteractiveStylesMap = new Map<string, InteractiveStyles>();
|
|
326
|
+
// Combo class name → owning identity, shared across every page emit so
|
|
327
|
+
// collisions are visible project-wide. Drives `mintInstanceComboName` in
|
|
328
|
+
// nodeToWebflow.ts.
|
|
329
|
+
const allComboIdentityByName = new Map<string, string>();
|
|
330
|
+
// First-encounter snapshot of each promoted component (Navigation, Footer)
|
|
331
|
+
// shared across every page so the extension registers each definition once.
|
|
332
|
+
const promotedComponents = new Map<string, WebflowComponentDef>();
|
|
333
|
+
|
|
334
|
+
// Theme + project-level CSS variables are resolved during the walk so the
|
|
335
|
+
// ancestor `theme` attribute can pick the right palette per element. The
|
|
336
|
+
// project var map is per-breakpoint so `var(--x)` refs in element styles
|
|
337
|
+
// can be inlined at each tier with the right scaled value.
|
|
338
|
+
const variablesCSS = generateVariablesCSS(variablesConfig, breakpoints, responsiveScales);
|
|
339
|
+
const projectVars = buildProjectVarMaps(variablesConfig, breakpoints, responsiveScales);
|
|
340
|
+
const { byTheme: themeVars, defaultTheme } = buildThemeVarMaps(themeConfig);
|
|
209
341
|
|
|
210
342
|
// Regular pages
|
|
211
343
|
for (const file of pageFiles) {
|
|
@@ -220,7 +352,7 @@ export async function buildWebflowPayload(
|
|
|
220
352
|
|
|
221
353
|
const slugs = pageData.meta?.slugs;
|
|
222
354
|
|
|
223
|
-
for (const localeConfig of
|
|
355
|
+
for (const localeConfig of localesToBuild) {
|
|
224
356
|
const locale = localeConfig.code;
|
|
225
357
|
const isDefault = locale === i18nConfig.defaultLocale;
|
|
226
358
|
|
|
@@ -259,17 +391,33 @@ export async function buildWebflowPayload(
|
|
|
259
391
|
fileName: pageName,
|
|
260
392
|
breakpoints,
|
|
261
393
|
styleClasses: allStyleClasses,
|
|
394
|
+
comboIdentityByName: allComboIdentityByName,
|
|
395
|
+
interactiveStylesMap: allInteractiveStylesMap,
|
|
396
|
+
cmsService,
|
|
397
|
+
i18nConfig,
|
|
398
|
+
locale,
|
|
399
|
+
slugMappings,
|
|
400
|
+
pagePath: urlPath,
|
|
401
|
+
themeVars,
|
|
402
|
+
projectVars,
|
|
403
|
+
defaultTheme,
|
|
404
|
+
responsiveScales,
|
|
405
|
+
promotedComponents,
|
|
406
|
+
promotedComponentNames,
|
|
407
|
+
bindCollectionLists,
|
|
262
408
|
};
|
|
263
409
|
|
|
264
410
|
const body = pageData.root || (pageData as any).node;
|
|
265
|
-
const elements = body ? nodeToWebflow(body, ctx) : [];
|
|
411
|
+
const elements = body ? await nodeToWebflow(body, ctx) : [];
|
|
412
|
+
normalizeListChildren(elements);
|
|
266
413
|
|
|
414
|
+
const pageMeta = extractPageMeta(pageData);
|
|
267
415
|
allPages.push({
|
|
268
416
|
title: result.title,
|
|
269
417
|
slug: slug || 'index',
|
|
270
|
-
metaDescription: typeof pageData.meta?.description === 'string' ? pageData.meta.description : undefined,
|
|
271
418
|
elements,
|
|
272
419
|
locale,
|
|
420
|
+
...buildPageMetaForLocale(pageMeta, locale, i18nConfig),
|
|
273
421
|
});
|
|
274
422
|
}
|
|
275
423
|
} catch (error: any) {
|
|
@@ -277,9 +425,11 @@ export async function buildWebflowPayload(
|
|
|
277
425
|
}
|
|
278
426
|
}
|
|
279
427
|
|
|
280
|
-
// CMS template pages
|
|
428
|
+
// CMS template pages — imported as a single regular Webflow page per locale,
|
|
429
|
+
// bound to the FIRST CMS item's content. Webflow can't natively bind a page
|
|
430
|
+
// to a Meno-side collection, and a separate "Sync CMS" flow in the extension
|
|
431
|
+
// handles collection schema/items, so this code path stays page-only.
|
|
281
432
|
const templatesDir = projectPaths.templates();
|
|
282
|
-
const cmsCollections: WebflowCMSCollection[] = [];
|
|
283
433
|
|
|
284
434
|
if (existsSync(templatesDir)) {
|
|
285
435
|
const templateFiles = readdirSync(templatesDir).filter(f => f.endsWith('.json'));
|
|
@@ -295,87 +445,95 @@ export async function buildWebflowPayload(
|
|
|
295
445
|
|
|
296
446
|
const cmsSchema = pageData.meta!.cms as CMSSchema;
|
|
297
447
|
const items = await cmsService.queryItems({ collection: cmsSchema.id });
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
});
|
|
448
|
+
if (items.length === 0) continue;
|
|
449
|
+
|
|
450
|
+
const item = items[0];
|
|
451
|
+
const pageName = file.replace('.json', '');
|
|
452
|
+
|
|
453
|
+
for (const localeConfig of localesToBuild) {
|
|
454
|
+
const locale = localeConfig.code;
|
|
455
|
+
// Use the first item's resolved URL (from cmsSchema.urlPattern) as
|
|
456
|
+
// the slug, so a template like `templates/blog.json` lands at
|
|
457
|
+
// `/blog/<first-slug>` and doesn't collide with a sibling listing
|
|
458
|
+
// page like `pages/blog.json`.
|
|
459
|
+
let itemSlug = item[cmsSchema.slugField] ?? item._slug ?? item._id;
|
|
460
|
+
if (isI18nValue(itemSlug)) {
|
|
461
|
+
itemSlug = resolveI18nValue(itemSlug, locale, i18nConfig) as string;
|
|
378
462
|
}
|
|
463
|
+
const resolvedItemPath = cmsSchema.urlPattern.replace('{{slug}}', String(itemSlug));
|
|
464
|
+
const isDefault = locale === i18nConfig.defaultLocale;
|
|
465
|
+
const localizedItemPath = isDefault
|
|
466
|
+
? resolvedItemPath
|
|
467
|
+
: `/${locale}${resolvedItemPath.startsWith('/') ? '' : '/'}${resolvedItemPath}`;
|
|
468
|
+
const slug = localizedItemPath.startsWith('/')
|
|
469
|
+
? localizedItemPath.substring(1)
|
|
470
|
+
: localizedItemPath;
|
|
471
|
+
const urlPath = localizedItemPath;
|
|
472
|
+
// SSR (`renderPageSSR`) gets the raw item — its `processCMSTemplate`
|
|
473
|
+
// resolves i18n + richtext at each interpolation. The Webflow path
|
|
474
|
+
// re-walks the same `pageData.root` with the simpler `resolveTemplate`
|
|
475
|
+
// (no i18n / richtext awareness), so we hand it a pre-flattened item.
|
|
476
|
+
const flatItem = flattenCMSItemForLocale(item, locale, i18nConfig);
|
|
477
|
+
const itemWithUrl: CMSItem = { ...flatItem, _url: urlPath } as CMSItem;
|
|
478
|
+
const ssrItem: CMSItem = { ...item, _url: urlPath };
|
|
479
|
+
|
|
480
|
+
const result = await renderPageSSR(
|
|
481
|
+
pageData,
|
|
482
|
+
globalComponents,
|
|
483
|
+
urlPath,
|
|
484
|
+
siteUrl,
|
|
485
|
+
locale,
|
|
486
|
+
i18nConfig,
|
|
487
|
+
slugMappings,
|
|
488
|
+
{ cms: ssrItem },
|
|
489
|
+
cmsService,
|
|
490
|
+
true
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const ctx: WebflowEmitContext = {
|
|
494
|
+
globalComponents,
|
|
495
|
+
elementPath: [0],
|
|
496
|
+
fileType: 'page',
|
|
497
|
+
fileName: pageName,
|
|
498
|
+
breakpoints,
|
|
499
|
+
styleClasses: allStyleClasses,
|
|
500
|
+
comboIdentityByName: allComboIdentityByName,
|
|
501
|
+
interactiveStylesMap: allInteractiveStylesMap,
|
|
502
|
+
cmsService,
|
|
503
|
+
i18nConfig,
|
|
504
|
+
locale,
|
|
505
|
+
slugMappings,
|
|
506
|
+
pagePath: urlPath,
|
|
507
|
+
cmsContext: { cms: itemWithUrl },
|
|
508
|
+
// Seed `templateContext` with the bound item so descendants inside
|
|
509
|
+
// a component slot still see `cms`. `emitInlineComponentBody`
|
|
510
|
+
// merges `ctx.templateContext` into the body's `instanceProps`
|
|
511
|
+
// (nodeToWebflow.ts:1140-1143), and `<slot>` rendering forwards
|
|
512
|
+
// those merged props to slot children — without this, anything
|
|
513
|
+
// wrapped in (e.g.) Section loses the page-level CMS scope and
|
|
514
|
+
// `{{cms.title}}` resolves to empty.
|
|
515
|
+
templateContext: { cms: itemWithUrl } as Record<string, unknown>,
|
|
516
|
+
themeVars,
|
|
517
|
+
projectVars,
|
|
518
|
+
defaultTheme,
|
|
519
|
+
responsiveScales,
|
|
520
|
+
promotedComponents,
|
|
521
|
+
promotedComponentNames,
|
|
522
|
+
bindCollectionLists,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const body = pageData.root || (pageData as any).node;
|
|
526
|
+
const elements = body ? await nodeToWebflow(body, ctx, { cms: itemWithUrl }) : [];
|
|
527
|
+
normalizeListChildren(elements);
|
|
528
|
+
|
|
529
|
+
const cmsPageMeta = extractPageMeta(pageData);
|
|
530
|
+
allPages.push({
|
|
531
|
+
title: result.title,
|
|
532
|
+
slug,
|
|
533
|
+
elements,
|
|
534
|
+
locale,
|
|
535
|
+
...buildPageMetaForLocale(cmsPageMeta, locale, i18nConfig),
|
|
536
|
+
});
|
|
379
537
|
}
|
|
380
538
|
} catch (error: any) {
|
|
381
539
|
console.error(`Error processing template ${file}:`, error?.message);
|
|
@@ -383,22 +541,84 @@ export async function buildWebflowPayload(
|
|
|
383
541
|
}
|
|
384
542
|
}
|
|
385
543
|
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
544
|
+
// Promoted components (Navigation, Footer) are emitted via the same
|
|
545
|
+
// `nodeToWebflow` walk as pages, so they share the same `<ul>`-with-
|
|
546
|
+
// non-`<li>`-children pitfall. Normalize their element trees too before
|
|
547
|
+
// they leave the build.
|
|
548
|
+
for (const def of promotedComponents.values()) {
|
|
549
|
+
normalizeListChildren(def.elements);
|
|
550
|
+
}
|
|
390
551
|
|
|
391
|
-
//
|
|
552
|
+
// 4. Scan assets
|
|
553
|
+
const styleClasses = Array.from(allStyleClasses.values());
|
|
392
554
|
const assets = scanAssets(projectPaths.project);
|
|
393
555
|
|
|
556
|
+
// 5. Collect component scripts and component-scoped CSS sidecars. Both get
|
|
557
|
+
// bundled into the manual-paste UI: scripts as a `<script>` at body end,
|
|
558
|
+
// CSS as part of the `<style>` block at head. CSS in particular is
|
|
559
|
+
// critical for components that drive visibility through hand-written
|
|
560
|
+
// rules (e.g. NavDropdown's `[data-nav-dropdown="container"].open ...`)
|
|
561
|
+
// — those don't translate to Webflow's class system at all.
|
|
562
|
+
const scripts: WebflowScript[] = [];
|
|
563
|
+
const cssBlocks: string[] = [];
|
|
564
|
+
for (const [name, def] of Object.entries(globalComponents)) {
|
|
565
|
+
const code = def.component?.javascript;
|
|
566
|
+
if (typeof code === 'string' && code.trim().length > 0) {
|
|
567
|
+
scripts.push({
|
|
568
|
+
componentName: name,
|
|
569
|
+
code,
|
|
570
|
+
defineVars: def.component?.defineVars,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const css = def.component?.css;
|
|
574
|
+
if (typeof css === 'string' && css.trim().length > 0) {
|
|
575
|
+
cssBlocks.push(`/* ${name}.css */\n${css.trim()}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const componentCss = cssBlocks.length > 0 ? cssBlocks.join('\n\n') : undefined;
|
|
579
|
+
|
|
580
|
+
// 6. Per-element interactiveStyles → raw CSS for rules Webflow's class
|
|
581
|
+
// system can't represent (anything with `prefix`, class-style postfix,
|
|
582
|
+
// or breakpoint-divided pseudos). Pseudo-only rules with empty prefix
|
|
583
|
+
// have already been written into `primaryClass.pseudoStates` upstream
|
|
584
|
+
// via `Style.setProperties({ pseudo })`; we filter those out here so
|
|
585
|
+
// the bundle doesn't double-emit them.
|
|
586
|
+
const interactiveCssBlocks: string[] = [];
|
|
587
|
+
for (const [elementClass, rules] of allInteractiveStylesMap) {
|
|
588
|
+
const customRules = rules.filter((r) => !isWebflowHandledRule(r));
|
|
589
|
+
if (customRules.length === 0) continue;
|
|
590
|
+
const css = generateInteractiveCSS(elementClass, customRules, breakpoints, undefined, responsiveScales);
|
|
591
|
+
if (css && css.trim().length > 0) {
|
|
592
|
+
interactiveCssBlocks.push(css);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const interactiveCss = interactiveCssBlocks.length > 0
|
|
596
|
+
? interactiveCssBlocks.join('\n\n')
|
|
597
|
+
: undefined;
|
|
598
|
+
|
|
394
599
|
return {
|
|
395
600
|
version: 1,
|
|
396
601
|
exportedAt: new Date().toISOString(),
|
|
397
602
|
pages: allPages,
|
|
398
|
-
styles:
|
|
399
|
-
cms:
|
|
603
|
+
styles: styleClasses,
|
|
604
|
+
cms: [],
|
|
400
605
|
assets,
|
|
401
|
-
|
|
606
|
+
slugMappings: slugMappings.length > 0 ? slugMappings : undefined,
|
|
607
|
+
scripts: scripts.length > 0 ? scripts : undefined,
|
|
608
|
+
components: promotedComponents.size > 0
|
|
609
|
+
? Array.from(promotedComponents.values())
|
|
610
|
+
: undefined,
|
|
611
|
+
componentCss,
|
|
612
|
+
interactiveCss,
|
|
613
|
+
i18n: {
|
|
614
|
+
defaultLocale: i18nConfig.defaultLocale,
|
|
615
|
+
locales: i18nConfig.locales.map(l => ({
|
|
616
|
+
code: l.code,
|
|
617
|
+
name: l.name,
|
|
618
|
+
nativeName: l.nativeName,
|
|
619
|
+
})),
|
|
620
|
+
selectedLocale,
|
|
621
|
+
},
|
|
402
622
|
};
|
|
403
623
|
}
|
|
404
624
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export { buildWebflowPayload } from './buildWebflow';
|
|
7
7
|
export { nodeToWebflow } from './nodeToWebflow';
|
|
8
8
|
export { mapStylesToWebflow } from './styleMapper';
|
|
9
|
+
export { wrapInWebflowTemplate } from './templateWrapper';
|
|
9
10
|
export type {
|
|
10
11
|
WebflowExportPayload,
|
|
11
12
|
WebflowPage,
|