meno-core 1.0.38 → 1.0.39

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.
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Page File Generator
3
+ * Generates .astro page files that import and compose components
4
+ */
5
+
6
+ import type { JSONPage, ComponentDefinition } 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
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface PageEmitOptions {
16
+ /** Page data */
17
+ pageData: JSONPage;
18
+ /** All global components */
19
+ globalComponents: Record<string, ComponentDefinition>;
20
+ /** Page title */
21
+ title: string;
22
+ /** Page meta HTML */
23
+ meta: string;
24
+ /** Locale */
25
+ locale: string;
26
+ /** Default theme */
27
+ theme: string;
28
+ /** Font preloads HTML */
29
+ fontPreloads: string;
30
+ /** Library tags */
31
+ libraryTags: { headCSS?: string; headJS?: string; bodyEndJS?: string };
32
+ /** Script paths */
33
+ scriptPaths: string[];
34
+ /** Import path to BaseLayout */
35
+ layoutImportPath: string;
36
+ /** File depth relative to src/pages */
37
+ fileDepth: number;
38
+ /** SSR HTML fallbacks: node path → rendered HTML (for ListNode, LocaleListNode) */
39
+ ssrFallbacks: Map<string, string>;
40
+ /** Page name (without extension) */
41
+ pageName: string;
42
+ /** Breakpoint config for responsive Tailwind classes */
43
+ breakpoints?: BreakpointConfig;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Helpers
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function escapeTemplateLiteral(s: string): string {
51
+ return s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
52
+ }
53
+
54
+ function escapeJSX(s: string): string {
55
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
56
+ }
57
+
58
+ /**
59
+ * Compute relative import path from page to components directory
60
+ */
61
+ function componentImportPath(fileDepth: number, componentName: string): string {
62
+ // fileDepth 0 = src/pages/index.astro → ../components/X.astro
63
+ // fileDepth 1 = src/pages/en/index.astro → ../../components/X.astro
64
+ const ups = '../'.repeat(fileDepth + 1);
65
+ return `${ups}components/${componentName}.astro`;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Main emitter
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * Generate a page .astro file from page data with component imports
74
+ */
75
+ export function emitAstroPage(options: PageEmitOptions): string {
76
+ const {
77
+ pageData,
78
+ globalComponents,
79
+ title,
80
+ meta,
81
+ locale,
82
+ theme,
83
+ fontPreloads,
84
+ libraryTags,
85
+ scriptPaths,
86
+ layoutImportPath,
87
+ fileDepth,
88
+ ssrFallbacks,
89
+ pageName,
90
+ breakpoints: breakpointsOpt,
91
+ } = options;
92
+
93
+ const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
94
+
95
+ const root = pageData.root;
96
+ if (!root) {
97
+ // Empty page - just layout wrapper
98
+ return buildEmptyPage(layoutImportPath, title, meta, locale, theme, fontPreloads, libraryTags, scriptPaths);
99
+ }
100
+
101
+ // Build the Astro emit context
102
+ const ctx: AstroEmitContext = {
103
+ imports: new Set<string>(),
104
+ isComponentDef: false,
105
+ componentProps: {},
106
+ globalComponents,
107
+ indent: 1, // inside BaseLayout
108
+ ssrFallbacks,
109
+ elementPath: [0],
110
+ fileType: 'page',
111
+ fileName: pageName,
112
+ breakpoints,
113
+ };
114
+
115
+ // Emit the template body
116
+ const templateBody = nodeToAstro(root, ctx);
117
+
118
+ // Build frontmatter with imports
119
+ const importLines: string[] = [];
120
+ importLines.push(`import BaseLayout from '${layoutImportPath}';`);
121
+
122
+ // Sort component imports alphabetically
123
+ const componentImports = Array.from(ctx.imports).sort();
124
+ for (const comp of componentImports) {
125
+ const path = componentImportPath(fileDepth, comp);
126
+ importLines.push(`import ${comp} from '${path}';`);
127
+ }
128
+
129
+ // Build script paths array
130
+ const scriptsArrayLiteral = scriptPaths.length > 0
131
+ ? `[${scriptPaths.map((s) => `"${s}"`).join(', ')}]`
132
+ : '[]';
133
+
134
+ // Build library tags literal
135
+ const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral(libraryTags.headCSS || '')}\`, headJS: \`${escapeTemplateLiteral(libraryTags.headJS || '')}\`, bodyEndJS: \`${escapeTemplateLiteral(libraryTags.bodyEndJS || '')}\` }`;
136
+
137
+ const escapedMeta = escapeTemplateLiteral(meta);
138
+ const escapedFontPreloads = escapeTemplateLiteral(fontPreloads);
139
+
140
+ return `---
141
+ ${importLines.join('\n')}
142
+ ---
143
+ <BaseLayout
144
+ title="${escapeJSX(title)}"
145
+ meta={\`${escapedMeta}\`}
146
+ scripts={${scriptsArrayLiteral}}
147
+ locale="${locale}"
148
+ theme="${theme}"
149
+ fontPreloads={\`${escapedFontPreloads}\`}
150
+ libraryTags={${libraryTagsLiteral}}
151
+ >
152
+ ${templateBody}</BaseLayout>
153
+ `;
154
+ }
155
+
156
+ /**
157
+ * Build an empty page with just the layout wrapper
158
+ */
159
+ function buildEmptyPage(
160
+ layoutImport: string,
161
+ title: string,
162
+ meta: string,
163
+ locale: string,
164
+ theme: string,
165
+ fontPreloads: string,
166
+ libraryTags: { headCSS?: string; headJS?: string; bodyEndJS?: string },
167
+ scriptPaths: string[]
168
+ ): string {
169
+ const escapedMeta = escapeTemplateLiteral(meta);
170
+ const escapedFontPreloads = escapeTemplateLiteral(fontPreloads);
171
+ const scriptsArrayLiteral = scriptPaths.length > 0
172
+ ? `[${scriptPaths.map((s) => `"${s}"`).join(', ')}]`
173
+ : '[]';
174
+ const libraryTagsLiteral = `{ headCSS: \`${escapeTemplateLiteral(libraryTags.headCSS || '')}\`, headJS: \`${escapeTemplateLiteral(libraryTags.headJS || '')}\`, bodyEndJS: \`${escapeTemplateLiteral(libraryTags.bodyEndJS || '')}\` }`;
175
+
176
+ return `---
177
+ import BaseLayout from '${layoutImport}';
178
+ ---
179
+ <BaseLayout
180
+ title="${escapeJSX(title)}"
181
+ meta={\`${escapedMeta}\`}
182
+ scripts={${scriptsArrayLiteral}}
183
+ locale="${locale}"
184
+ theme="${theme}"
185
+ fontPreloads={\`${escapedFontPreloads}\`}
186
+ libraryTags={${libraryTagsLiteral}}
187
+ >
188
+ </BaseLayout>
189
+ `;
190
+ }