meno-core 1.0.0

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 (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. package/vite.config.ts +43 -0
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Validation Schemas
3
+ * Zod schemas for runtime validation of JSON data structures
4
+ * All schemas use .passthrough() to maintain backward compatibility with unknown fields
5
+ *
6
+ * Note: Node type definitions also define their own schemas for TypeScript type inference.
7
+ * The schemas here are the authoritative source for VALIDATION (with proper recursive
8
+ * ComponentNode references). Node type files use simplified schemas for type inference.
9
+ */
10
+
11
+ import { z } from 'zod';
12
+ import type { StyleObject, StyleMapping, LinkMapping } from '../types/styles';
13
+ import { NODE_TYPE } from '../constants';
14
+
15
+ /**
16
+ * Prop type schema - validates PropType values
17
+ */
18
+ export const PropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file']);
19
+
20
+ /**
21
+ * Prop definition schema
22
+ * Validates prop definitions from component interfaces
23
+ */
24
+ export const PropDefinitionSchema = z.object({
25
+ type: PropTypeSchema,
26
+ default: z.union([z.string(), z.number(), z.boolean(), z.object({ href: z.string(), target: z.string().optional() })]).optional(),
27
+ options: z.array(z.string()).readonly().optional(),
28
+ accept: z.string().optional(), // For 'file' type: MIME pattern like "image/*", "video/*"
29
+ }).passthrough();
30
+
31
+ /**
32
+ * Style mapping schema
33
+ */
34
+ export const StyleMappingSchema: z.ZodType<StyleMapping> = z.object({
35
+ _mapping: z.literal(true),
36
+ prop: z.string(),
37
+ values: z.record(z.string(), z.union([z.string(), z.number()])),
38
+ }).passthrough();
39
+
40
+ /**
41
+ * Link mapping schema - for object-link href mappings
42
+ * values is optional - when missing, acts as passthrough for link-type props
43
+ */
44
+ export const LinkMappingSchema: z.ZodType<LinkMapping> = z.object({
45
+ _mapping: z.literal(true),
46
+ prop: z.string(),
47
+ values: z.record(z.string(), z.object({
48
+ href: z.string(),
49
+ target: z.string().optional(),
50
+ })).optional(),
51
+ }).passthrough();
52
+
53
+ /**
54
+ * Style object schema - flat style object
55
+ */
56
+ export const StyleObjectSchema: z.ZodType<StyleObject> = z.record(
57
+ z.string(),
58
+ z.union([
59
+ z.string(),
60
+ z.number(),
61
+ StyleMappingSchema,
62
+ ])
63
+ );
64
+
65
+ /**
66
+ * Responsive style object schema
67
+ */
68
+ export const ResponsiveStyleObjectSchema = z.object({
69
+ base: StyleObjectSchema.optional(),
70
+ tablet: StyleObjectSchema.optional(),
71
+ mobile: StyleObjectSchema.optional(),
72
+ }).passthrough();
73
+
74
+ /**
75
+ * Style value schema - can be either flat or responsive
76
+ */
77
+ export const StyleValueSchema = z.union([
78
+ StyleObjectSchema,
79
+ ResponsiveStyleObjectSchema,
80
+ ]);
81
+
82
+ // ============================================================================
83
+ // Node Type Schemas (Validation)
84
+ // These schemas handle proper recursive validation with ComponentNode references.
85
+ // Node type files also have schemas for TypeScript type inference.
86
+ // ============================================================================
87
+
88
+ /**
89
+ * Slot marker schema
90
+ */
91
+ export const SlotMarkerSchema = z.object({
92
+ type: z.literal(NODE_TYPE.SLOT),
93
+ }).passthrough();
94
+
95
+ /**
96
+ * Component node schema - discriminated union (forward declaration)
97
+ * Uses z.ZodType<any> because schema is more permissive than TypeScript types
98
+ */
99
+ let ComponentNodeSchema: z.ZodType<any>;
100
+
101
+ /**
102
+ * HTML node schema - recursive definition
103
+ */
104
+ export const HtmlNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
105
+ type: z.literal(NODE_TYPE.NODE),
106
+ tag: z.string(),
107
+ style: StyleValueSchema.optional(),
108
+ attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
109
+ props: z.record(z.string(), z.any()).optional(), // Allow props for backward compatibility
110
+ children: z.union([
111
+ z.array(z.union([ComponentNodeSchema, z.string()])),
112
+ z.string(),
113
+ ]).optional(),
114
+ }).passthrough());
115
+
116
+ /**
117
+ * Component instance node schema - recursive definition
118
+ */
119
+ export const ComponentInstanceNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
120
+ type: z.literal(NODE_TYPE.COMPONENT),
121
+ component: z.string(),
122
+ props: z.record(z.string(), z.any()).optional(),
123
+ style: StyleValueSchema.optional(),
124
+ attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
125
+ children: z.union([
126
+ z.array(z.union([ComponentNodeSchema, z.string()])),
127
+ z.string(),
128
+ ]).optional(),
129
+ }).passthrough());
130
+
131
+ /**
132
+ * Embed node schema - for rendering custom HTML/SVG content
133
+ */
134
+ export const EmbedNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
135
+ type: z.literal(NODE_TYPE.EMBED),
136
+ html: z.string(),
137
+ style: StyleValueSchema.optional(),
138
+ attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
139
+ }).passthrough());
140
+
141
+ /**
142
+ * Object link node schema - renders as div in editor, <a> in SSR
143
+ */
144
+ export const ObjectLinkNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
145
+ type: z.literal(NODE_TYPE.OBJECT_LINK),
146
+ href: z.union([z.string(), LinkMappingSchema]),
147
+ style: StyleValueSchema.optional(),
148
+ attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
149
+ children: z.union([
150
+ z.array(z.union([ComponentNodeSchema, z.string()])),
151
+ z.string(),
152
+ ]).optional(),
153
+ }).passthrough());
154
+
155
+ /**
156
+ * Locale list node schema - renders locale switcher links
157
+ */
158
+ export const LocaleListNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
159
+ type: z.literal(NODE_TYPE.LOCALE_LIST),
160
+ style: StyleValueSchema.optional(),
161
+ itemStyle: StyleValueSchema.optional(),
162
+ activeItemStyle: StyleValueSchema.optional(),
163
+ separatorStyle: StyleValueSchema.optional(),
164
+ showCurrent: z.boolean().optional(),
165
+ showSeparator: z.boolean().optional(),
166
+ showFlag: z.boolean().optional(),
167
+ flagStyle: StyleValueSchema.optional(),
168
+ attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
169
+ }).passthrough());
170
+
171
+ /**
172
+ * Text node schema - static text in component structures
173
+ */
174
+ export const TextNodeSchema = z.object({
175
+ type: z.literal(NODE_TYPE.TEXT),
176
+ value: z.string(),
177
+ }).passthrough();
178
+
179
+ /**
180
+ * Component node schema - discriminated union (now defined after dependencies)
181
+ */
182
+ ComponentNodeSchema = z.union([
183
+ HtmlNodeSchema,
184
+ ComponentInstanceNodeSchema,
185
+ SlotMarkerSchema,
186
+ EmbedNodeSchema,
187
+ ObjectLinkNodeSchema,
188
+ LocaleListNodeSchema,
189
+ TextNodeSchema,
190
+ ]);
191
+
192
+ // Export ComponentNodeSchema
193
+ export { ComponentNodeSchema };
194
+
195
+ /**
196
+ * Structured component definition schema
197
+ */
198
+ export const StructuredComponentDefinitionSchema = z.object({
199
+ interface: z.record(z.string(), PropDefinitionSchema).optional(),
200
+ structure: ComponentNodeSchema.optional(),
201
+ javascript: z.string().optional(),
202
+ css: z.string().optional(),
203
+ category: z.string().optional(),
204
+ }).passthrough();
205
+
206
+ /**
207
+ * Component definition schema
208
+ * Accepts both legacy format (with type/props/children) and new format (just component)
209
+ * New format checked first since it's more common
210
+ */
211
+ export const ComponentDefinitionSchema = z.union([
212
+ // New format: { component: {...} }
213
+ z.object({
214
+ component: StructuredComponentDefinitionSchema,
215
+ }).passthrough(),
216
+ // Legacy format: { type, props?, children?, component }
217
+ z.object({
218
+ type: z.string(),
219
+ props: z.record(z.string(), z.any()).optional(),
220
+ children: z.array(z.any()).optional(),
221
+ component: StructuredComponentDefinitionSchema,
222
+ }).passthrough(),
223
+ ]);
224
+
225
+ /**
226
+ * I18n value schema - accepts either a string or an i18n object
227
+ */
228
+ const I18nOrStringSchema = z.union([
229
+ z.string(),
230
+ z.object({
231
+ _i18n: z.literal(true),
232
+ }).passthrough(),
233
+ ]);
234
+
235
+ /**
236
+ * Page metadata schema (base version without CMS - used internally)
237
+ * Note: CMS fields added via PageMetaDataWithCMSSchema after CMS schemas are defined
238
+ */
239
+ const PageMetaDataBaseSchema = z.object({
240
+ title: I18nOrStringSchema.optional(),
241
+ description: I18nOrStringSchema.optional(),
242
+ keywords: I18nOrStringSchema.optional(),
243
+ ogTitle: I18nOrStringSchema.optional(),
244
+ ogDescription: I18nOrStringSchema.optional(),
245
+ ogImage: z.string().optional(),
246
+ ogType: z.string().optional(),
247
+ slugs: z.record(z.string(), z.string()).optional(),
248
+ }).passthrough();
249
+
250
+ // Temporary export for backward compatibility - will be replaced with full schema below
251
+ export const PageMetaDataSchema = PageMetaDataBaseSchema;
252
+
253
+ /**
254
+ * Line range schema for element line number tracking
255
+ */
256
+ export const LineRangeSchema = z.object({
257
+ startLine: z.number(),
258
+ endLine: z.number(),
259
+ });
260
+
261
+ /**
262
+ * JSON page schema
263
+ * Requires at least one field to prevent empty objects from matching
264
+ */
265
+ export const JSONPageSchema = z.object({
266
+ components: z.record(z.string(), ComponentDefinitionSchema).optional(),
267
+ root: ComponentNodeSchema.optional(),
268
+ meta: PageMetaDataSchema.optional(),
269
+ _lineMap: z.record(z.string(), LineRangeSchema).optional(),
270
+ }).passthrough().refine((data) => {
271
+ // Require at least one field to be present
272
+ return data.meta !== undefined || data.components !== undefined || data.root !== undefined;
273
+ }, {
274
+ message: "JSONPage must have at least one of: meta, components, or root"
275
+ });
276
+
277
+ /**
278
+ * Page data with component schema
279
+ */
280
+ export const PageDataWithComponentSchema = z.object({
281
+ component: z.object({
282
+ structure: ComponentNodeSchema.optional(),
283
+ interface: z.record(z.string(), PropDefinitionSchema).optional(),
284
+ javascript: z.string().optional(),
285
+ css: z.string().optional(),
286
+ }),
287
+ }).passthrough();
288
+
289
+ /**
290
+ * Page data schema - union of JSONPage and PageDataWithComponent
291
+ * PageDataWithComponent checked first because it's more specific
292
+ */
293
+ export const PageDataSchema = z.union([
294
+ PageDataWithComponentSchema,
295
+ JSONPageSchema,
296
+ ]);
297
+
298
+ // ============================================================================
299
+ // CMS Schemas
300
+ // ============================================================================
301
+
302
+ /**
303
+ * CMS field type schema
304
+ */
305
+ export const CMSFieldTypeSchema = z.enum([
306
+ 'string', 'text', 'number', 'boolean', 'image', 'date', 'select', 'reference', 'i18n', 'i18n-text'
307
+ ]);
308
+
309
+ /**
310
+ * CMS field definition schema
311
+ */
312
+ export const CMSFieldDefinitionSchema = z.object({
313
+ type: CMSFieldTypeSchema,
314
+ required: z.boolean().optional(),
315
+ default: z.unknown().optional(),
316
+ label: z.string().optional(),
317
+ description: z.string().optional(),
318
+ options: z.array(z.string()).optional(),
319
+ collection: z.string().optional(),
320
+ }).passthrough();
321
+
322
+ /**
323
+ * CMS schema schema (defines collection structure)
324
+ */
325
+ export const CMSSchemaSchema = z.object({
326
+ id: z.string(),
327
+ name: z.string(),
328
+ slugField: z.string(),
329
+ urlPattern: z.string(),
330
+ fields: z.record(CMSFieldDefinitionSchema),
331
+ }).passthrough();
332
+
333
+ /**
334
+ * CMS item schema (content entry)
335
+ */
336
+ export const CMSItemSchema = z.object({
337
+ _id: z.string(),
338
+ _slug: z.string().optional(),
339
+ _createdAt: z.string().optional(),
340
+ _updatedAt: z.string().optional(),
341
+ }).passthrough();
342
+
343
+ /**
344
+ * CMS filter condition schema
345
+ */
346
+ export const CMSFilterConditionSchema = z.object({
347
+ field: z.string(),
348
+ operator: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'in']).optional(),
349
+ value: z.unknown(),
350
+ });
351
+
352
+ /**
353
+ * CMS sort config schema
354
+ */
355
+ export const CMSSortConfigSchema = z.object({
356
+ field: z.string(),
357
+ order: z.enum(['asc', 'desc']).optional(),
358
+ });
359
+
360
+ /**
361
+ * CMSList query schema
362
+ */
363
+ export const CMSListQuerySchema = z.object({
364
+ collection: z.string(),
365
+ filter: z.union([
366
+ CMSFilterConditionSchema,
367
+ z.array(CMSFilterConditionSchema),
368
+ z.record(z.unknown()),
369
+ ]).optional(),
370
+ sort: z.union([CMSSortConfigSchema, z.array(CMSSortConfigSchema)]).optional(),
371
+ limit: z.number().positive().optional(),
372
+ offset: z.number().nonnegative().optional(),
373
+ });
374
+
375
+ /**
376
+ * Page CMS config schema (embedded in page meta)
377
+ */
378
+ export const PageCmsConfigSchema = z.object({
379
+ id: z.string(),
380
+ name: z.string(),
381
+ slugField: z.string(),
382
+ urlPattern: z.string(),
383
+ fields: z.record(CMSFieldDefinitionSchema),
384
+ }).passthrough();
385
+
386
+ /**
387
+ * Page metadata schema with CMS support
388
+ * Extends base schema with source and cms fields
389
+ */
390
+ export const PageMetaDataWithCMSSchema = z.object({
391
+ title: z.union([z.string(), z.object({ _i18n: z.literal(true) }).passthrough()]).optional(),
392
+ description: z.union([z.string(), z.object({ _i18n: z.literal(true) }).passthrough()]).optional(),
393
+ keywords: z.union([z.string(), z.object({ _i18n: z.literal(true) }).passthrough()]).optional(),
394
+ ogTitle: z.union([z.string(), z.object({ _i18n: z.literal(true) }).passthrough()]).optional(),
395
+ ogDescription: z.union([z.string(), z.object({ _i18n: z.literal(true) }).passthrough()]).optional(),
396
+ ogImage: z.string().optional(),
397
+ ogType: z.string().optional(),
398
+ slugs: z.record(z.string(), z.string()).optional(),
399
+ source: z.enum(['static', 'cms']).optional(),
400
+ cms: PageCmsConfigSchema.optional(),
401
+ }).passthrough();
@@ -0,0 +1,109 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ validatePropDefinition,
4
+ validateComponentNode,
5
+ validateOrThrow,
6
+ type ValidationResult,
7
+ } from './validators';
8
+ import { NODE_TYPE } from '../constants';
9
+
10
+ describe('validators', () => {
11
+ describe('validatePropDefinition', () => {
12
+ test('validates valid prop definition', () => {
13
+ const validProp = {
14
+ name: 'title',
15
+ type: 'string',
16
+ defaultValue: 'Hello',
17
+ };
18
+ const result = validatePropDefinition(validProp);
19
+ expect(result.valid).toBe(true);
20
+ if (result.valid) {
21
+ expect(result.data.name).toBe('title');
22
+ }
23
+ });
24
+
25
+ test('returns errors for invalid prop definition', () => {
26
+ const invalidProp = {
27
+ name: 123, // Should be string
28
+ };
29
+ const result = validatePropDefinition(invalidProp);
30
+ expect(result.valid).toBe(false);
31
+ if (!result.valid) {
32
+ expect(result.errors.length).toBeGreaterThan(0);
33
+ }
34
+ });
35
+
36
+ test('handles missing required fields', () => {
37
+ const result = validatePropDefinition({});
38
+ expect(result.valid).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe('validateComponentNode', () => {
43
+ test('validates valid HTML node', () => {
44
+ const validNode = {
45
+ type: NODE_TYPE.NODE,
46
+ tag: 'div',
47
+ children: [],
48
+ };
49
+ const result = validateComponentNode(validNode);
50
+ expect(result.valid).toBe(true);
51
+ if (result.valid) {
52
+ expect(result.data.type).toBe(NODE_TYPE.NODE);
53
+ }
54
+ });
55
+
56
+ test('validates valid component node', () => {
57
+ const validNode = {
58
+ type: NODE_TYPE.COMPONENT,
59
+ component: 'Card',
60
+ props: {},
61
+ };
62
+ const result = validateComponentNode(validNode);
63
+ expect(result.valid).toBe(true);
64
+ if (result.valid) {
65
+ expect(result.data.type).toBe(NODE_TYPE.COMPONENT);
66
+ }
67
+ });
68
+
69
+ test('returns errors for invalid node', () => {
70
+ const invalidNode = {
71
+ type: 'invalid_type',
72
+ };
73
+ const result = validateComponentNode(invalidNode);
74
+ expect(result.valid).toBe(false);
75
+ });
76
+
77
+ test('handles null/undefined', () => {
78
+ expect(validateComponentNode(null).valid).toBe(false);
79
+ expect(validateComponentNode(undefined).valid).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe('validateOrThrow', () => {
84
+ test('returns data on valid input', () => {
85
+ const validator = (data: unknown): ValidationResult<string> => ({
86
+ valid: true,
87
+ data: data as string,
88
+ });
89
+ const result = validateOrThrow('test', validator);
90
+ expect(result).toBe('test');
91
+ });
92
+
93
+ test('throws on invalid input', () => {
94
+ const validator = (data: unknown): ValidationResult<string> => ({
95
+ valid: false,
96
+ errors: [{ path: 'root', message: 'Invalid' }],
97
+ });
98
+ expect(() => validateOrThrow('test', validator)).toThrow();
99
+ });
100
+
101
+ test('includes custom error message', () => {
102
+ const validator = (data: unknown): ValidationResult<string> => ({
103
+ valid: false,
104
+ errors: [{ path: 'field', message: 'Required' }],
105
+ });
106
+ expect(() => validateOrThrow('test', validator, 'Custom error')).toThrow('Custom error');
107
+ });
108
+ });
109
+ });