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.
- package/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- 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
|
+
|