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,293 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { BaseComponentRegistry } from './ComponentRegistry';
|
|
3
|
+
import type { ComponentDefinition } from '../types';
|
|
4
|
+
|
|
5
|
+
// Create a concrete implementation for testing
|
|
6
|
+
class TestRegistry extends BaseComponentRegistry {}
|
|
7
|
+
|
|
8
|
+
describe('BaseComponentRegistry', () => {
|
|
9
|
+
let registry: TestRegistry;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
registry = new TestRegistry();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('register', () => {
|
|
16
|
+
test('registers a component', () => {
|
|
17
|
+
const component: ComponentDefinition = {
|
|
18
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
registry.register('Button', component);
|
|
22
|
+
|
|
23
|
+
expect(registry.has('Button')).toBe(true);
|
|
24
|
+
expect(registry.get('Button')).toEqual(component);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('overwrites existing component', () => {
|
|
28
|
+
const component1: ComponentDefinition = {
|
|
29
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
30
|
+
};
|
|
31
|
+
const component2: ComponentDefinition = {
|
|
32
|
+
component: { structure: { type: 'node' as const, tag: 'span' } }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
registry.register('Button', component1);
|
|
36
|
+
registry.register('Button', component2);
|
|
37
|
+
|
|
38
|
+
expect(registry.get('Button')).toEqual(component2);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('get', () => {
|
|
43
|
+
test('returns component by name', () => {
|
|
44
|
+
const component: ComponentDefinition = {
|
|
45
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
registry.register('Button', component);
|
|
49
|
+
|
|
50
|
+
expect(registry.get('Button')).toEqual(component);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('returns undefined for non-existent component', () => {
|
|
54
|
+
expect(registry.get('NonExistent')).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('has', () => {
|
|
59
|
+
test('returns true for registered component', () => {
|
|
60
|
+
const component: ComponentDefinition = {
|
|
61
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
registry.register('Button', component);
|
|
65
|
+
|
|
66
|
+
expect(registry.has('Button')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('returns false for non-existent component', () => {
|
|
70
|
+
expect(registry.has('NonExistent')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('clear', () => {
|
|
75
|
+
test('removes all components', () => {
|
|
76
|
+
const component1: ComponentDefinition = {
|
|
77
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
78
|
+
};
|
|
79
|
+
const component2: ComponentDefinition = {
|
|
80
|
+
component: { structure: { type: 'node' as const, tag: 'span' } }
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
registry.register('Button', component1);
|
|
84
|
+
registry.register('Card', component2);
|
|
85
|
+
|
|
86
|
+
registry.clear();
|
|
87
|
+
|
|
88
|
+
expect(registry.has('Button')).toBe(false);
|
|
89
|
+
expect(registry.has('Card')).toBe(false);
|
|
90
|
+
expect(registry.getNames()).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('merge', () => {
|
|
95
|
+
test('merges multiple components', () => {
|
|
96
|
+
const components: Record<string, ComponentDefinition> = {
|
|
97
|
+
'Button': { component: { structure: { type: 'node' as const, tag: 'button' } } },
|
|
98
|
+
'Card': { component: { structure: { type: 'node' as const, tag: 'div' } } }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
registry.merge(components);
|
|
102
|
+
|
|
103
|
+
expect(registry.has('Button')).toBe(true);
|
|
104
|
+
expect(registry.has('Card')).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('overwrites existing components', () => {
|
|
108
|
+
const component1: ComponentDefinition = {
|
|
109
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
110
|
+
};
|
|
111
|
+
const component2: ComponentDefinition = {
|
|
112
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
registry.register('Button', component1);
|
|
116
|
+
registry.merge({ 'Button': component2 });
|
|
117
|
+
|
|
118
|
+
expect(registry.get('Button')).toEqual(component2);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('getAll', () => {
|
|
123
|
+
test('returns all registered components', () => {
|
|
124
|
+
const component1: ComponentDefinition = {
|
|
125
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
126
|
+
};
|
|
127
|
+
const component2: ComponentDefinition = {
|
|
128
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
registry.register('Button', component1);
|
|
132
|
+
registry.register('Card', component2);
|
|
133
|
+
|
|
134
|
+
const all = registry.getAll();
|
|
135
|
+
|
|
136
|
+
expect(all['Button']).toEqual(component1);
|
|
137
|
+
expect(all['Card']).toEqual(component2);
|
|
138
|
+
expect(Object.keys(all).length).toBe(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('returns copy of registry', () => {
|
|
142
|
+
const component: ComponentDefinition = {
|
|
143
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
registry.register('Button', component);
|
|
147
|
+
|
|
148
|
+
const all = registry.getAll();
|
|
149
|
+
delete all['Button'];
|
|
150
|
+
|
|
151
|
+
expect(registry.has('Button')).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('returns empty object when no components', () => {
|
|
155
|
+
expect(registry.getAll()).toEqual({});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('getNames', () => {
|
|
160
|
+
test('returns list of component names', () => {
|
|
161
|
+
const component1: ComponentDefinition = {
|
|
162
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
163
|
+
};
|
|
164
|
+
const component2: ComponentDefinition = {
|
|
165
|
+
component: { structure: { type: 'node' as const, tag: 'div' } }
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
registry.register('Button', component1);
|
|
169
|
+
registry.register('Card', component2);
|
|
170
|
+
|
|
171
|
+
const names = registry.getNames();
|
|
172
|
+
|
|
173
|
+
expect(names).toContain('Button');
|
|
174
|
+
expect(names).toContain('Card');
|
|
175
|
+
expect(names.length).toBe(2);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('returns empty array when no components', () => {
|
|
179
|
+
expect(registry.getNames()).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('remove', () => {
|
|
184
|
+
test('removes component and returns true', () => {
|
|
185
|
+
const component: ComponentDefinition = {
|
|
186
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
registry.register('Button', component);
|
|
190
|
+
|
|
191
|
+
const result = registry.remove('Button');
|
|
192
|
+
|
|
193
|
+
expect(result).toBe(true);
|
|
194
|
+
expect(registry.has('Button')).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('returns false when component does not exist', () => {
|
|
198
|
+
const result = registry.remove('NonExistent');
|
|
199
|
+
|
|
200
|
+
expect(result).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('subscribe', () => {
|
|
205
|
+
test('notifies listeners on register', () => {
|
|
206
|
+
let called = false;
|
|
207
|
+
const listener = () => { called = true; };
|
|
208
|
+
|
|
209
|
+
registry.subscribe(listener);
|
|
210
|
+
|
|
211
|
+
const component: ComponentDefinition = {
|
|
212
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
213
|
+
};
|
|
214
|
+
registry.register('Button', component);
|
|
215
|
+
|
|
216
|
+
expect(called).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('notifies listeners on clear', () => {
|
|
220
|
+
let called = false;
|
|
221
|
+
const listener = () => { called = true; };
|
|
222
|
+
|
|
223
|
+
registry.subscribe(listener);
|
|
224
|
+
|
|
225
|
+
registry.clear();
|
|
226
|
+
|
|
227
|
+
expect(called).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('notifies listeners on merge', () => {
|
|
231
|
+
let called = false;
|
|
232
|
+
const listener = () => { called = true; };
|
|
233
|
+
|
|
234
|
+
registry.subscribe(listener);
|
|
235
|
+
|
|
236
|
+
registry.merge({});
|
|
237
|
+
|
|
238
|
+
expect(called).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('notifies listeners on remove', () => {
|
|
242
|
+
let called = false;
|
|
243
|
+
const component: ComponentDefinition = {
|
|
244
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
registry.register('Button', component);
|
|
248
|
+
|
|
249
|
+
const listener = () => { called = true; };
|
|
250
|
+
registry.subscribe(listener);
|
|
251
|
+
|
|
252
|
+
registry.remove('Button');
|
|
253
|
+
|
|
254
|
+
expect(called).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('returns unsubscribe function', () => {
|
|
258
|
+
let callCount = 0;
|
|
259
|
+
const listener = () => { callCount++; };
|
|
260
|
+
|
|
261
|
+
const unsubscribe = registry.subscribe(listener);
|
|
262
|
+
|
|
263
|
+
const component: ComponentDefinition = {
|
|
264
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
265
|
+
};
|
|
266
|
+
registry.register('Button', component);
|
|
267
|
+
|
|
268
|
+
unsubscribe();
|
|
269
|
+
|
|
270
|
+
registry.register('Card', component);
|
|
271
|
+
|
|
272
|
+
expect(callCount).toBe(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('handles multiple listeners', () => {
|
|
276
|
+
let count1 = 0;
|
|
277
|
+
let count2 = 0;
|
|
278
|
+
const listener1 = () => { count1++; };
|
|
279
|
+
const listener2 = () => { count2++; };
|
|
280
|
+
|
|
281
|
+
registry.subscribe(listener1);
|
|
282
|
+
registry.subscribe(listener2);
|
|
283
|
+
|
|
284
|
+
const component: ComponentDefinition = {
|
|
285
|
+
component: { structure: { type: 'node' as const, tag: 'button' } }
|
|
286
|
+
};
|
|
287
|
+
registry.register('Button', component);
|
|
288
|
+
|
|
289
|
+
expect(count1).toBe(1);
|
|
290
|
+
expect(count2).toBe(1);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Component Registry
|
|
3
|
+
* Abstract base class for component registries
|
|
4
|
+
* Provides common functionality for client and SSR registries
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ComponentDefinition } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base registry class with common functionality
|
|
11
|
+
*/
|
|
12
|
+
export abstract class BaseComponentRegistry {
|
|
13
|
+
protected registry: Record<string, ComponentDefinition> = {};
|
|
14
|
+
private listeners: Set<() => void> = new Set();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a component with the given name
|
|
18
|
+
*/
|
|
19
|
+
register(name: string, definition: ComponentDefinition): void {
|
|
20
|
+
this.registry[name] = definition;
|
|
21
|
+
this.notify();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get a component by name
|
|
26
|
+
*/
|
|
27
|
+
get(name: string): ComponentDefinition | undefined {
|
|
28
|
+
return this.registry[name];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a component exists
|
|
33
|
+
*/
|
|
34
|
+
has(name: string): boolean {
|
|
35
|
+
return name in this.registry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Clear all registered components
|
|
40
|
+
*/
|
|
41
|
+
clear(): void {
|
|
42
|
+
this.registry = {};
|
|
43
|
+
this.notify();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Merge components into the registry
|
|
48
|
+
* Existing components with the same name will be overwritten
|
|
49
|
+
*/
|
|
50
|
+
merge(components: Record<string, ComponentDefinition>): void {
|
|
51
|
+
Object.assign(this.registry, components);
|
|
52
|
+
this.notify();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get all registered components
|
|
57
|
+
*/
|
|
58
|
+
getAll(): Record<string, ComponentDefinition> {
|
|
59
|
+
return { ...this.registry };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get list of registered component names
|
|
64
|
+
*/
|
|
65
|
+
getNames(): string[] {
|
|
66
|
+
return Object.keys(this.registry);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Remove a component by name
|
|
71
|
+
*/
|
|
72
|
+
remove(name: string): boolean {
|
|
73
|
+
if (this.has(name)) {
|
|
74
|
+
delete this.registry[name];
|
|
75
|
+
this.notify();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe to registry changes. Returns an unsubscribe function.
|
|
83
|
+
*/
|
|
84
|
+
subscribe(listener: () => void): () => void {
|
|
85
|
+
this.listeners.add(listener);
|
|
86
|
+
return () => {
|
|
87
|
+
this.listeners.delete(listener);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
protected notify(): void {
|
|
92
|
+
for (const listener of this.listeners) {
|
|
93
|
+
try {
|
|
94
|
+
listener();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Type Definition
|
|
3
|
+
* TypeScript interfaces for the node type registry system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from 'react';
|
|
7
|
+
import type { ZodType } from 'zod';
|
|
8
|
+
import type { ComponentNode } from '../types/nodes';
|
|
9
|
+
import type { StyleValue } from '../types/styles';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tree icon types for node display
|
|
13
|
+
*/
|
|
14
|
+
export type TreeIcon = 'TEXT' | 'COMPONENT' | 'HTML_ELEMENT' | 'IMAGE' | 'OBJECT' | 'SLOT_MARKER' | 'ARRAY' | 'UNKNOWN' | 'FORM';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Node category for grouping in command palette
|
|
18
|
+
*/
|
|
19
|
+
export type NodeCategory = 'core' | 'layout' | 'content' | 'special';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Field types for editable fields in the props panel
|
|
23
|
+
*/
|
|
24
|
+
export type EditableFieldType = 'string' | 'url' | 'boolean' | 'select' | 'number' | 'text';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Definition for an editable field in the props panel
|
|
28
|
+
* Used to auto-generate editor UI for node types
|
|
29
|
+
*/
|
|
30
|
+
export interface EditableField {
|
|
31
|
+
/** Property name on the node object */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Display label in the UI */
|
|
34
|
+
label: string;
|
|
35
|
+
/** Input type to render */
|
|
36
|
+
type: EditableFieldType;
|
|
37
|
+
/** Show as required field */
|
|
38
|
+
required?: boolean;
|
|
39
|
+
/** Placeholder text for input */
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
/** Options for 'select' type */
|
|
42
|
+
options?: string[];
|
|
43
|
+
/** Group related fields together (e.g., "Dimensions", "Playback") */
|
|
44
|
+
group?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Tree display configuration
|
|
49
|
+
*/
|
|
50
|
+
export interface TreeDisplayConfig<TNode extends ComponentNode = ComponentNode> {
|
|
51
|
+
icon: TreeIcon;
|
|
52
|
+
getLabel: (node: TNode) => string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Node capabilities - what the node supports
|
|
57
|
+
*/
|
|
58
|
+
export interface NodeCapabilities {
|
|
59
|
+
canHaveChildren: boolean;
|
|
60
|
+
canHaveStyle: boolean;
|
|
61
|
+
canHaveAttributes: boolean;
|
|
62
|
+
canBeNested: boolean;
|
|
63
|
+
requiresProps: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Client-side render context
|
|
68
|
+
* Passed to client renderers with utilities and state
|
|
69
|
+
*/
|
|
70
|
+
export interface ClientRenderContext {
|
|
71
|
+
key: number;
|
|
72
|
+
elementPath: (string | number)[];
|
|
73
|
+
customProps?: Record<string, unknown>;
|
|
74
|
+
parentComponentName?: string | null;
|
|
75
|
+
viewportWidth: number;
|
|
76
|
+
componentContext?: string | null;
|
|
77
|
+
locale?: string;
|
|
78
|
+
cmsContext?: Record<string, unknown> | null;
|
|
79
|
+
cmsLocale?: string | null;
|
|
80
|
+
|
|
81
|
+
// Utilities provided by ComponentBuilder
|
|
82
|
+
buildChildren: (
|
|
83
|
+
children: Array<ComponentNode | string> | string | undefined,
|
|
84
|
+
elementPath: (string | number)[],
|
|
85
|
+
parentComponentName?: string | null,
|
|
86
|
+
componentContext?: string | null
|
|
87
|
+
) => ReactElement | ReactElement[] | string | number | null;
|
|
88
|
+
registerElement: (
|
|
89
|
+
path: (string | number)[],
|
|
90
|
+
element: HTMLElement | null,
|
|
91
|
+
parentComponentName?: string | null,
|
|
92
|
+
isComponentRoot?: boolean
|
|
93
|
+
) => void;
|
|
94
|
+
getComponentDefinition: (name: string) => unknown | undefined;
|
|
95
|
+
processI18n: (value: unknown) => string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* SSR-side render context
|
|
100
|
+
* Passed to SSR renderers with utilities and state
|
|
101
|
+
*/
|
|
102
|
+
export interface SSRRenderContext {
|
|
103
|
+
locale?: string;
|
|
104
|
+
slugMappings?: Array<{ slug: string; locale: string; pagePath: string }>;
|
|
105
|
+
pagePath?: string;
|
|
106
|
+
breakpoints?: { tablet: number; mobile: number };
|
|
107
|
+
viewportWidth?: number;
|
|
108
|
+
cmsContext?: Record<string, unknown> | null;
|
|
109
|
+
|
|
110
|
+
// Utilities provided by ssrRenderer
|
|
111
|
+
renderNode: (node: ComponentNode | string, context: SSRRenderContext) => string;
|
|
112
|
+
renderChildren: (children: Array<ComponentNode | string> | string | undefined, context: SSRRenderContext) => string;
|
|
113
|
+
escapeHtml: (str: string) => string;
|
|
114
|
+
buildAttributes: (attrs: Record<string, unknown>, exclude?: string[]) => string;
|
|
115
|
+
processI18n: (value: unknown) => string;
|
|
116
|
+
getComponentDefinition: (name: string) => unknown | undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Client-side renderer function type
|
|
121
|
+
*/
|
|
122
|
+
export type ClientNodeRenderer<TNode extends ComponentNode> = (
|
|
123
|
+
node: TNode,
|
|
124
|
+
context: ClientRenderContext
|
|
125
|
+
) => ReactElement | null;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* SSR renderer function type
|
|
129
|
+
*/
|
|
130
|
+
export type SSRNodeRenderer<TNode extends ComponentNode> = (
|
|
131
|
+
node: TNode,
|
|
132
|
+
context: SSRRenderContext
|
|
133
|
+
) => string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Props editor component props
|
|
137
|
+
*/
|
|
138
|
+
export interface PropsEditorProps<TNode extends ComponentNode = ComponentNode> {
|
|
139
|
+
node: TNode;
|
|
140
|
+
selectedPath: string | null;
|
|
141
|
+
onPropChange?: (path: string, propName: string, newValue: unknown) => void;
|
|
142
|
+
themeColors?: Record<string, string>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Node Type Definition
|
|
147
|
+
* Complete definition for a node type including all behavior
|
|
148
|
+
*/
|
|
149
|
+
export interface NodeTypeDefinition<TNode extends ComponentNode = ComponentNode> {
|
|
150
|
+
// Identity
|
|
151
|
+
type: string;
|
|
152
|
+
displayName: string;
|
|
153
|
+
category: NodeCategory;
|
|
154
|
+
|
|
155
|
+
// Validation
|
|
156
|
+
schema: ZodType<TNode>;
|
|
157
|
+
typeGuard: (node: unknown) => node is TNode;
|
|
158
|
+
|
|
159
|
+
// Rendering
|
|
160
|
+
clientRenderer: ClientNodeRenderer<TNode>;
|
|
161
|
+
ssrRenderer: SSRNodeRenderer<TNode>;
|
|
162
|
+
|
|
163
|
+
// Editor UI
|
|
164
|
+
treeDisplay: TreeDisplayConfig<TNode>;
|
|
165
|
+
propsEditor?: React.ComponentType<PropsEditorProps<TNode>>;
|
|
166
|
+
/** Editable fields for auto-generated props editor UI */
|
|
167
|
+
editableFields?: EditableField[];
|
|
168
|
+
|
|
169
|
+
// Creation
|
|
170
|
+
defaultFactory: () => TNode;
|
|
171
|
+
|
|
172
|
+
// Capabilities
|
|
173
|
+
capabilities: NodeCapabilities;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Partial node type definition for defineNodeType helper
|
|
178
|
+
* Makes some fields optional with sensible defaults
|
|
179
|
+
*/
|
|
180
|
+
export interface NodeTypeDefinitionInput<TNode extends ComponentNode = ComponentNode> {
|
|
181
|
+
type: string;
|
|
182
|
+
displayName: string;
|
|
183
|
+
category?: NodeCategory;
|
|
184
|
+
|
|
185
|
+
schema: ZodType<TNode>;
|
|
186
|
+
typeGuard: (node: unknown) => node is TNode;
|
|
187
|
+
|
|
188
|
+
clientRenderer: ClientNodeRenderer<TNode>;
|
|
189
|
+
ssrRenderer: SSRNodeRenderer<TNode>;
|
|
190
|
+
|
|
191
|
+
treeDisplay: TreeDisplayConfig<TNode>;
|
|
192
|
+
propsEditor?: React.ComponentType<PropsEditorProps<TNode>>;
|
|
193
|
+
editableFields?: EditableField[];
|
|
194
|
+
|
|
195
|
+
defaultFactory: () => TNode;
|
|
196
|
+
|
|
197
|
+
capabilities?: Partial<NodeCapabilities>;
|
|
198
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Type Manager
|
|
3
|
+
* Global singleton managing both client and SSR registries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ClientNodeTypeRegistry } from './ClientNodeTypeRegistry';
|
|
7
|
+
import { SSRNodeTypeRegistry } from './SSRNodeTypeRegistry';
|
|
8
|
+
import type { NodeTypeDefinition } from './NodeTypeDefinition';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages both client and SSR node type registries
|
|
12
|
+
* Ensures both registries stay in sync
|
|
13
|
+
*/
|
|
14
|
+
export class NodeTypeManager {
|
|
15
|
+
private clientRegistry: ClientNodeTypeRegistry;
|
|
16
|
+
private ssrRegistry: SSRNodeTypeRegistry;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.clientRegistry = new ClientNodeTypeRegistry();
|
|
20
|
+
this.ssrRegistry = new SSRNodeTypeRegistry();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the client-side registry
|
|
25
|
+
*/
|
|
26
|
+
getClient(): ClientNodeTypeRegistry {
|
|
27
|
+
return this.clientRegistry;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the SSR registry
|
|
32
|
+
*/
|
|
33
|
+
getSSR(): SSRNodeTypeRegistry {
|
|
34
|
+
return this.ssrRegistry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Register a node type in both registries
|
|
39
|
+
*/
|
|
40
|
+
register(definition: NodeTypeDefinition): void {
|
|
41
|
+
this.clientRegistry.register(definition);
|
|
42
|
+
this.ssrRegistry.register(definition);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register multiple node types in both registries
|
|
47
|
+
*/
|
|
48
|
+
registerAll(definitions: NodeTypeDefinition[]): void {
|
|
49
|
+
this.clientRegistry.registerAll(definitions);
|
|
50
|
+
this.ssrRegistry.registerAll(definitions);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a node type is registered
|
|
55
|
+
*/
|
|
56
|
+
has(type: string): boolean {
|
|
57
|
+
return this.clientRegistry.has(type);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get a node type definition
|
|
62
|
+
*/
|
|
63
|
+
get(type: string): NodeTypeDefinition | undefined {
|
|
64
|
+
return this.clientRegistry.get(type);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get all registered node types
|
|
69
|
+
*/
|
|
70
|
+
getAll(): NodeTypeDefinition[] {
|
|
71
|
+
return this.clientRegistry.getAll();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get list of registered type names
|
|
76
|
+
*/
|
|
77
|
+
getNames(): string[] {
|
|
78
|
+
return this.clientRegistry.getNames();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear both registries
|
|
83
|
+
*/
|
|
84
|
+
clear(): void {
|
|
85
|
+
this.clientRegistry.clear();
|
|
86
|
+
this.ssrRegistry.clear();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Global singleton node type manager
|
|
92
|
+
* Use this instance throughout the application
|
|
93
|
+
*/
|
|
94
|
+
export const globalNodeTypeManager = new NodeTypeManager();
|