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,227 @@
1
+ /**
2
+ * File System CMS Provider
3
+ * Implements CMSProvider for loading CMS data from the file system
4
+ */
5
+
6
+ import { existsSync, readdirSync, mkdirSync } from 'fs';
7
+ import { join } from 'path';
8
+ import type { CMSProvider, CMSSchemaInfo } from '../../shared/interfaces/contentProvider';
9
+ import type { CMSSchema, CMSItem } from '../../shared/types';
10
+ import { validateCMSItem } from '../../shared/validation/validators';
11
+
12
+ /**
13
+ * Load JSON file content from disk
14
+ */
15
+ async function loadJSONFile(filePath: string): Promise<unknown | null> {
16
+ try {
17
+ const file = Bun.file(filePath);
18
+ if (await file.exists()) {
19
+ return await file.json();
20
+ }
21
+ return null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * FileSystemCMSProvider
29
+ * Loads CMS schemas from page files and CMS items from cms/<collection>/<slug>.json
30
+ */
31
+ export class FileSystemCMSProvider implements CMSProvider {
32
+ private schemaCache: Map<string, CMSSchemaInfo> | null = null;
33
+
34
+ constructor(
35
+ private pagesDir: string, // e.g., './pages' - schemas extracted from here
36
+ private cmsDir: string // e.g., './cms' - items stored here
37
+ ) {}
38
+
39
+ /**
40
+ * Load all CMS schemas from page files with source: 'cms'
41
+ */
42
+ async getAllSchemas(): Promise<Map<string, CMSSchemaInfo>> {
43
+ // Return cached if available
44
+ if (this.schemaCache) {
45
+ return this.schemaCache;
46
+ }
47
+
48
+ const schemas = new Map<string, CMSSchemaInfo>();
49
+
50
+ if (!existsSync(this.pagesDir)) {
51
+ return schemas;
52
+ }
53
+
54
+ const files = readdirSync(this.pagesDir);
55
+ for (const file of files) {
56
+ if (!file.endsWith('.json')) continue;
57
+
58
+ const filePath = join(this.pagesDir, file);
59
+ const content = await loadJSONFile(filePath);
60
+
61
+ if (
62
+ content &&
63
+ typeof content === 'object' &&
64
+ 'meta' in content &&
65
+ typeof (content as Record<string, unknown>).meta === 'object'
66
+ ) {
67
+ const meta = (content as Record<string, unknown>).meta as Record<string, unknown>;
68
+
69
+ if (meta.source === 'cms' && meta.cms) {
70
+ const schema = meta.cms as CMSSchema;
71
+ schemas.set(schema.id, {
72
+ schema,
73
+ pagePath: filePath,
74
+ });
75
+ }
76
+ }
77
+ }
78
+
79
+ this.schemaCache = schemas;
80
+ return schemas;
81
+ }
82
+
83
+ /**
84
+ * Load all items for a collection
85
+ */
86
+ async getItems(collection: string): Promise<CMSItem[]> {
87
+ const collectionDir = join(this.cmsDir, collection);
88
+
89
+ if (!existsSync(collectionDir)) {
90
+ return [];
91
+ }
92
+
93
+ const items: CMSItem[] = [];
94
+ const files = readdirSync(collectionDir);
95
+
96
+ for (const file of files) {
97
+ if (!file.endsWith('.json')) continue;
98
+
99
+ const filePath = join(collectionDir, file);
100
+ const content = await loadJSONFile(filePath);
101
+
102
+ if (content && typeof content === 'object') {
103
+ const slug = file.replace('.json', '');
104
+ const item = { ...content as CMSItem, _slug: slug };
105
+
106
+ const validation = validateCMSItem(item);
107
+ if (validation.valid) {
108
+ items.push(validation.data);
109
+ } else {
110
+ console.warn(`Invalid CMS item at ${filePath}:`, validation.errors);
111
+ }
112
+ }
113
+ }
114
+
115
+ return items;
116
+ }
117
+
118
+ /**
119
+ * Get single item by slug
120
+ */
121
+ async getItemBySlug(collection: string, slug: string): Promise<CMSItem | null> {
122
+ const filePath = join(this.cmsDir, collection, `${slug}.json`);
123
+ const content = await loadJSONFile(filePath);
124
+
125
+ if (!content || typeof content !== 'object') {
126
+ return null;
127
+ }
128
+
129
+ const item = { ...content as CMSItem, _slug: slug };
130
+ const validation = validateCMSItem(item);
131
+
132
+ if (validation.valid) {
133
+ return validation.data;
134
+ }
135
+
136
+ console.warn(`Invalid CMS item at ${filePath}:`, validation.errors);
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Get single item by ID
142
+ */
143
+ async getItemById(collection: string, id: string): Promise<CMSItem | null> {
144
+ const items = await this.getItems(collection);
145
+ return items.find(item => item._id === id) || null;
146
+ }
147
+
148
+ /**
149
+ * Save item to file system
150
+ */
151
+ async saveItem(collection: string, item: CMSItem): Promise<void> {
152
+ const { writeFile } = await import('fs/promises');
153
+
154
+ // Get schema to determine slug field
155
+ const schemas = await this.getAllSchemas();
156
+ const schemaInfo = schemas.get(collection);
157
+
158
+ if (!schemaInfo) {
159
+ throw new Error(`Unknown collection: ${collection}`);
160
+ }
161
+
162
+ const slugField = schemaInfo.schema.slugField;
163
+ const slug = String(item[slugField]);
164
+
165
+ if (!slug) {
166
+ throw new Error(`Missing slug field: ${slugField}`);
167
+ }
168
+
169
+ // Ensure collection directory exists
170
+ const collectionDir = join(this.cmsDir, collection);
171
+ if (!existsSync(collectionDir)) {
172
+ mkdirSync(collectionDir, { recursive: true });
173
+ }
174
+
175
+ // Remove internal fields before saving
176
+ const { _slug, ...itemData } = item;
177
+
178
+ const filePath = join(collectionDir, `${slug}.json`);
179
+ await writeFile(filePath, JSON.stringify(itemData, null, 2), 'utf-8');
180
+ }
181
+
182
+ /**
183
+ * Delete item by slug
184
+ */
185
+ async deleteItem(collection: string, slug: string): Promise<void> {
186
+ const { unlink } = await import('fs/promises');
187
+ const filePath = join(this.cmsDir, collection, `${slug}.json`);
188
+
189
+ if (existsSync(filePath)) {
190
+ await unlink(filePath);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Clear schema cache (useful when pages are modified)
196
+ */
197
+ clearSchemaCache(): void {
198
+ this.schemaCache = null;
199
+ }
200
+
201
+ /**
202
+ * Save a new collection schema by creating a page file
203
+ */
204
+ async saveSchema(collectionId: string, pageData: unknown): Promise<void> {
205
+ const { writeFile, mkdir } = await import('fs/promises');
206
+
207
+ // Create page file path (use collection ID as filename)
208
+ const pageFilePath = join(this.pagesDir, `${collectionId}.json`);
209
+
210
+ // Check if file already exists
211
+ if (existsSync(pageFilePath)) {
212
+ throw new Error(`Page file already exists: ${collectionId}.json`);
213
+ }
214
+
215
+ // Write the page file
216
+ await writeFile(pageFilePath, JSON.stringify(pageData, null, 2), 'utf-8');
217
+
218
+ // Create the CMS collection directory
219
+ const collectionDir = join(this.cmsDir, collectionId);
220
+ if (!existsSync(collectionDir)) {
221
+ await mkdir(collectionDir, { recursive: true });
222
+ }
223
+
224
+ // Clear the schema cache so the new collection is detected
225
+ this.clearSchemaCache();
226
+ }
227
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * File System Page Provider
3
+ * Implements PageProvider for loading pages from the file system
4
+ */
5
+
6
+ import { existsSync, readdirSync } from 'fs';
7
+ import { join } from 'path';
8
+ import type { PageProvider } from '../../shared/interfaces/contentProvider';
9
+ import { isJSONFile, stripExtension, mapPageNameToPath, mapPathToPageName } from '../../shared/utils/fileUtils';
10
+
11
+ /**
12
+ * Load JSON file content from disk
13
+ */
14
+ async function loadJSONFile(filePath: string): Promise<string | null> {
15
+ try {
16
+ const file = Bun.file(filePath);
17
+ if (await file.exists()) {
18
+ return await file.text();
19
+ }
20
+ return null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * FileSystemPageProvider
28
+ * Loads and saves page JSON files from/to the file system
29
+ */
30
+ export class FileSystemPageProvider implements PageProvider {
31
+ constructor(private pagesDir: string) {}
32
+
33
+ async loadAll(): Promise<Map<string, string>> {
34
+ const pages = new Map<string, string>();
35
+
36
+ if (!existsSync(this.pagesDir)) {
37
+ return pages;
38
+ }
39
+
40
+ const files = readdirSync(this.pagesDir);
41
+ for (const file of files) {
42
+ if (isJSONFile(file)) {
43
+ const pageName = stripExtension(file);
44
+ const content = await loadJSONFile(join(this.pagesDir, file));
45
+ if (content) {
46
+ const pagePath = mapPageNameToPath(pageName);
47
+ pages.set(pagePath, content);
48
+ }
49
+ }
50
+ }
51
+
52
+ return pages;
53
+ }
54
+
55
+ async get(path: string): Promise<string | null> {
56
+ const pageName = mapPathToPageName(path);
57
+ const filePath = join(this.pagesDir, `${pageName}.json`);
58
+ return loadJSONFile(filePath);
59
+ }
60
+
61
+ async save(path: string, content: string): Promise<void> {
62
+ const { writeFile } = await import('fs/promises');
63
+ const pageName = mapPathToPageName(path);
64
+ const filePath = join(this.pagesDir, `${pageName}.json`);
65
+ await writeFile(filePath, content, 'utf-8');
66
+ }
67
+
68
+ async delete(path: string): Promise<void> {
69
+ const { unlink } = await import('fs/promises');
70
+ const pageName = mapPathToPageName(path);
71
+ const filePath = join(this.pagesDir, `${pageName}.json`);
72
+
73
+ if (existsSync(filePath)) {
74
+ await unlink(filePath);
75
+ }
76
+ }
77
+
78
+ async exists(path: string): Promise<boolean> {
79
+ const pageName = mapPathToPageName(path);
80
+ const filePath = join(this.pagesDir, `${pageName}.json`);
81
+ return existsSync(filePath);
82
+ }
83
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * CMS API Routes Tests
3
+ * Tests for CMS CRUD endpoints
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach } from 'bun:test';
7
+ import type { CMSProvider, CMSSchemaInfo } from '../../../shared/interfaces/contentProvider';
8
+ import type { CMSItem, CMSSchema } from '../../../shared/types';
9
+ import { CMSService } from '../../services/cmsService';
10
+ import {
11
+ handleCollectionsRoute,
12
+ handleCollectionSchemaRoute,
13
+ handleItemsRoute,
14
+ handleItemRoute,
15
+ } from './cms';
16
+
17
+ // Note: Write handler tests (handleCreateItemRoute, handleUpdateItemRoute, handleDeleteItemRoute)
18
+ // moved to @meno/studio
19
+
20
+ // Mock schema info
21
+ const mockBlogSchema: CMSSchema = {
22
+ id: 'blog-posts',
23
+ name: 'Blog Posts',
24
+ slugField: 'slug',
25
+ urlPattern: '/blog/{{slug}}',
26
+ fields: {
27
+ title: { type: 'string', required: true, label: 'Title' },
28
+ slug: { type: 'string', required: true, label: 'Slug' },
29
+ content: { type: 'text', label: 'Content' },
30
+ featured: { type: 'boolean', label: 'Featured' },
31
+ },
32
+ };
33
+
34
+ const mockProductSchema: CMSSchema = {
35
+ id: 'products',
36
+ name: 'Products',
37
+ slugField: 'slug',
38
+ urlPattern: '/shop/{{slug}}',
39
+ fields: {
40
+ name: { type: 'string', required: true },
41
+ slug: { type: 'string', required: true },
42
+ price: { type: 'number' },
43
+ },
44
+ };
45
+
46
+ const mockSchemaInfo: CMSSchemaInfo = {
47
+ schema: mockBlogSchema,
48
+ pagePath: '/pages/blog-post.json',
49
+ };
50
+
51
+ const mockProductSchemaInfo: CMSSchemaInfo = {
52
+ schema: mockProductSchema,
53
+ pagePath: '/pages/product.json',
54
+ };
55
+
56
+ const mockBlogItems: CMSItem[] = [
57
+ { _id: '1', _slug: 'hello-world', slug: 'hello-world', title: 'Hello World', content: 'First post', featured: true },
58
+ { _id: '2', _slug: 'getting-started', slug: 'getting-started', title: 'Getting Started', content: 'Guide', featured: false },
59
+ ];
60
+
61
+ describe('CMS API Routes - Read Operations', () => {
62
+ let cmsService: CMSService;
63
+ let mockProvider: CMSProvider;
64
+
65
+ beforeEach(async () => {
66
+ mockProvider = {
67
+ getAllSchemas: async () => new Map([
68
+ ['blog-posts', mockSchemaInfo],
69
+ ['products', mockProductSchemaInfo],
70
+ ]),
71
+ getItems: async (collection: string) => {
72
+ if (collection === 'blog-posts') return [...mockBlogItems];
73
+ return [];
74
+ },
75
+ getItemBySlug: async (collection: string, slug: string) => {
76
+ if (collection === 'blog-posts') {
77
+ return mockBlogItems.find(i => i._slug === slug) || null;
78
+ }
79
+ return null;
80
+ },
81
+ getItemById: async (collection: string, id: string) => {
82
+ if (collection === 'blog-posts') {
83
+ return mockBlogItems.find(i => i._id === id) || null;
84
+ }
85
+ return null;
86
+ },
87
+ saveItem: async () => {},
88
+ deleteItem: async () => {},
89
+ };
90
+
91
+ cmsService = new CMSService(mockProvider);
92
+ await cmsService.initialize();
93
+ });
94
+
95
+ describe('GET /api/cms/collections', () => {
96
+ it('should return all collections with schemas', async () => {
97
+ const response = handleCollectionsRoute(cmsService);
98
+ const data = await response.json();
99
+
100
+ expect(response.status).toBe(200);
101
+ expect(data.collections).toHaveLength(2);
102
+ expect(data.collections[0].id).toBe('blog-posts');
103
+ expect(data.collections[0].name).toBe('Blog Posts');
104
+ expect(data.collections[1].id).toBe('products');
105
+ });
106
+
107
+ it('should include field definitions in schema', async () => {
108
+ const response = handleCollectionsRoute(cmsService);
109
+ const data = await response.json();
110
+
111
+ const blogCollection = data.collections.find((c: any) => c.id === 'blog-posts');
112
+ expect(blogCollection.fields).toBeDefined();
113
+ expect(blogCollection.fields.title.type).toBe('string');
114
+ expect(blogCollection.fields.title.required).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe('GET /api/cms/collections/:collection', () => {
119
+ it('should return schema for specific collection', async () => {
120
+ const response = handleCollectionSchemaRoute('blog-posts', cmsService);
121
+ const data = await response.json();
122
+
123
+ expect(response.status).toBe(200);
124
+ expect(data.schema.id).toBe('blog-posts');
125
+ expect(data.schema.name).toBe('Blog Posts');
126
+ expect(data.schema.slugField).toBe('slug');
127
+ });
128
+
129
+ it('should return 404 for unknown collection', async () => {
130
+ const response = handleCollectionSchemaRoute('unknown', cmsService);
131
+ const data = await response.json();
132
+
133
+ expect(response.status).toBe(404);
134
+ expect(data.error).toBe('Collection not found');
135
+ });
136
+ });
137
+
138
+ describe('GET /api/cms/:collection', () => {
139
+ it('should return all items in collection', async () => {
140
+ const response = await handleItemsRoute('blog-posts', cmsService);
141
+ const data = await response.json();
142
+
143
+ expect(response.status).toBe(200);
144
+ expect(data.items).toHaveLength(2);
145
+ expect(data.items[0].title).toBe('Hello World');
146
+ });
147
+
148
+ it('should return empty array for empty collection', async () => {
149
+ const response = await handleItemsRoute('products', cmsService);
150
+ const data = await response.json();
151
+
152
+ expect(response.status).toBe(200);
153
+ expect(data.items).toEqual([]);
154
+ });
155
+ });
156
+
157
+ describe('GET /api/cms/:collection/:slug', () => {
158
+ it('should return specific item by slug', async () => {
159
+ const response = await handleItemRoute('blog-posts', 'hello-world', cmsService);
160
+ const data = await response.json();
161
+
162
+ expect(response.status).toBe(200);
163
+ expect(data.item.title).toBe('Hello World');
164
+ expect(data.item._slug).toBe('hello-world');
165
+ });
166
+
167
+ it('should return 404 for non-existent item', async () => {
168
+ const response = await handleItemRoute('blog-posts', 'non-existent', cmsService);
169
+ const data = await response.json();
170
+
171
+ expect(response.status).toBe(404);
172
+ expect(data.error).toBe('Item not found');
173
+ });
174
+ });
175
+
176
+ // Note: POST/PUT/DELETE tests moved to @meno/studio/lib/server/routes/api/cms.test.ts
177
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * CMS API Routes
3
+ * Handles CMS content CRUD endpoints
4
+ */
5
+
6
+ import type { CMSService } from '../../services/cmsService';
7
+ import type { CMSSchema } from '../../../shared/types';
8
+ import { jsonResponse, errorResponse } from './shared';
9
+
10
+ /**
11
+ * GET /api/cms/collections - List all collections with their schemas
12
+ */
13
+ export function handleCollectionsRoute(cmsService: CMSService): Response {
14
+ const schemas = cmsService.getAllSchemas();
15
+ const collections: CMSSchema[] = [];
16
+
17
+ for (const [, schemaInfo] of schemas) {
18
+ collections.push(schemaInfo.schema);
19
+ }
20
+
21
+ return jsonResponse({ collections });
22
+ }
23
+
24
+ /**
25
+ * GET /api/cms/collections/:collection - Get schema for specific collection
26
+ */
27
+ export function handleCollectionSchemaRoute(
28
+ collection: string,
29
+ cmsService: CMSService
30
+ ): Response {
31
+ const schema = cmsService.getSchema(collection);
32
+
33
+ if (!schema) {
34
+ return errorResponse('Collection not found', 404);
35
+ }
36
+
37
+ return jsonResponse({ schema });
38
+ }
39
+
40
+ // Note: handleCreateCollectionRoute moved to @meno/studio
41
+
42
+ /**
43
+ * GET /api/cms/:collection - List all items in a collection
44
+ */
45
+ export async function handleItemsRoute(
46
+ collection: string,
47
+ cmsService: CMSService
48
+ ): Promise<Response> {
49
+ const items = await cmsService.queryItems({ collection });
50
+ return jsonResponse({ items });
51
+ }
52
+
53
+ /**
54
+ * GET /api/cms/:collection/:slug - Get specific item by slug
55
+ */
56
+ export async function handleItemRoute(
57
+ collection: string,
58
+ slug: string,
59
+ cmsService: CMSService
60
+ ): Promise<Response> {
61
+ const match = await cmsService.matchRoute(
62
+ cmsService.getSchema(collection)?.urlPattern.replace('{{slug}}', slug) || ''
63
+ );
64
+
65
+ // If no match via route, try direct provider access
66
+ if (!match) {
67
+ // For direct item access, we need to check if item exists
68
+ const items = await cmsService.queryItems({ collection });
69
+ const item = items.find(i => i._slug === slug || i[cmsService.getSchema(collection)?.slugField || 'slug'] === slug);
70
+
71
+ if (!item) {
72
+ return errorResponse('Item not found', 404);
73
+ }
74
+
75
+ return jsonResponse({ item });
76
+ }
77
+
78
+ return jsonResponse({ item: match.item });
79
+ }
80
+
81
+ // Note: Write handlers (handleCreateItemRoute, handleUpdateItemRoute, handleDeleteItemRoute)
82
+ // moved to @meno/studio
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Colors API Routes
3
+ * Handles color variables endpoints with theme support
4
+ */
5
+
6
+ import { colorService } from '../../services/ColorService';
7
+ import { jsonResponse } from './shared';
8
+
9
+ /**
10
+ * Handle colors API endpoint - GET /api/colors?theme=<themeName>
11
+ */
12
+ export async function handleColorsRoute(url?: URL): Promise<Response> {
13
+ try {
14
+ const themeName = url?.searchParams.get('theme') || undefined;
15
+ const colors = await colorService.getThemeColors(themeName);
16
+ return jsonResponse(colors);
17
+ } catch (error) {
18
+ console.error('Error loading colors:', error);
19
+ return jsonResponse(
20
+ { error: 'Failed to load colors' },
21
+ { status: 500 }
22
+ );
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Handle themes API endpoint - GET /api/themes
28
+ */
29
+ export async function handleThemesRoute(): Promise<Response> {
30
+ try {
31
+ const themes = await colorService.getAvailableThemes();
32
+ const defaultTheme = await colorService.getDefaultTheme();
33
+ return jsonResponse({ themes, default: defaultTheme });
34
+ } catch (error) {
35
+ console.error('Error loading themes:', error);
36
+ return jsonResponse(
37
+ { error: 'Failed to load themes' },
38
+ { status: 500 }
39
+ );
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Handle full colors config API endpoint - GET /api/colors-config
45
+ */
46
+ export async function handleColorsConfigRoute(): Promise<Response> {
47
+ try {
48
+ const config = await colorService.getFullConfig();
49
+ return jsonResponse(config);
50
+ } catch (error) {
51
+ console.error('Error loading colors config:', error);
52
+ return jsonResponse(
53
+ { error: 'Failed to load colors config' },
54
+ { status: 500 }
55
+ );
56
+ }
57
+ }
58
+
59
+ // Note: handleSaveColorsRoute moved to @meno/studio
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Components API Routes
3
+ * Handles component CRUD endpoints
4
+ */
5
+
6
+ import type { ComponentService } from '../../services/componentService';
7
+ import { createCorsHeaders } from '../../middleware/cors';
8
+ import { jsonResponse } from './shared';
9
+
10
+ /**
11
+ * Handle components API endpoint - GET /api/components
12
+ */
13
+ export function handleComponentsRoute(componentService: ComponentService): Response {
14
+ const components = componentService.getAllComponents();
15
+ return jsonResponse(components);
16
+ }
17
+
18
+ /**
19
+ * Handle component data API endpoint - GET /api/component-data/:name
20
+ * Validates that component has structure before returning
21
+ */
22
+ export function handleComponentDataRoute(
23
+ url: URL,
24
+ componentService: ComponentService
25
+ ): Response {
26
+ const componentName = url.pathname.replace('/api/component-data/', '');
27
+ const componentDef = componentService.getComponent(componentName);
28
+
29
+ if (!componentDef) {
30
+ return jsonResponse({ error: 'Component not found' }, { status: 404 });
31
+ }
32
+
33
+ // Debug: Log what we're about to return
34
+
35
+ // Validate that component has structure
36
+ if (!componentService.validateComponentStructure(componentDef)) {
37
+ return jsonResponse(
38
+ {
39
+ error: 'Component structure is missing',
40
+ message: `Component "${componentName}" does not have a valid structure definition. Please add a structure field to the component definition.`
41
+ },
42
+ { status: 400 }
43
+ );
44
+ }
45
+
46
+ return jsonResponse(componentDef);
47
+ }
48
+
49
+ /**
50
+ * Handle component JavaScript API endpoint - GET /api/component-js/:name
51
+ */
52
+ export async function handleComponentJavaScriptRoute(
53
+ url: URL,
54
+ componentService: ComponentService
55
+ ): Promise<Response> {
56
+ const componentName = url.pathname.replace('/api/component-js/', '');
57
+ const jsContent = await componentService.getComponentJavaScript(componentName);
58
+ const corsHeaders = createCorsHeaders();
59
+
60
+ return new Response(jsContent || '', {
61
+ headers: {
62
+ 'Content-Type': 'text/javascript',
63
+ ...corsHeaders,
64
+ },
65
+ });
66
+ }
67
+
68
+ // Note: Write handlers (handleSaveComponentRoute, handleSaveComponentJavaScriptRoute,
69
+ // handleSaveComponentCSSRoute, handleComponentCategoryRoute) moved to @meno/studio
70
+