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,208 @@
1
+ /**
2
+ * Component File Generator
3
+ * Generates .astro files from StructuredComponentDefinitions
4
+ */
5
+
6
+ import type {
7
+ ComponentDefinition,
8
+ StructuredComponentDefinition,
9
+ PropDefinition,
10
+ } from '../../shared/types';
11
+ import type { BasePropDefinition, ListPropDefinition, LinkPropValue } from '../../shared/types/components';
12
+ import type { BreakpointConfig } from '../../shared/breakpoints';
13
+ import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
14
+ import { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Convert a PropDefinition to a TypeScript type string
22
+ */
23
+ function propDefToTSType(def: PropDefinition): string {
24
+ switch (def.type) {
25
+ case 'string':
26
+ case 'rich-text':
27
+ case 'file':
28
+ return 'string';
29
+ case 'number':
30
+ return 'number';
31
+ case 'boolean':
32
+ return 'boolean';
33
+ case 'select':
34
+ if ('options' in def && def.options && def.options.length > 0) {
35
+ return def.options.map((o) => `'${o}'`).join(' | ');
36
+ }
37
+ return 'string';
38
+ case 'link':
39
+ return '{ href: string; target?: string }';
40
+ case 'list':
41
+ return 'any[]';
42
+ default:
43
+ return 'any';
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Format a default value for destructuring
49
+ */
50
+ function formatDefault(def: PropDefinition): string | null {
51
+ if (!('default' in def) || def.default === undefined) return null;
52
+ const val = def.default;
53
+
54
+ if (typeof val === 'string') return JSON.stringify(val);
55
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val);
56
+
57
+ // I18nValue
58
+ if (typeof val === 'object' && val !== null && '_i18n' in val) {
59
+ // Use the first available locale value
60
+ for (const [key, v] of Object.entries(val)) {
61
+ if (key !== '_i18n' && typeof v === 'string') return JSON.stringify(v);
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // Link value
67
+ if (typeof val === 'object' && val !== null && 'href' in val) {
68
+ return JSON.stringify(val);
69
+ }
70
+
71
+ // Arrays
72
+ if (Array.isArray(val)) {
73
+ return JSON.stringify(val);
74
+ }
75
+
76
+ return JSON.stringify(val);
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Main emitter
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Generate a .astro file string from a component definition
85
+ */
86
+ export function emitAstroComponent(
87
+ name: string,
88
+ def: ComponentDefinition,
89
+ allComponents: Record<string, ComponentDefinition>,
90
+ breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS
91
+ ): string {
92
+ const comp = def.component;
93
+ const propDefs = comp.interface || {};
94
+ const structure = comp.structure;
95
+
96
+ if (!structure) {
97
+ // Component with no structure - just CSS/JS
98
+ return buildNoStructureComponent(name, comp);
99
+ }
100
+
101
+ // Build the Astro context for template emission
102
+ const ctx: AstroEmitContext = {
103
+ imports: new Set<string>(),
104
+ isComponentDef: true,
105
+ componentProps: propDefs,
106
+ globalComponents: allComponents,
107
+ indent: 0,
108
+ ssrFallbacks: new Map(),
109
+ elementPath: [0],
110
+ fileType: 'component',
111
+ fileName: name,
112
+ breakpoints,
113
+ };
114
+
115
+ // Emit the template body
116
+ const templateBody = nodeToAstro(structure, ctx);
117
+
118
+ // Build frontmatter
119
+ const frontmatter = buildFrontmatter(name, propDefs, ctx.imports, ctx.dynamicTags);
120
+
121
+ // Build style/script sections
122
+ 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` : '';
124
+
125
+ return `---\n${frontmatter}---\n${templateBody}${styleSection}${scriptSection}`;
126
+ }
127
+
128
+ /**
129
+ * Build the frontmatter section (imports, Props interface, destructuring)
130
+ */
131
+ function buildFrontmatter(
132
+ componentName: string,
133
+ propDefs: Record<string, PropDefinition>,
134
+ imports: Set<string>,
135
+ dynamicTags?: Map<string, string>
136
+ ): string {
137
+ const lines: string[] = [];
138
+
139
+ // Component imports
140
+ for (const imp of Array.from(imports).sort()) {
141
+ lines.push(`import ${imp} from './${imp}.astro';`);
142
+ }
143
+
144
+ if (lines.length > 0) lines.push('');
145
+
146
+ const propEntries = Object.entries(propDefs);
147
+
148
+ if (propEntries.length > 0) {
149
+ // Interface
150
+ lines.push('interface Props {');
151
+ for (const [propName, propDef] of propEntries) {
152
+ if (propName === 'children') continue;
153
+ const tsType = propDefToTSType(propDef);
154
+ const optional = 'default' in propDef && propDef.default !== undefined;
155
+ lines.push(` ${propName}${optional ? '?' : ''}: ${tsType};`);
156
+ }
157
+ lines.push('}');
158
+ lines.push('');
159
+
160
+ // Destructuring with defaults
161
+ const destructParts: string[] = [];
162
+ for (const [propName, propDef] of propEntries) {
163
+ if (propName === 'children') continue;
164
+ const defaultVal = formatDefault(propDef);
165
+ if (defaultVal !== null) {
166
+ destructParts.push(`${propName} = ${defaultVal}`);
167
+ } else {
168
+ destructParts.push(propName);
169
+ }
170
+ }
171
+
172
+ if (destructParts.length > 0) {
173
+ if (destructParts.length <= 3 && destructParts.join(', ').length < 80) {
174
+ lines.push(`const { ${destructParts.join(', ')} } = Astro.props;`);
175
+ } else {
176
+ lines.push('const {');
177
+ for (const part of destructParts) {
178
+ lines.push(` ${part},`);
179
+ }
180
+ lines.push('} = Astro.props;');
181
+ }
182
+ }
183
+ }
184
+
185
+ // Dynamic tag definitions (e.g., const Tag_0_0 = `h${size}`)
186
+ if (dynamicTags && dynamicTags.size > 0) {
187
+ lines.push('');
188
+ for (const [varName, templateExpr] of dynamicTags) {
189
+ lines.push(`const ${varName} = \`${templateExpr}\`;`);
190
+ }
191
+ }
192
+
193
+ if (lines.length > 0) lines.push('');
194
+ return lines.join('\n');
195
+ }
196
+
197
+ /**
198
+ * Build a component with no structure (CSS/JS only)
199
+ */
200
+ function buildNoStructureComponent(
201
+ name: string,
202
+ comp: StructuredComponentDefinition
203
+ ): string {
204
+ let content = '---\n---\n<slot />\n';
205
+ 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`;
207
+ return content;
208
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Mapping Variant CSS Collector (Tailwind)
3
+ * Walks all component definitions to find StyleMapping objects,
4
+ * generates Tailwind classes for every possible value, ensuring
5
+ * the Tailwind safelist covers all prop variants.
6
+ */
7
+
8
+ import type { ComponentDefinition, ComponentNode } from '../../shared/types';
9
+ import type {
10
+ StyleObject,
11
+ ResponsiveStyleObject,
12
+ StyleMapping,
13
+ } from '../../shared/types/styles';
14
+ import type { BreakpointConfig } from '../../shared/breakpoints';
15
+ import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
16
+ import { propertyToTailwind } from './tailwindMapper';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function isStyleMapping(value: unknown): value is StyleMapping {
23
+ return (
24
+ typeof value === 'object' &&
25
+ value !== null &&
26
+ '_mapping' in value &&
27
+ (value as StyleMapping)._mapping === true
28
+ );
29
+ }
30
+
31
+ function isResponsiveStyle(
32
+ style: StyleObject | ResponsiveStyleObject
33
+ ): style is ResponsiveStyleObject {
34
+ return 'base' in style || 'tablet' in style || 'mobile' in style;
35
+ }
36
+
37
+ /**
38
+ * Walk a style object and collect Tailwind classes for every possible mapping value
39
+ */
40
+ function collectFromStyle(
41
+ style: StyleObject | ResponsiveStyleObject | undefined,
42
+ classes: Set<string>,
43
+ breakpoints: BreakpointConfig
44
+ ): void {
45
+ if (!style) return;
46
+
47
+ if (isResponsiveStyle(style)) {
48
+ for (const [bp, bpStyle] of Object.entries(style)) {
49
+ if (!bpStyle) continue;
50
+ let prefix = '';
51
+ if (bp !== 'base') {
52
+ const bpValue = breakpoints[bp]?.breakpoint;
53
+ if (bpValue) {
54
+ prefix = `max-[${bpValue}px]:`;
55
+ }
56
+ }
57
+ collectFromFlatStyle(bpStyle, prefix, classes);
58
+ }
59
+ } else {
60
+ collectFromFlatStyle(style, '', classes);
61
+ }
62
+ }
63
+
64
+ function collectFromFlatStyle(
65
+ style: StyleObject,
66
+ prefix: string,
67
+ classes: Set<string>
68
+ ): void {
69
+ for (const [property, value] of Object.entries(style)) {
70
+ if (!isStyleMapping(value)) continue;
71
+
72
+ // Generate a Tailwind class for each possible value in the mapping
73
+ for (const [, cssValue] of Object.entries(value.values)) {
74
+ const twClass = propertyToTailwind(property, cssValue);
75
+ if (twClass) {
76
+ classes.add(prefix ? `${prefix}${twClass}` : twClass);
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Recursively walk a component node tree to collect mapping classes
84
+ */
85
+ function walkNode(
86
+ node: ComponentNode | ComponentNode[] | string | number | null | undefined,
87
+ classes: Set<string>,
88
+ breakpoints: BreakpointConfig
89
+ ): void {
90
+ if (!node || typeof node === 'string' || typeof node === 'number') return;
91
+
92
+ if (Array.isArray(node)) {
93
+ for (const child of node) {
94
+ walkNode(child, classes, breakpoints);
95
+ }
96
+ return;
97
+ }
98
+
99
+ // Collect from style
100
+ if ('style' in node && node.style) {
101
+ collectFromStyle(node.style as StyleObject | ResponsiveStyleObject, classes, breakpoints);
102
+ }
103
+
104
+ // Collect from interactive styles
105
+ if ('interactiveStyles' in node && Array.isArray((node as any).interactiveStyles)) {
106
+ for (const rule of (node as any).interactiveStyles) {
107
+ if (rule.style) {
108
+ collectFromStyle(rule.style, classes, breakpoints);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Recurse into children
114
+ if ('children' in node && node.children) {
115
+ if (Array.isArray(node.children)) {
116
+ for (const child of node.children) {
117
+ walkNode(child as ComponentNode, classes, breakpoints);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Main export
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Walk all component definitions, find all StyleMapping objects,
129
+ * and generate Tailwind classes for every value.
130
+ *
131
+ * @returns Set of Tailwind class names for the safelist
132
+ */
133
+ export function collectAllMappingClasses(
134
+ componentDefs: Record<string, ComponentDefinition>,
135
+ breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS
136
+ ): Set<string> {
137
+ const classes = new Set<string>();
138
+
139
+ for (const def of Object.values(componentDefs)) {
140
+ const structure = def.component?.structure;
141
+ if (structure) {
142
+ walkNode(structure, classes, breakpoints);
143
+ }
144
+ }
145
+
146
+ return classes;
147
+ }
@@ -0,0 +1,5 @@
1
+ export { nodeToAstro, type AstroEmitContext } from './nodeToAstro';
2
+ export { emitAstroComponent } from './componentEmitter';
3
+ export { emitAstroPage, type PageEmitOptions } from './pageEmitter';
4
+ export { collectAllMappingClasses } from './cssCollector';
5
+ export { propertyToTailwind, stylesToTailwind, responsiveStylesToTailwind } from './tailwindMapper';