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
@@ -23,7 +23,7 @@ import {
23
23
  isValidIdentifier,
24
24
  resolvePaletteColor,
25
25
  resolveSafePath
26
- } from "../../chunks/chunk-P3FX5HJM.js";
26
+ } from "../../chunks/chunk-LOJLO2EY.js";
27
27
  import {
28
28
  CSS_PROPERTIES,
29
29
  CSS_PROPERTIES_DEFINITION,
@@ -201,7 +201,7 @@ import {
201
201
  validatePath,
202
202
  validatePropDefinition,
203
203
  validateStructuredComponentDefinition
204
- } from "../../chunks/chunk-NV25WXCA.js";
204
+ } from "../../chunks/chunk-OJ5SROQN.js";
205
205
  import {
206
206
  DEFAULT_BREAKPOINTS,
207
207
  DEFAULT_I18N_CONFIG,
@@ -255,7 +255,7 @@ import {
255
255
  TREE_SCROLL_DELAY_MS,
256
256
  WEBSOCKET_STATES,
257
257
  init_constants
258
- } from "../../chunks/chunk-KULPBDC7.js";
258
+ } from "../../chunks/chunk-Z7SAOCDG.js";
259
259
  import "../../chunks/chunk-KSBZ2L7C.js";
260
260
 
261
261
  // lib/shared/index.ts
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  NODE_TYPE,
3
3
  init_constants
4
- } from "../../chunks/chunk-KULPBDC7.js";
4
+ } from "../../chunks/chunk-Z7SAOCDG.js";
5
5
  import "../../chunks/chunk-KSBZ2L7C.js";
6
6
 
7
7
  // lib/test-utils/mocks.ts
@@ -38,8 +38,8 @@ export interface EmbedBuilderDeps {
38
38
  * Script tags and event handlers are still removed for security
39
39
  */
40
40
  const SANITIZE_CONFIG = {
41
- 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'],
42
- 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'],
41
+ 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'],
42
+ 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'],
43
43
  KEEP_CONTENT: true
44
44
  };
45
45
 
@@ -0,0 +1,417 @@
1
+ /**
2
+ * CMS Page File Generator
3
+ * Generates .astro page files for CMS template pages with getStaticPaths()
4
+ */
5
+
6
+ import type { JSONPage, ComponentDefinition, CMSSchema, I18nConfig } from '../../shared/types';
7
+ import type { BreakpointConfig } from '../../shared/breakpoints';
8
+ import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
9
+ import { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
10
+ import { transformCMSTemplate } from './templateTransformer';
11
+ import type { ImageMetadataMap } from '../ssr/imageMetadata';
12
+ import type { SlugMap } from '../../shared/slugTranslator';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface CMSPageEmitOptions {
19
+ /** Page data */
20
+ pageData: JSONPage;
21
+ /** All global components */
22
+ globalComponents: Record<string, ComponentDefinition>;
23
+ /** CMS collection schema */
24
+ cmsSchema: CMSSchema;
25
+ /** Page title (may contain {{cms.field}}) */
26
+ title: string;
27
+ /** Page meta HTML */
28
+ meta: string;
29
+ /** Locale */
30
+ locale: string;
31
+ /** Default theme */
32
+ theme: string;
33
+ /** Font preloads HTML */
34
+ fontPreloads: string;
35
+ /** Library tags */
36
+ libraryTags: { headCSS?: string; headJS?: string; bodyEndJS?: string };
37
+ /** Script paths */
38
+ scriptPaths: string[];
39
+ /** Import path to BaseLayout */
40
+ layoutImportPath: string;
41
+ /** File depth relative to src/pages */
42
+ fileDepth: number;
43
+ /** SSR HTML fallbacks: node path -> rendered HTML (for ListNode, LocaleListNode) */
44
+ ssrFallbacks: Map<string, string>;
45
+ /** Page name (without extension) */
46
+ pageName: string;
47
+ /** Breakpoint config for responsive Tailwind classes */
48
+ breakpoints?: BreakpointConfig;
49
+ /** Image metadata map for responsive image generation */
50
+ imageMetadataMap?: ImageMetadataMap;
51
+ /** Internationalization config */
52
+ i18nConfig: I18nConfig;
53
+ /** Whether site has multiple locales */
54
+ isMultiLocale: boolean;
55
+ /** Slug mappings for translating internal link hrefs */
56
+ slugMappings?: SlugMap[];
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Helpers
61
+ // ---------------------------------------------------------------------------
62
+
63
+ function escapeTemplateLiteral(s: string): string {
64
+ return s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
65
+ }
66
+
67
+ function escapeJSX(s: string): string {
68
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
69
+ }
70
+
71
+ function componentImportPath(fileDepth: number, componentName: string): string {
72
+ const ups = '../'.repeat(fileDepth + 1);
73
+ return `${ups}components/${componentName}.astro`;
74
+ }
75
+
76
+ /**
77
+ * Collect rich-text field names from CMS schema
78
+ */
79
+ function collectRichTextFields(schema: CMSSchema): Set<string> {
80
+ const richTextFields = new Set<string>();
81
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
82
+ if (fieldDef.type === 'rich-text') {
83
+ richTextFields.add(fieldName);
84
+ }
85
+ }
86
+ return richTextFields;
87
+ }
88
+
89
+ /**
90
+ * Transform a title string that may contain {{cms.field}} to an Astro expression.
91
+ * Returns the transformed title suitable for use in a JSX attribute.
92
+ */
93
+ function transformTitleExpression(
94
+ title: string,
95
+ binding: string,
96
+ richTextFields: Set<string>,
97
+ wrapFn?: string
98
+ ): string {
99
+ if (!/\{\{cms\./.test(title)) {
100
+ return `"${escapeJSX(title)}"`;
101
+ }
102
+
103
+ const w = (expr: string) => wrapFn ? `${wrapFn}(${expr})` : expr;
104
+
105
+ // Full match: entire title is a single {{cms.field}}
106
+ const fullMatch = title.match(/^\{\{cms\.([^}]+)\}\}$/);
107
+ if (fullMatch) {
108
+ return `{${w(`${binding}.data.${fullMatch[1].trim()}`)}}`;
109
+ }
110
+
111
+ // Mixed content: "Page - {{cms.title}}" -> {`Page - ${entry.data.title}`}
112
+ const replaced = title.replace(/\{\{cms\.([^}]+)\}\}/g, (_, fieldPath) => {
113
+ return `\${${w(`${binding}.data.${fieldPath.trim()}`)}}`;
114
+ });
115
+ return `{\`${replaced}\`}`;
116
+ }
117
+
118
+ /**
119
+ * Extract the path prefix from a URL pattern.
120
+ * E.g., "/blog/{{slug}}" -> "blog/"
121
+ * E.g., "/posts/{{slug}}" -> "posts/"
122
+ */
123
+ function extractPathPrefix(urlPattern: string): string {
124
+ // Remove leading slash, then remove the slug placeholder and everything after
125
+ const withoutLeading = urlPattern.replace(/^\//, '');
126
+ const idx = withoutLeading.indexOf('{{');
127
+ if (idx <= 0) return '';
128
+ return withoutLeading.substring(0, idx);
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // getStaticPaths generator
133
+ // ---------------------------------------------------------------------------
134
+
135
+ function buildGetStaticPaths(
136
+ schema: CMSSchema,
137
+ isMultiLocale: boolean,
138
+ i18nConfig: I18nConfig,
139
+ locale?: string
140
+ ): string {
141
+ const collectionId = schema.id;
142
+ const slugField = schema.slugField || 'slug';
143
+ const pathPrefix = extractPathPrefix(schema.urlPattern);
144
+ const targetLocale = locale || i18nConfig.defaultLocale;
145
+
146
+ if (!isMultiLocale) {
147
+ // Single-locale version: resolve slug for this specific locale
148
+ // Route file is at blog/[slug].astro (or pl/blog/[slug].astro for non-default)
149
+ // If i18n values exist, resolve for the target locale
150
+ const slugExpr = i18nConfig.locales.length > 1
151
+ ? `entry.data.${slugField}?.${targetLocale} || entry.data.${slugField} || entry.id`
152
+ : `entry.data.${slugField} || entry.id`;
153
+
154
+ return [
155
+ `export async function getStaticPaths() {`,
156
+ ` const entries = await getCollection('${collectionId}');`,
157
+ ` return entries.map(entry => ({`,
158
+ ` params: { slug: ${slugExpr} },`,
159
+ ` props: { entry },`,
160
+ ` }));`,
161
+ `}`,
162
+ ``,
163
+ `const { entry } = Astro.props;`,
164
+ ].join('\n');
165
+ }
166
+
167
+ // Multi-locale version: enumerate items x locales
168
+ // Route file is at [...slug].astro (top level), so slug includes full path
169
+ const defaultLocale = i18nConfig.defaultLocale;
170
+ const locales = i18nConfig.locales;
171
+
172
+ const lines: string[] = [
173
+ `export async function getStaticPaths() {`,
174
+ ` const entries = await getCollection('${collectionId}');`,
175
+ ` const paths = [];`,
176
+ ` for (const entry of entries) {`,
177
+ ];
178
+
179
+ for (const locale of locales) {
180
+ const code = locale.code;
181
+ const slugExpr = `entry.data.${slugField}?.${code} || entry.data.${slugField} || entry.id`;
182
+
183
+ if (code === defaultLocale) {
184
+ // Default locale: include path prefix but no locale prefix
185
+ // e.g., /blog/{{slug}} → slug = "blog/hello"
186
+ if (pathPrefix) {
187
+ lines.push(
188
+ ` paths.push({`,
189
+ ` params: { slug: \`${pathPrefix}\${${slugExpr}}\` },`,
190
+ ` props: { entry, locale: '${code}' },`,
191
+ ` });`
192
+ );
193
+ } else {
194
+ lines.push(
195
+ ` paths.push({`,
196
+ ` params: { slug: ${slugExpr} },`,
197
+ ` props: { entry, locale: '${code}' },`,
198
+ ` });`
199
+ );
200
+ }
201
+ } else {
202
+ // Non-default locale: locale prefix + path prefix + slug
203
+ // e.g., slug = "pl/blog/witaj"
204
+ lines.push(
205
+ ` paths.push({`,
206
+ ` params: { slug: \`${code}/${pathPrefix}\${${slugExpr}}\` },`,
207
+ ` props: { entry, locale: '${code}' },`,
208
+ ` });`
209
+ );
210
+ }
211
+ }
212
+
213
+ lines.push(
214
+ ` }`,
215
+ ` return paths;`,
216
+ `}`,
217
+ ``,
218
+ `const { entry, locale = '${defaultLocale}' } = Astro.props;`
219
+ );
220
+
221
+ return lines.join('\n');
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // Main emitter
226
+ // ---------------------------------------------------------------------------
227
+
228
+ /**
229
+ * Generate a CMS template .astro page file with getStaticPaths()
230
+ */
231
+ export function emitCMSPage(options: CMSPageEmitOptions): string {
232
+ const {
233
+ pageData,
234
+ globalComponents,
235
+ cmsSchema,
236
+ title,
237
+ meta,
238
+ locale,
239
+ theme,
240
+ fontPreloads,
241
+ libraryTags,
242
+ scriptPaths,
243
+ layoutImportPath,
244
+ fileDepth,
245
+ ssrFallbacks,
246
+ pageName,
247
+ breakpoints: breakpointsOpt,
248
+ imageMetadataMap,
249
+ i18nConfig,
250
+ isMultiLocale,
251
+ slugMappings,
252
+ } = options;
253
+
254
+ const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
255
+ const binding = 'entry';
256
+ const richTextFields = collectRichTextFields(cmsSchema);
257
+ const wrapFn = 'r';
258
+
259
+ const root = pageData.root;
260
+ if (!root) {
261
+ return buildEmptyCMSPage(
262
+ layoutImportPath,
263
+ title,
264
+ meta,
265
+ locale,
266
+ theme,
267
+ fontPreloads,
268
+ libraryTags,
269
+ scriptPaths,
270
+ cmsSchema,
271
+ isMultiLocale,
272
+ i18nConfig,
273
+ binding,
274
+ richTextFields
275
+ );
276
+ }
277
+
278
+ // Build the Astro emit context with CMS mode enabled
279
+ const ctx: AstroEmitContext = {
280
+ imports: new Set<string>(),
281
+ isComponentDef: false,
282
+ componentProps: {},
283
+ globalComponents,
284
+ indent: 1, // inside BaseLayout
285
+ ssrFallbacks,
286
+ elementPath: [0],
287
+ fileType: 'page',
288
+ fileName: pageName,
289
+ breakpoints,
290
+ imageMetadataMap,
291
+ locale,
292
+ cmsMode: true,
293
+ cmsEntryBinding: binding,
294
+ cmsRichTextFields: richTextFields,
295
+ cmsWrapFn: wrapFn,
296
+ slugMappings,
297
+ i18nDefaultLocale: i18nConfig.defaultLocale,
298
+ };
299
+
300
+ // Emit the template body
301
+ const templateBody = nodeToAstro(root, ctx);
302
+
303
+ // Build frontmatter with imports
304
+ const importLines: string[] = [];
305
+ importLines.push(`import { getCollection } from 'astro:content';`);
306
+ importLines.push(`import BaseLayout from '${layoutImportPath}';`);
307
+
308
+ // Sort component imports alphabetically
309
+ const componentImports = Array.from(ctx.imports).sort();
310
+ for (const comp of componentImports) {
311
+ const path = componentImportPath(fileDepth, comp);
312
+ importLines.push(`import ${comp} from '${path}';`);
313
+ }
314
+
315
+ // Build getStaticPaths
316
+ const staticPaths = buildGetStaticPaths(cmsSchema, isMultiLocale, i18nConfig, locale);
317
+
318
+ // Build script paths array
319
+ const scriptsArrayLiteral = scriptPaths.length > 0
320
+ ? `[${scriptPaths.map((s) => `"${s}"`).join(', ')}]`
321
+ : '[]';
322
+
323
+ // Build library tags literal
324
+ const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral(libraryTags.headCSS || '')}\`, headJS: \`${escapeTemplateLiteral(libraryTags.headJS || '')}\`, bodyEndJS: \`${escapeTemplateLiteral(libraryTags.bodyEndJS || '')}\` }`;
325
+
326
+ // Escape meta first, then transform CMS templates ({{cms.X}} survives escaping intact)
327
+ const escapedMeta = escapeTemplateLiteral(meta).replace(
328
+ /\{\{cms\.([^}]+)\}\}/g,
329
+ (_, fieldPath) => `\${${wrapFn}(${binding}.data.${fieldPath.trim()})}`
330
+ );
331
+ const escapedFontPreloads = escapeTemplateLiteral(fontPreloads);
332
+
333
+ // Transform title for CMS entry data
334
+ const titleExpr = transformTitleExpression(title, binding, richTextFields, wrapFn);
335
+
336
+ // i18n resolver helper — resolves {_i18n: true, en: "...", pl: "..."} to the correct locale string
337
+ const resolverHelper = `function r(v) {
338
+ if (v && typeof v === 'object' && v._i18n) return v['${locale}'] ?? v['${i18nConfig.defaultLocale}'] ?? Object.values(v).find(x => x !== true && x !== undefined) ?? '';
339
+ return v ?? '';
340
+ }`;
341
+
342
+ return `---
343
+ ${importLines.join('\n')}
344
+
345
+ ${staticPaths}
346
+
347
+ ${resolverHelper}
348
+ ---
349
+ <BaseLayout
350
+ title=${titleExpr}
351
+ meta={\`${escapedMeta}\`}
352
+ scripts={${scriptsArrayLiteral}}
353
+ locale="${locale}"
354
+ theme="${theme}"
355
+ fontPreloads={\`${escapedFontPreloads}\`}
356
+ libraryTags={${libraryTagsLiteral}}
357
+ >
358
+ <div id="root">
359
+ ${templateBody} </div>
360
+ </BaseLayout>
361
+ `;
362
+ }
363
+
364
+ /**
365
+ * Build an empty CMS page with just the layout wrapper and getStaticPaths
366
+ */
367
+ function buildEmptyCMSPage(
368
+ layoutImport: string,
369
+ title: string,
370
+ meta: string,
371
+ locale: string,
372
+ theme: string,
373
+ fontPreloads: string,
374
+ libraryTags: { headCSS?: string; headJS?: string; bodyEndJS?: string },
375
+ scriptPaths: string[],
376
+ cmsSchema: CMSSchema,
377
+ isMultiLocale: boolean,
378
+ i18nConfig: I18nConfig,
379
+ binding: string,
380
+ richTextFields: Set<string>
381
+ ): string {
382
+ const escapedMeta = escapeTemplateLiteral(meta);
383
+ const escapedFontPreloads = escapeTemplateLiteral(fontPreloads);
384
+ const scriptsArrayLiteral = scriptPaths.length > 0
385
+ ? `[${scriptPaths.map((s) => `"${s}"`).join(', ')}]`
386
+ : '[]';
387
+ const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral(libraryTags.headCSS || '')}\`, headJS: \`${escapeTemplateLiteral(libraryTags.headJS || '')}\`, bodyEndJS: \`${escapeTemplateLiteral(libraryTags.bodyEndJS || '')}\` }`;
388
+
389
+ const wrapFn = 'r';
390
+ const staticPaths = buildGetStaticPaths(cmsSchema, isMultiLocale, i18nConfig, locale);
391
+ const titleExpr = transformTitleExpression(title, binding, richTextFields, wrapFn);
392
+
393
+ const resolverHelper = `function r(v) {
394
+ if (v && typeof v === 'object' && v._i18n) return v['${locale}'] ?? v['${i18nConfig.defaultLocale}'] ?? Object.values(v).find(x => x !== true && x !== undefined) ?? '';
395
+ return v ?? '';
396
+ }`;
397
+
398
+ return `---
399
+ import { getCollection } from 'astro:content';
400
+ import BaseLayout from '${layoutImport}';
401
+
402
+ ${staticPaths}
403
+
404
+ ${resolverHelper}
405
+ ---
406
+ <BaseLayout
407
+ title=${titleExpr}
408
+ meta={\`${escapedMeta}\`}
409
+ scripts={${scriptsArrayLiteral}}
410
+ locale="${locale}"
411
+ theme="${theme}"
412
+ fontPreloads={\`${escapedFontPreloads}\`}
413
+ libraryTags={${libraryTagsLiteral}}
414
+ >
415
+ </BaseLayout>
416
+ `;
417
+ }
@@ -87,7 +87,8 @@ export function emitAstroComponent(
87
87
  name: string,
88
88
  def: ComponentDefinition,
89
89
  allComponents: Record<string, ComponentDefinition>,
90
- breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS
90
+ breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
91
+ defaultLocale: string = 'en'
91
92
  ): string {
92
93
  const comp = def.component;
93
94
  const propDefs = comp.interface || {};
@@ -110,17 +111,20 @@ export function emitAstroComponent(
110
111
  fileType: 'component',
111
112
  fileName: name,
112
113
  breakpoints,
114
+ defaultLocale,
113
115
  };
114
116
 
115
117
  // Emit the template body
116
118
  const templateBody = nodeToAstro(structure, ctx);
117
119
 
118
120
  // Build frontmatter
119
- const frontmatter = buildFrontmatter(name, propDefs, ctx.imports, ctx.dynamicTags);
121
+ const frontmatter = buildFrontmatter(name, propDefs, ctx.imports, ctx.dynamicTags, ctx.needsI18nResolver ? defaultLocale : undefined);
120
122
 
121
123
  // Build style/script sections
122
124
  const styleSection = comp.css ? `\n<style>\n${comp.css}\n</style>\n` : '';
123
- const scriptSection = comp.javascript ? `\n<script>\n${comp.javascript}\n</script>\n` : '';
125
+ const scriptSection = comp.javascript
126
+ ? buildScriptSection(comp.javascript, comp, propDefs)
127
+ : '';
124
128
 
125
129
  return `---\n${frontmatter}---\n${templateBody}${styleSection}${scriptSection}`;
126
130
  }
@@ -132,7 +136,8 @@ function buildFrontmatter(
132
136
  componentName: string,
133
137
  propDefs: Record<string, PropDefinition>,
134
138
  imports: Set<string>,
135
- dynamicTags?: Map<string, string>
139
+ dynamicTags?: Map<string, string>,
140
+ i18nDefaultLocale?: string
136
141
  ): string {
137
142
  const lines: string[] = [];
138
143
 
@@ -164,6 +169,8 @@ function buildFrontmatter(
164
169
  const defaultVal = formatDefault(propDef);
165
170
  if (defaultVal !== null) {
166
171
  destructParts.push(`${propName} = ${defaultVal}`);
172
+ } else if (propDef.type === 'link') {
173
+ destructParts.push(`${propName} = { href: "#" }`);
167
174
  } else {
168
175
  destructParts.push(propName);
169
176
  }
@@ -190,6 +197,18 @@ function buildFrontmatter(
190
197
  }
191
198
  }
192
199
 
200
+ // i18n resolver helper — resolves { _i18n: true, en: "...", pl: "..." } at runtime
201
+ if (i18nDefaultLocale) {
202
+ lines.push('');
203
+ lines.push(`const r = (v: any) => {`);
204
+ lines.push(` if (v && typeof v === 'object' && v._i18n) {`);
205
+ lines.push(` const locale = Astro.currentLocale ?? '${i18nDefaultLocale}';`);
206
+ lines.push(` return v[locale] ?? v['${i18nDefaultLocale}'] ?? Object.values(v).find((s: any) => typeof s === 'string' && s !== '') ?? '';`);
207
+ lines.push(` }`);
208
+ lines.push(` return v ?? '';`);
209
+ lines.push(`};`);
210
+ }
211
+
193
212
  if (lines.length > 0) lines.push('');
194
213
  return lines.join('\n');
195
214
  }
@@ -203,6 +222,72 @@ function buildNoStructureComponent(
203
222
  ): string {
204
223
  let content = '---\n---\n<slot />\n';
205
224
  if (comp.css) content += `\n<style>\n${comp.css}\n</style>\n`;
206
- if (comp.javascript) content += `\n<script>\n${comp.javascript}\n</script>\n`;
225
+ if (comp.javascript) content += `\n<script is:inline>\n${comp.javascript}\n</script>\n`;
207
226
  return content;
208
227
  }
228
+
229
+ /**
230
+ * Transform JS for define:vars compatibility.
231
+ * Astro's define:vars injects each prop as a script-scope variable, not a `props` object.
232
+ * This function:
233
+ * 1. Removes `const/let/var { x, y } = props;` destructuring lines
234
+ * 2. Replaces `props.X` references with direct `X` variable access
235
+ * 3. Drops `var/let/const` from redeclarations of define:vars variables
236
+ */
237
+ function transformDefineVarsJS(js: string, varNames: string[]): string {
238
+ let result = js;
239
+
240
+ // 1. Remove destructuring from props: `const { x, y } = props;`
241
+ result = result.replace(
242
+ /^\s*(const|let|var)\s+\{([^}]+)\}\s*=\s*props\s*;?\s*$/gm,
243
+ (match, _keyword, inner) => {
244
+ const names = inner.split(',').map((s: string) => s.trim()).filter(Boolean);
245
+ if (names.every((n: string) => varNames.includes(n))) return '';
246
+ return match;
247
+ }
248
+ );
249
+
250
+ // 2. Replace `props.X` with `X` (longest names first to avoid substring conflicts)
251
+ const sorted = [...varNames].sort((a, b) => b.length - a.length);
252
+ for (const name of sorted) {
253
+ result = result.replace(new RegExp(`props\\.${name}\\b`, 'g'), name);
254
+ }
255
+
256
+ // 3. Remove redeclarations of define:vars variables (already injected as const by Astro)
257
+ for (const name of varNames) {
258
+ result = result.replace(
259
+ new RegExp(`^\\s*(var|let|const)\\s+${name}\\s*=[^;]*;?\\s*$`, 'gm'),
260
+ ''
261
+ );
262
+ }
263
+
264
+ return result;
265
+ }
266
+
267
+ /**
268
+ * Build the script section with proper el/props initialization.
269
+ * - defineVars components: use Astro's define:vars to pass props into inline script
270
+ * - other components: use is:inline to avoid module bundling
271
+ * Both cases use document.currentScript.previousElementSibling to get el.
272
+ */
273
+ function buildScriptSection(
274
+ js: string,
275
+ comp: StructuredComponentDefinition,
276
+ propDefs: Record<string, PropDefinition>
277
+ ): string {
278
+ const elInit = 'const el = document.currentScript.previousElementSibling;';
279
+
280
+ if (comp.defineVars) {
281
+ const vars = comp.defineVars === true
282
+ ? Object.keys(propDefs).filter(k => k !== 'children')
283
+ : comp.defineVars;
284
+
285
+ if (vars.length > 0) {
286
+ const transformedJS = transformDefineVarsJS(js, vars);
287
+ const defineVarsObj = `{ ${vars.join(', ')} }`;
288
+ return `\n<script define:vars={${defineVarsObj}}>\n${elInit}\n${transformedJS}\n</script>\n`;
289
+ }
290
+ }
291
+
292
+ return `\n<script is:inline>\n${elInit}\n${js}\n</script>\n`;
293
+ }