meno-core 1.0.39 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/build-astro.ts +195 -68
  2. package/dist/bin/cli.js +1 -1
  3. package/dist/build-static.js +6 -6
  4. package/dist/chunks/{chunk-WK5XLASY.js → chunk-3NOZVNM4.js} +3 -3
  5. package/dist/chunks/{chunk-W6HDII4T.js → chunk-GKICS7CF.js} +27 -14
  6. package/dist/chunks/chunk-GKICS7CF.js.map +7 -0
  7. package/dist/chunks/{chunk-P3FX5HJM.js → chunk-LOJLO2EY.js} +1 -1
  8. package/dist/chunks/chunk-LOJLO2EY.js.map +7 -0
  9. package/dist/chunks/{chunk-HNAS6BSS.js → chunk-MOCRENNU.js} +55 -5
  10. package/dist/chunks/{chunk-HNAS6BSS.js.map → chunk-MOCRENNU.js.map} +3 -3
  11. package/dist/chunks/{chunk-NV25WXCA.js → chunk-OJ5SROQN.js} +5 -3
  12. package/dist/chunks/chunk-OJ5SROQN.js.map +7 -0
  13. package/dist/chunks/{chunk-AIXKUVNG.js → chunk-V4SVSX3X.js} +3 -3
  14. package/dist/chunks/{chunk-KULPBDC7.js → chunk-Z7SAOCDG.js} +5 -2
  15. package/dist/chunks/{chunk-KULPBDC7.js.map → chunk-Z7SAOCDG.js.map} +2 -2
  16. package/dist/chunks/{constants-5CRJRQNR.js → constants-L75FR445.js} +2 -2
  17. package/dist/entries/server-router.js +6 -6
  18. package/dist/lib/client/index.js +5 -5
  19. package/dist/lib/client/index.js.map +2 -2
  20. package/dist/lib/server/index.js +2007 -197
  21. package/dist/lib/server/index.js.map +4 -4
  22. package/dist/lib/shared/index.js +3 -3
  23. package/dist/lib/test-utils/index.js +1 -1
  24. package/lib/client/core/builders/embedBuilder.ts +2 -2
  25. package/lib/server/astro/cmsPageEmitter.ts +417 -0
  26. package/lib/server/astro/componentEmitter.ts +90 -5
  27. package/lib/server/astro/nodeToAstro.ts +830 -37
  28. package/lib/server/astro/pageEmitter.ts +39 -3
  29. package/lib/server/astro/tailwindMapper.ts +69 -8
  30. package/lib/server/astro/templateTransformer.ts +107 -0
  31. package/lib/server/index.ts +9 -0
  32. package/lib/server/routes/api/components.ts +62 -0
  33. package/lib/server/routes/api/core-routes.ts +8 -0
  34. package/lib/server/ssr/ssrRenderer.ts +30 -10
  35. package/lib/server/webflow/buildWebflow.ts +415 -0
  36. package/lib/server/webflow/index.ts +22 -0
  37. package/lib/server/webflow/nodeToWebflow.ts +423 -0
  38. package/lib/server/webflow/styleMapper.ts +241 -0
  39. package/lib/server/webflow/types.ts +196 -0
  40. package/lib/shared/constants.ts +2 -0
  41. package/lib/shared/types/components.ts +1 -0
  42. package/lib/shared/validation/schemas.ts +1 -0
  43. package/package.json +1 -1
  44. package/dist/chunks/chunk-NV25WXCA.js.map +0 -7
  45. package/dist/chunks/chunk-P3FX5HJM.js.map +0 -7
  46. package/dist/chunks/chunk-W6HDII4T.js.map +0 -7
  47. /package/dist/chunks/{chunk-WK5XLASY.js.map → chunk-3NOZVNM4.js.map} +0 -0
  48. /package/dist/chunks/{chunk-AIXKUVNG.js.map → chunk-V4SVSX3X.js.map} +0 -0
  49. /package/dist/chunks/{constants-5CRJRQNR.js.map → constants-L75FR445.js.map} +0 -0
@@ -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';