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,276 @@
1
+ import { test, expect, describe, beforeEach, mock } from 'bun:test';
2
+ import { ComponentService, type ComponentServiceFs, type ComponentLoader } from './componentService';
3
+ import { createMockComponentDefinition } from '../../test-utils';
4
+ import {
5
+ createTypedMockComponentServiceFs,
6
+ createTypedMockComponentLoader,
7
+ type TypedMockComponentServiceFs,
8
+ type TypedMockComponentLoader,
9
+ } from '../../test-utils/factories/ServerMockFactory';
10
+ import type { ComponentDefinition } from '../../shared/types';
11
+ import { NODE_TYPE } from '../../shared/constants';
12
+
13
+ describe('ComponentService', () => {
14
+ let componentService: ComponentService;
15
+ let mockFs: TypedMockComponentServiceFs;
16
+ let mockLoader: TypedMockComponentLoader;
17
+ let testComponents: Map<string, ComponentDefinition>;
18
+
19
+ beforeEach(() => {
20
+ testComponents = new Map([
21
+ ['TestComponent', createMockComponentDefinition('TestComponent')],
22
+ ['Button', createMockComponentDefinition('Button')],
23
+ ]);
24
+ mockFs = createTypedMockComponentServiceFs();
25
+ mockLoader = createTypedMockComponentLoader(testComponents);
26
+ componentService = new ComponentService({
27
+ fs: mockFs,
28
+ loader: mockLoader,
29
+ });
30
+ });
31
+
32
+ describe('loadAllComponents', () => {
33
+ test('should load all components from components directory', async () => {
34
+ await componentService.loadAllComponents();
35
+
36
+ expect(componentService.hasComponent('TestComponent')).toBe(true);
37
+ expect(componentService.hasComponent('Button')).toBe(true);
38
+ expect(mockLoader.loadDirectory).toHaveBeenCalledTimes(1);
39
+ });
40
+
41
+ test('should clear existing components before loading', async () => {
42
+ // Load first time
43
+ await componentService.loadAllComponents();
44
+ expect(componentService.hasComponent('TestComponent')).toBe(true);
45
+
46
+ // Clear and reload with empty
47
+ mockLoader.loadDirectory.mockReturnValueOnce(Promise.resolve(new Map()));
48
+ await componentService.loadAllComponents();
49
+
50
+ // Should be cleared
51
+ expect(componentService.hasComponent('TestComponent')).toBe(false);
52
+ });
53
+
54
+ test('should work without loader (uses default)', async () => {
55
+ // Create service without loader - will fail to load since no real fs
56
+ // Just test it doesn't crash when loader is not set
57
+ const serviceNoLoader = new ComponentService({ fs: mockFs });
58
+
59
+ // This will try to use real loadComponentDirectory which may fail
60
+ // But the point is it shouldn't use mockLoader
61
+ try {
62
+ await serviceNoLoader.loadAllComponents();
63
+ } catch {
64
+ // Expected to fail without real fs
65
+ }
66
+
67
+ expect(mockLoader.loadDirectory).not.toHaveBeenCalled();
68
+ });
69
+ });
70
+
71
+ describe('getComponent', () => {
72
+ test('should return component definition', async () => {
73
+ await componentService.loadAllComponents();
74
+
75
+ const component = componentService.getComponent('TestComponent');
76
+
77
+ expect(component).toBeDefined();
78
+ expect(component?.component).toBeDefined();
79
+ });
80
+
81
+ test('should return undefined for non-existent component', () => {
82
+ const component = componentService.getComponent('NonExistent');
83
+
84
+ expect(component).toBeUndefined();
85
+ });
86
+ });
87
+
88
+ describe('hasComponent', () => {
89
+ test('should return true for existing component', async () => {
90
+ await componentService.loadAllComponents();
91
+
92
+ expect(componentService.hasComponent('TestComponent')).toBe(true);
93
+ });
94
+
95
+ test('should return false for non-existent component', () => {
96
+ expect(componentService.hasComponent('NonExistent')).toBe(false);
97
+ });
98
+ });
99
+
100
+ describe('getAllComponents', () => {
101
+ test('should return all components as Record', async () => {
102
+ await componentService.loadAllComponents();
103
+
104
+ const allComponents = componentService.getAllComponents();
105
+
106
+ expect(allComponents).toBeDefined();
107
+ expect(allComponents['TestComponent']).toBeDefined();
108
+ expect(allComponents['Button']).toBeDefined();
109
+ });
110
+
111
+ test('should return empty object when no components loaded', () => {
112
+ const allComponents = componentService.getAllComponents();
113
+
114
+ expect(allComponents).toEqual({});
115
+ });
116
+ });
117
+
118
+ describe('validateComponentStructure', () => {
119
+ test('should return true for valid component structure', () => {
120
+ const validComponent = createMockComponentDefinition('Valid');
121
+
122
+ expect(componentService.validateComponentStructure(validComponent)).toBe(true);
123
+ });
124
+
125
+ test('should return false for undefined', () => {
126
+ expect(componentService.validateComponentStructure(undefined)).toBe(false);
127
+ });
128
+
129
+ test('should return false for missing component field', () => {
130
+ expect(componentService.validateComponentStructure({} as any)).toBe(false);
131
+ });
132
+
133
+ test('should return false for missing structure', () => {
134
+ expect(componentService.validateComponentStructure({
135
+ component: {}
136
+ } as any)).toBe(false);
137
+ });
138
+
139
+ test('should return false for invalid structure type', () => {
140
+ expect(componentService.validateComponentStructure({
141
+ component: { structure: 'invalid' }
142
+ } as any)).toBe(false);
143
+ });
144
+
145
+ test('should return false for structure without type', () => {
146
+ expect(componentService.validateComponentStructure({
147
+ component: { structure: { tag: 'div' } }
148
+ } as any)).toBe(false);
149
+ });
150
+
151
+ test('should return true for structure with valid type', () => {
152
+ expect(componentService.validateComponentStructure({
153
+ component: {
154
+ structure: {
155
+ type: NODE_TYPE.NODE,
156
+ tag: 'div',
157
+ children: []
158
+ }
159
+ }
160
+ } as any)).toBe(true);
161
+ });
162
+ });
163
+
164
+ describe('saveComponent', () => {
165
+ test('should save component to file system', async () => {
166
+ const componentData = createMockComponentDefinition('TestComponent');
167
+
168
+ await componentService.saveComponent('TestComponent', componentData);
169
+
170
+ expect(mockFs.writeFile).toHaveBeenCalled();
171
+ expect(componentService.hasComponent('TestComponent')).toBe(true);
172
+ });
173
+
174
+ test('should remove javascript field from saved data', async () => {
175
+ const componentData = createMockComponentDefinition('TestComponent');
176
+ if (componentData.component) {
177
+ componentData.component.javascript = 'console.log("test");';
178
+ }
179
+
180
+ await componentService.saveComponent('TestComponent', componentData);
181
+
182
+ const callArgs = (mockFs.writeFile as any).mock.calls[0];
183
+ const savedContent = callArgs[1];
184
+ const savedData = JSON.parse(savedContent);
185
+
186
+ expect(savedData.component.javascript).toBeUndefined();
187
+ });
188
+
189
+ test('should update in-memory cache after saving', async () => {
190
+ const componentData = createMockComponentDefinition('TestComponent');
191
+
192
+ await componentService.saveComponent('TestComponent', componentData);
193
+
194
+ const cached = componentService.getComponent('TestComponent');
195
+ expect(cached).toBeDefined();
196
+ });
197
+
198
+ test('should save with correct path format', async () => {
199
+ const componentData = createMockComponentDefinition('Button');
200
+
201
+ await componentService.saveComponent('Button', componentData);
202
+
203
+ const callArgs = (mockFs.writeFile as any).mock.calls[0];
204
+ const filePath = callArgs[0];
205
+
206
+ expect(filePath).toContain('Button.json');
207
+ });
208
+ });
209
+
210
+ describe('saveComponentJavaScript', () => {
211
+ test('should save JavaScript to .js file', async () => {
212
+ await componentService.saveComponentJavaScript('TestComponent', 'console.log("test");');
213
+
214
+ const callArgs = (mockFs.writeFile as any).mock.calls[0];
215
+ const [filePath, content] = callArgs;
216
+
217
+ expect(filePath).toContain('TestComponent.js');
218
+ expect(content).toBe('console.log("test");');
219
+ });
220
+
221
+ test('should handle empty JavaScript', async () => {
222
+ await componentService.saveComponentJavaScript('TestComponent', '');
223
+
224
+ const callArgs = (mockFs.writeFile as any).mock.calls[0];
225
+ const content = callArgs[1];
226
+
227
+ expect(content).toBe('');
228
+ });
229
+ });
230
+
231
+ describe('saveComponentCSS', () => {
232
+ test('should save CSS to .css file', async () => {
233
+ await componentService.saveComponentCSS('TestComponent', '.test { color: red; }');
234
+
235
+ const callArgs = (mockFs.writeFile as any).mock.calls[0];
236
+ const [filePath, content] = callArgs;
237
+
238
+ expect(filePath).toContain('TestComponent.css');
239
+ expect(content).toBe('.test { color: red; }');
240
+ });
241
+
242
+ test('should handle empty CSS', async () => {
243
+ await componentService.saveComponentCSS('TestComponent', '');
244
+
245
+ const callArgs = (mockFs.writeFile as any).mock.calls[0];
246
+ const content = callArgs[1];
247
+
248
+ expect(content).toBe('');
249
+ });
250
+ });
251
+
252
+ describe('constructor options', () => {
253
+ test('should work with no options', () => {
254
+ const service = new ComponentService();
255
+ expect(service).toBeDefined();
256
+ });
257
+
258
+ test('should work with fs only', () => {
259
+ const service = new ComponentService({ fs: mockFs });
260
+ expect(service).toBeDefined();
261
+ });
262
+
263
+ test('should work with loader only', () => {
264
+ const service = new ComponentService({ loader: mockLoader });
265
+ expect(service).toBeDefined();
266
+ });
267
+
268
+ test('should work with both fs and loader', () => {
269
+ const service = new ComponentService({
270
+ fs: mockFs,
271
+ loader: mockLoader,
272
+ });
273
+ expect(service).toBeDefined();
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Component Service
3
+ * Handles component loading and management
4
+ *
5
+ * Provides methods for loading components from the file system, caching them in memory,
6
+ * and managing component definitions including saving JavaScript and CSS files.
7
+ */
8
+
9
+ import { join } from 'path';
10
+ import { loadComponentDirectory, loadJSONFile, parseJSON } from '../jsonLoader';
11
+ import { projectPaths } from '../projectContext';
12
+ import type { ComponentDefinition } from '../../shared/types';
13
+
14
+ /**
15
+ * File system interface for dependency injection
16
+ * Allows mocking file operations in tests
17
+ */
18
+ export interface ComponentServiceFs {
19
+ writeFile(path: string, content: string, encoding: string): Promise<void>;
20
+ readFile?(path: string): Promise<string | null>;
21
+ }
22
+
23
+ /**
24
+ * Component loader interface for dependency injection
25
+ * Allows mocking component loading in tests
26
+ */
27
+ export interface ComponentLoader {
28
+ loadDirectory(dir: string): Promise<Map<string, ComponentDefinition>>;
29
+ loadFile(path: string): Promise<string | null>;
30
+ }
31
+
32
+ export class ComponentService {
33
+ private components = new Map<string, ComponentDefinition>();
34
+ private fs?: ComponentServiceFs;
35
+ private loader?: ComponentLoader;
36
+
37
+ /**
38
+ * Creates a new ComponentService instance
39
+ *
40
+ * @param options - Optional configuration for dependency injection
41
+ * @param options.fs - Optional file system interface for testing
42
+ * @param options.loader - Optional component loader interface for testing
43
+ */
44
+ constructor(options?: { fs?: ComponentServiceFs; loader?: ComponentLoader }) {
45
+ this.fs = options?.fs;
46
+ this.loader = options?.loader;
47
+ }
48
+
49
+ /**
50
+ * Load all components from the components directory
51
+ *
52
+ * Scans the ./components directory and loads all component definitions.
53
+ * Clears existing components before loading. Components are loaded from .json files,
54
+ * and associated .js and .css files are automatically loaded if they exist.
55
+ *
56
+ * @returns Promise that resolves when all components are loaded
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * await componentService.loadAllComponents();
61
+ * const count = Object.keys(componentService.getAllComponents()).length;
62
+ * ```
63
+ */
64
+ async loadAllComponents(): Promise<void> {
65
+ this.components.clear();
66
+ const loadedComponents = this.loader
67
+ ? await this.loader.loadDirectory(projectPaths.components())
68
+ : await loadComponentDirectory(projectPaths.components());
69
+ loadedComponents.forEach((value, key) => {
70
+ this.components.set(key, value);
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Get component by name
76
+ *
77
+ * @param name - Component name (without .json extension)
78
+ * @returns ComponentDefinition if found, undefined otherwise
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const button = componentService.getComponent('Button');
83
+ * if (button) {
84
+ * console.log(button.component.interface);
85
+ * }
86
+ * ```
87
+ */
88
+ getComponent(name: string): ComponentDefinition | undefined {
89
+ return this.components.get(name);
90
+ }
91
+
92
+ /**
93
+ * Check if component exists
94
+ *
95
+ * @param name - Component name to check
96
+ * @returns True if component exists in cache, false otherwise
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * if (componentService.hasComponent('Button')) {
101
+ * // Component is available
102
+ * }
103
+ * ```
104
+ */
105
+ hasComponent(name: string): boolean {
106
+ return this.components.has(name);
107
+ }
108
+
109
+ /**
110
+ * Get all components as a Record
111
+ *
112
+ * Returns all loaded components as a plain object for easy iteration
113
+ * or serialization.
114
+ *
115
+ * @returns Record mapping component names to ComponentDefinition objects
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const allComponents = componentService.getAllComponents();
120
+ * Object.keys(allComponents).forEach(name => {
121
+ * console.log(`Component: ${name}`);
122
+ * });
123
+ * ```
124
+ */
125
+ getAllComponents(): Record<string, ComponentDefinition> {
126
+ const record: Record<string, ComponentDefinition> = {};
127
+ this.components.forEach((value, key) => {
128
+ record[key] = value;
129
+ });
130
+ return record;
131
+ }
132
+
133
+ /**
134
+ * Validate that a component has a valid structure
135
+ *
136
+ * @param componentDef - ComponentDefinition to validate
137
+ * @returns true if component has valid structure, false otherwise
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const component = componentService.getComponent('Button');
142
+ * if (component && componentService.validateComponentStructure(component)) {
143
+ * // Component has valid structure
144
+ * }
145
+ * ```
146
+ */
147
+ validateComponentStructure(componentDef: ComponentDefinition | undefined): boolean {
148
+ if (!componentDef) {
149
+ return false;
150
+ }
151
+
152
+ // Handle ComponentDefinition in any format:
153
+ // 1. Legacy: { type: string, component: { structure: {...} } }
154
+ // 2. New: { component: { structure: {...} } }
155
+ // 3. Direct: { component: { structure: {...} } } (already normalized)
156
+
157
+ // Access component field safely (handle both TypeScript types and runtime data)
158
+ const component = (componentDef as any).component;
159
+
160
+ if (!component || typeof component !== 'object') {
161
+ return false;
162
+ }
163
+
164
+ // Check for structure in component
165
+ const structure = component.structure;
166
+ if (!structure) {
167
+ return false;
168
+ }
169
+
170
+ // Validate structure is a proper object with type property
171
+ if (typeof structure !== 'object' || structure === null || Array.isArray(structure)) {
172
+ return false;
173
+ }
174
+
175
+ // Structure must have a type property that's a string
176
+ if (!('type' in structure) || typeof structure.type !== 'string') {
177
+ return false;
178
+ }
179
+
180
+ return true;
181
+ }
182
+
183
+ /**
184
+ * Get component JavaScript from .js file
185
+ *
186
+ * Loads the JavaScript code for a component from its .js file.
187
+ * Returns null if the file doesn't exist.
188
+ *
189
+ * @param name - Component name (without .js extension)
190
+ * @returns JavaScript code as string, or null if file doesn't exist
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * const js = await componentService.getComponentJavaScript('Button');
195
+ * if (js) {
196
+ * console.log('Component has JavaScript');
197
+ * }
198
+ * ```
199
+ */
200
+ async getComponentJavaScript(name: string): Promise<string | null> {
201
+ const jsFilePath = join(projectPaths.components(), `${name}.js`);
202
+ try {
203
+ const file = Bun.file(jsFilePath);
204
+ if (await file.exists()) {
205
+ return await file.text();
206
+ }
207
+ return null;
208
+ } catch (error) {
209
+ return null;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Save component definition
215
+ *
216
+ * Saves component definition to a .json file and updates the cache.
217
+ * The javascript field is automatically removed from the saved data
218
+ * (JavaScript should only be in .js files).
219
+ *
220
+ * @param name - Component name (without .json extension)
221
+ * @param data - ComponentDefinition object to save
222
+ * @returns Promise that resolves when the component is saved
223
+ *
224
+ * @throws {Error} If file write fails
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * await componentService.saveComponent('Button', {
229
+ * component: {
230
+ * interface: { label: { type: 'string', default: 'Click me' } },
231
+ * structure: { type: 'node', tag: 'button', children: [] }
232
+ * }
233
+ * });
234
+ * ```
235
+ */
236
+ async saveComponent(name: string, data: ComponentDefinition): Promise<void> {
237
+ const writeFile = this.fs
238
+ ? this.fs.writeFile.bind(this.fs)
239
+ : (await import('fs/promises')).writeFile;
240
+
241
+ // Create a copy without the javascript field (JavaScript should only be in .js files)
242
+ const dataWithoutJS = JSON.parse(JSON.stringify(data));
243
+ if (dataWithoutJS?.component?.javascript !== undefined) {
244
+ delete dataWithoutJS.component.javascript;
245
+ }
246
+
247
+ const filePath = join(projectPaths.components(), `${name}.json`);
248
+ await writeFile(filePath, JSON.stringify(dataWithoutJS, null, 2), 'utf-8');
249
+
250
+ // Update in-memory cache
251
+ this.components.set(name, dataWithoutJS);
252
+ }
253
+
254
+ /**
255
+ * Save component JavaScript to .js file
256
+ *
257
+ * Saves JavaScript code to a component's .js file and reloads the component
258
+ * to update the in-memory cache with the new JavaScript.
259
+ *
260
+ * @param name - Component name (without .js extension)
261
+ * @param javascript - JavaScript code to save
262
+ * @returns Promise that resolves when the JavaScript is saved
263
+ *
264
+ * @throws {Error} If file write fails
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * await componentService.saveComponentJavaScript('Button', 'console.log("clicked");');
269
+ * ```
270
+ */
271
+ async saveComponentJavaScript(name: string, javascript: string): Promise<void> {
272
+ const writeFile = this.fs
273
+ ? this.fs.writeFile.bind(this.fs)
274
+ : (await import('fs/promises')).writeFile;
275
+ const componentsDir = projectPaths.components();
276
+ const jsFilePath = join(componentsDir, `${name}.js`);
277
+ await writeFile(jsFilePath, javascript || '', 'utf-8');
278
+
279
+ // Reload the component to update the registry with the new JS
280
+ const componentPath = join(componentsDir, `${name}.json`);
281
+ const componentData = await loadJSONFile(componentPath);
282
+ if (componentData) {
283
+ const parsed = parseJSON<ComponentDefinition>(componentData);
284
+ try {
285
+ const jsFile = Bun.file(jsFilePath);
286
+ if (await jsFile.exists()) {
287
+ const jsContent = await jsFile.text();
288
+ if (!parsed.component) {
289
+ parsed.component = {};
290
+ }
291
+ parsed.component.javascript = jsContent;
292
+ }
293
+ } catch (error) {
294
+ // Silently handle JS file read errors
295
+ }
296
+ this.components.set(name, parsed);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Save component CSS to .css file
302
+ *
303
+ * Saves CSS code to a component's .css file and reloads the component
304
+ * to update the in-memory cache with the new CSS.
305
+ *
306
+ * @param name - Component name (without .css extension)
307
+ * @param css - CSS code to save
308
+ * @returns Promise that resolves when the CSS is saved
309
+ *
310
+ * @throws {Error} If file write fails
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * await componentService.saveComponentCSS('Button', '.button { color: blue; }');
315
+ * ```
316
+ */
317
+ async saveComponentCSS(name: string, css: string): Promise<void> {
318
+ const writeFile = this.fs
319
+ ? this.fs.writeFile.bind(this.fs)
320
+ : (await import('fs/promises')).writeFile;
321
+ const componentsDir = projectPaths.components();
322
+ const cssFilePath = join(componentsDir, `${name}.css`);
323
+ await writeFile(cssFilePath, css || '', 'utf-8');
324
+
325
+ // Reload the component to update the registry with the new CSS
326
+ const componentPath = join(componentsDir, `${name}.json`);
327
+ const componentData = await loadJSONFile(componentPath);
328
+ if (componentData) {
329
+ const parsed = parseJSON<ComponentDefinition>(componentData);
330
+ try {
331
+ const cssFile = Bun.file(cssFilePath);
332
+ if (await cssFile.exists()) {
333
+ const cssContent = await cssFile.text();
334
+ if (!parsed.component) {
335
+ parsed.component = {};
336
+ }
337
+ parsed.component.css = cssContent;
338
+ }
339
+ } catch (error) {
340
+ // Silently handle CSS file read errors
341
+ }
342
+ this.components.set(name, parsed);
343
+ }
344
+ }
345
+ }
346
+