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.
- package/build-astro.ts +195 -68
- package/dist/bin/cli.js +1 -1
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-WK5XLASY.js → chunk-3NOZVNM4.js} +3 -3
- package/dist/chunks/{chunk-W6HDII4T.js → chunk-GKICS7CF.js} +27 -14
- package/dist/chunks/chunk-GKICS7CF.js.map +7 -0
- package/dist/chunks/{chunk-P3FX5HJM.js → chunk-LOJLO2EY.js} +1 -1
- package/dist/chunks/chunk-LOJLO2EY.js.map +7 -0
- package/dist/chunks/{chunk-HNAS6BSS.js → chunk-MOCRENNU.js} +55 -5
- package/dist/chunks/{chunk-HNAS6BSS.js.map → chunk-MOCRENNU.js.map} +3 -3
- package/dist/chunks/{chunk-NV25WXCA.js → chunk-OJ5SROQN.js} +5 -3
- package/dist/chunks/chunk-OJ5SROQN.js.map +7 -0
- package/dist/chunks/{chunk-AIXKUVNG.js → chunk-V4SVSX3X.js} +3 -3
- package/dist/chunks/{chunk-KULPBDC7.js → chunk-Z7SAOCDG.js} +5 -2
- package/dist/chunks/{chunk-KULPBDC7.js.map → chunk-Z7SAOCDG.js.map} +2 -2
- package/dist/chunks/{constants-5CRJRQNR.js → constants-L75FR445.js} +2 -2
- package/dist/entries/server-router.js +6 -6
- package/dist/lib/client/index.js +5 -5
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +2007 -197
- 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/builders/embedBuilder.ts +2 -2
- package/lib/server/astro/cmsPageEmitter.ts +417 -0
- package/lib/server/astro/componentEmitter.ts +90 -5
- package/lib/server/astro/nodeToAstro.ts +830 -37
- package/lib/server/astro/pageEmitter.ts +39 -3
- package/lib/server/astro/tailwindMapper.ts +69 -8
- package/lib/server/astro/templateTransformer.ts +107 -0
- package/lib/server/index.ts +9 -0
- package/lib/server/routes/api/components.ts +62 -0
- package/lib/server/routes/api/core-routes.ts +8 -0
- package/lib/server/ssr/ssrRenderer.ts +30 -10
- 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 +2 -0
- package/lib/shared/types/components.ts +1 -0
- package/lib/shared/validation/schemas.ts +1 -0
- package/package.json +1 -1
- 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-3NOZVNM4.js.map} +0 -0
- /package/dist/chunks/{chunk-AIXKUVNG.js.map → chunk-V4SVSX3X.js.map} +0 -0
- /package/dist/chunks/{constants-5CRJRQNR.js.map → constants-L75FR445.js.map} +0 -0
package/dist/lib/shared/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
isValidIdentifier,
|
|
24
24
|
resolvePaletteColor,
|
|
25
25
|
resolveSafePath
|
|
26
|
-
} from "../../chunks/chunk-
|
|
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-
|
|
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-
|
|
258
|
+
} from "../../chunks/chunk-Z7SAOCDG.js";
|
|
259
259
|
import "../../chunks/chunk-KSBZ2L7C.js";
|
|
260
260
|
|
|
261
261
|
// lib/shared/index.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, '&').replace(/"/g, '"');
|
|
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
|
|
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
|
+
}
|