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,124 @@
1
+ /**
2
+ * Shared prop resolution utility
3
+ * Resolves component props by applying defaults from interface definitions
4
+ * Used by both client (React) and server (SSR) rendering
5
+ */
6
+
7
+ import type { StructuredComponentDefinition, ComponentNode, PropDefinition, I18nConfig } from './types';
8
+ import { validateComponentProps } from './validation/propValidator';
9
+ import { resolveI18nInProps } from './i18n';
10
+
11
+ export interface ResolvePropsOptions {
12
+ propDefs: Record<string, PropDefinition>;
13
+ passedProps: Record<string, unknown>;
14
+ children: Array<ComponentNode | string> | string | ComponentNode | null | undefined;
15
+ locale?: string;
16
+ i18nConfig?: I18nConfig;
17
+ }
18
+
19
+ /**
20
+ * Resolves component props by applying defaults and merging with passed props
21
+ * Now includes validation for type safety
22
+ *
23
+ * @param options - Object containing prop definitions, passed props, and children
24
+ * @returns Resolved props object with defaults applied and validated
25
+ */
26
+ export function resolveComponentProps(options: ResolvePropsOptions): Record<string, unknown> {
27
+ const { propDefs, passedProps, children = [], locale, i18nConfig } = options;
28
+
29
+ // Validate props against interface definition
30
+ // This provides runtime type checking while maintaining backward compatibility
31
+ try {
32
+ const validationResult = validateComponentProps(propDefs, passedProps);
33
+
34
+ if (!validationResult.valid && validationResult.errors.length > 0) {
35
+ // Log validation errors but continue (graceful degradation)
36
+ }
37
+
38
+ // Use validated props (includes coerced values and defaults)
39
+ const validatedProps = validationResult.props;
40
+
41
+ // Handle children prop separately (not validated by prop validator)
42
+ const resolvedProps: Record<string, unknown> = { ...validatedProps };
43
+
44
+ // Apply children handling
45
+ if ('children' in propDefs) {
46
+ const childrenPropDef = propDefs.children;
47
+ if (childrenPropDef && typeof childrenPropDef === 'object' && 'default' in childrenPropDef) {
48
+ resolvedProps.children = Array.isArray(children) && children.length > 0
49
+ ? children
50
+ : childrenPropDef.default;
51
+ } else {
52
+ resolvedProps.children = Array.isArray(children) && children.length > 0
53
+ ? children
54
+ : undefined;
55
+ }
56
+ } else if (Array.isArray(children) && children.length > 0) {
57
+ // Children passed but not in interface - allow it (backward compatibility)
58
+ resolvedProps.children = children;
59
+ }
60
+
61
+ // Resolve i18n translations if locale and config are provided
62
+ if (locale && i18nConfig) {
63
+ return resolveI18nInProps(resolvedProps, locale, i18nConfig);
64
+ }
65
+
66
+ return resolvedProps;
67
+ } catch (error) {
68
+ // Fallback to original behavior if validation fails (backward compatibility)
69
+ const resolvedProps: Record<string, unknown> = {};
70
+
71
+ // Apply defaults and passed props (original logic)
72
+ for (const [propName, propDef] of Object.entries(propDefs)) {
73
+ if (propDef && typeof propDef === 'object' && 'default' in propDef) {
74
+ if (propName === 'children') {
75
+ resolvedProps.children = Array.isArray(children) && children.length > 0
76
+ ? children
77
+ : propDef.default;
78
+ } else {
79
+ resolvedProps[propName] = passedProps[propName] !== undefined
80
+ ? passedProps[propName]
81
+ : propDef.default;
82
+ }
83
+ } else {
84
+ if (propName === 'children') {
85
+ resolvedProps.children = Array.isArray(children) && children.length > 0
86
+ ? children
87
+ : undefined;
88
+ } else {
89
+ resolvedProps[propName] = passedProps[propName];
90
+ }
91
+ }
92
+ }
93
+
94
+ return resolvedProps;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Resolves props from a structured component definition
100
+ *
101
+ * @param componentDef - Structured component definition
102
+ * @param passedProps - Props passed to the component instance
103
+ * @param children - Children passed to the component instance
104
+ * @param locale - Optional locale for i18n resolution
105
+ * @param i18nConfig - Optional i18n configuration
106
+ * @returns Resolved props object
107
+ */
108
+ export function resolvePropsFromDefinition(
109
+ componentDef: StructuredComponentDefinition,
110
+ passedProps: Record<string, unknown> = {},
111
+ children: Array<ComponentNode | string> | string | ComponentNode | null | undefined = [],
112
+ locale?: string,
113
+ i18nConfig?: I18nConfig
114
+ ): Record<string, unknown> {
115
+ const propDefs = componentDef.interface || {};
116
+ return resolveComponentProps({
117
+ propDefs,
118
+ passedProps,
119
+ children,
120
+ locale,
121
+ i18nConfig,
122
+ });
123
+ }
124
+
@@ -0,0 +1,190 @@
1
+ /**
2
+ * BaseNodeTypeRegistry Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'bun:test';
6
+ import { ClientNodeTypeRegistry } from './ClientNodeTypeRegistry';
7
+ import { SSRNodeTypeRegistry } from './SSRNodeTypeRegistry';
8
+ import { NodeTypeManager, globalNodeTypeManager } from './NodeTypeManager';
9
+ import { defineNodeType } from './defineNodeType';
10
+ import { registerBuiltInNodeTypes, builtInNodeTypes } from './nodeTypes';
11
+ import { NODE_TYPE } from '../constants';
12
+ import type { HtmlNode, SlotMarker } from '../types/nodes';
13
+
14
+ describe('BaseNodeTypeRegistry', () => {
15
+ let registry: ClientNodeTypeRegistry;
16
+
17
+ beforeEach(() => {
18
+ registry = new ClientNodeTypeRegistry();
19
+ });
20
+
21
+ describe('register', () => {
22
+ it('should register a node type definition', () => {
23
+ const mockDefinition = defineNodeType<HtmlNode>({
24
+ type: 'test-node',
25
+ displayName: 'Test Node',
26
+ category: 'core',
27
+ schema: {} as any,
28
+ typeGuard: (node): node is HtmlNode =>
29
+ node !== null && typeof node === 'object' && 'type' in node && node.type === 'test-node',
30
+ defaultFactory: () => ({ type: 'node', tag: 'div' }),
31
+ treeDisplay: { icon: 'HTML_ELEMENT', getLabel: () => 'Test' },
32
+ clientRenderer: () => null,
33
+ ssrRenderer: () => '',
34
+ });
35
+
36
+ registry.register(mockDefinition);
37
+ expect(registry.has('test-node')).toBe(true);
38
+ expect(registry.get('test-node')).toBe(mockDefinition);
39
+ });
40
+ });
41
+
42
+ describe('registerAll', () => {
43
+ it('should register multiple node type definitions', () => {
44
+ registry.registerAll(builtInNodeTypes);
45
+ expect(registry.size).toBe(7);
46
+ expect(registry.has(NODE_TYPE.NODE)).toBe(true);
47
+ expect(registry.has(NODE_TYPE.COMPONENT)).toBe(true);
48
+ expect(registry.has(NODE_TYPE.EMBED)).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe('findByNode', () => {
53
+ beforeEach(() => {
54
+ registry.registerAll(builtInNodeTypes);
55
+ });
56
+
57
+ it('should find node type for HtmlNode', () => {
58
+ const htmlNode: HtmlNode = { type: 'node', tag: 'div' };
59
+ const definition = registry.findByNode(htmlNode);
60
+ expect(definition).toBeDefined();
61
+ expect(definition?.type).toBe(NODE_TYPE.NODE);
62
+ });
63
+
64
+ it('should find node type for SlotMarker', () => {
65
+ const slotNode: SlotMarker = { type: 'slot' };
66
+ const definition = registry.findByNode(slotNode);
67
+ expect(definition).toBeDefined();
68
+ expect(definition?.type).toBe(NODE_TYPE.SLOT);
69
+ });
70
+
71
+ it('should return undefined for unknown nodes', () => {
72
+ const unknownNode = { type: 'unknown' } as any;
73
+ const definition = registry.findByNode(unknownNode);
74
+ expect(definition).toBeUndefined();
75
+ });
76
+ });
77
+
78
+ describe('isType', () => {
79
+ beforeEach(() => {
80
+ registry.registerAll(builtInNodeTypes);
81
+ });
82
+
83
+ it('should return true for matching types', () => {
84
+ const htmlNode = { type: 'node', tag: 'div' };
85
+ expect(registry.isType(htmlNode, NODE_TYPE.NODE)).toBe(true);
86
+ });
87
+
88
+ it('should return false for non-matching types', () => {
89
+ const htmlNode = { type: 'node', tag: 'div' };
90
+ expect(registry.isType(htmlNode, NODE_TYPE.COMPONENT)).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe('getNodeLabel', () => {
95
+ beforeEach(() => {
96
+ registry.registerAll(builtInNodeTypes);
97
+ });
98
+
99
+ it('should return label info for html node', () => {
100
+ const htmlNode: HtmlNode = { type: 'node', tag: 'header' };
101
+ const labelInfo = registry.getNodeLabel(htmlNode);
102
+ expect(labelInfo).toEqual({ label: 'header', icon: 'HTML_ELEMENT' });
103
+ });
104
+
105
+ it('should return label info for embed node', () => {
106
+ const embedNode = { type: 'embed', html: '<div>test</div>' };
107
+ const labelInfo = registry.getNodeLabel(embedNode as any);
108
+ expect(labelInfo).toEqual({ label: 'Embed', icon: 'HTML_ELEMENT' });
109
+ });
110
+ });
111
+
112
+ describe('createDefault', () => {
113
+ beforeEach(() => {
114
+ registry.registerAll(builtInNodeTypes);
115
+ });
116
+
117
+ it('should create default html node', () => {
118
+ const node = registry.createDefault(NODE_TYPE.NODE);
119
+ expect(node).toBeDefined();
120
+ expect(node?.type).toBe('node');
121
+ expect((node as HtmlNode).tag).toBe('div');
122
+ });
123
+
124
+ it('should create default embed node', () => {
125
+ const node = registry.createDefault(NODE_TYPE.EMBED);
126
+ expect(node).toBeDefined();
127
+ expect(node?.type).toBe('embed');
128
+ });
129
+
130
+ it('should return undefined for unknown type', () => {
131
+ const node = registry.createDefault('unknown');
132
+ expect(node).toBeUndefined();
133
+ });
134
+ });
135
+
136
+ describe('subscribe', () => {
137
+ it('should notify listeners on registration', () => {
138
+ let called = false;
139
+ registry.subscribe(() => { called = true; });
140
+ registry.registerAll(builtInNodeTypes);
141
+ expect(called).toBe(true);
142
+ });
143
+
144
+ it('should allow unsubscribing', () => {
145
+ let callCount = 0;
146
+ const unsubscribe = registry.subscribe(() => { callCount++; });
147
+ registry.registerAll(builtInNodeTypes);
148
+ expect(callCount).toBe(1);
149
+
150
+ unsubscribe();
151
+ registry.clear();
152
+ expect(callCount).toBe(1); // Should not increment after unsubscribe
153
+ });
154
+ });
155
+
156
+ describe('getByCategory', () => {
157
+ beforeEach(() => {
158
+ registry.registerAll(builtInNodeTypes);
159
+ });
160
+
161
+ it('should filter by category', () => {
162
+ const coreTypes = registry.getByCategory('core');
163
+ expect(coreTypes.length).toBeGreaterThan(0);
164
+ expect(coreTypes.every(t => t.category === 'core')).toBe(true);
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('NodeTypeManager', () => {
170
+ it('should register types in both client and SSR registries', () => {
171
+ const manager = new NodeTypeManager();
172
+ manager.registerAll(builtInNodeTypes);
173
+
174
+ expect(manager.getClient().has(NODE_TYPE.NODE)).toBe(true);
175
+ expect(manager.getSSR().has(NODE_TYPE.NODE)).toBe(true);
176
+ });
177
+ });
178
+
179
+ describe('globalNodeTypeManager', () => {
180
+ beforeEach(() => {
181
+ globalNodeTypeManager.clear();
182
+ });
183
+
184
+ it('should be a singleton', () => {
185
+ registerBuiltInNodeTypes();
186
+ expect(globalNodeTypeManager.has(NODE_TYPE.NODE)).toBe(true);
187
+ expect(globalNodeTypeManager.has(NODE_TYPE.EMBED)).toBe(true);
188
+ expect(globalNodeTypeManager.getAll().length).toBe(7);
189
+ });
190
+ });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Base Node Type Registry
3
+ * Abstract base class for node type registries
4
+ * Provides common functionality for client and SSR registries
5
+ */
6
+
7
+ import type { ComponentNode } from '../types/nodes';
8
+ import type { NodeTypeDefinition, NodeCategory, TreeIcon } from './NodeTypeDefinition';
9
+
10
+ /**
11
+ * Node label info returned by getNodeLabel
12
+ */
13
+ export interface NodeLabelInfo {
14
+ label: string;
15
+ icon: TreeIcon;
16
+ }
17
+
18
+ /**
19
+ * Base registry class with common functionality
20
+ */
21
+ export abstract class BaseNodeTypeRegistry {
22
+ protected registry: Map<string, NodeTypeDefinition> = new Map();
23
+ private listeners: Set<() => void> = new Set();
24
+ private nodeTypeCache: WeakMap<object, NodeTypeDefinition | null> = new WeakMap();
25
+
26
+ /**
27
+ * Register a node type definition
28
+ */
29
+ register(definition: NodeTypeDefinition): void {
30
+ if (!definition.type) {
31
+ throw new Error('Node type definition must have a type');
32
+ }
33
+ this.registry.set(definition.type, definition);
34
+ this.nodeTypeCache = new WeakMap(); // Clear cache on registration
35
+ this.notify();
36
+ }
37
+
38
+ /**
39
+ * Register multiple node type definitions at once
40
+ */
41
+ registerAll(definitions: NodeTypeDefinition[]): void {
42
+ for (const definition of definitions) {
43
+ if (!definition.type) {
44
+ throw new Error('Node type definition must have a type');
45
+ }
46
+ this.registry.set(definition.type, definition);
47
+ }
48
+ this.nodeTypeCache = new WeakMap(); // Clear cache on registration
49
+ this.notify();
50
+ }
51
+
52
+ /**
53
+ * Get a node type definition by type string
54
+ */
55
+ get(type: string): NodeTypeDefinition | undefined {
56
+ return this.registry.get(type);
57
+ }
58
+
59
+ /**
60
+ * Check if a node type is registered
61
+ */
62
+ has(type: string): boolean {
63
+ return this.registry.has(type);
64
+ }
65
+
66
+ /**
67
+ * Get all registered node types
68
+ */
69
+ getAll(): NodeTypeDefinition[] {
70
+ return Array.from(this.registry.values());
71
+ }
72
+
73
+ /**
74
+ * Get node types by category
75
+ */
76
+ getByCategory(category: NodeCategory): NodeTypeDefinition[] {
77
+ return this.getAll().filter((def) => def.category === category);
78
+ }
79
+
80
+ /**
81
+ * Get list of registered type names
82
+ */
83
+ getNames(): string[] {
84
+ return Array.from(this.registry.keys());
85
+ }
86
+
87
+ /**
88
+ * Find node type definition by node object using type guards
89
+ * Uses WeakMap caching for performance
90
+ */
91
+ findByNode(node: ComponentNode): NodeTypeDefinition | undefined {
92
+ if (!node || typeof node !== 'object') {
93
+ return undefined;
94
+ }
95
+
96
+ // Check cache first
97
+ const cached = this.nodeTypeCache.get(node);
98
+ if (cached !== undefined) {
99
+ return cached || undefined;
100
+ }
101
+
102
+ // Linear search through registered types
103
+ for (const definition of this.registry.values()) {
104
+ if (definition.typeGuard(node)) {
105
+ this.nodeTypeCache.set(node, definition);
106
+ return definition;
107
+ }
108
+ }
109
+
110
+ // Cache miss result too
111
+ this.nodeTypeCache.set(node, null);
112
+ return undefined;
113
+ }
114
+
115
+ /**
116
+ * Check if a node matches a specific type
117
+ * More efficient than findByNode when you know the type
118
+ */
119
+ isType<T extends ComponentNode>(node: unknown, type: string): node is T {
120
+ const definition = this.registry.get(type);
121
+ if (!definition) {
122
+ return false;
123
+ }
124
+ return definition.typeGuard(node);
125
+ }
126
+
127
+ /**
128
+ * Get node label info for tree display
129
+ */
130
+ getNodeLabel(node: ComponentNode): NodeLabelInfo | undefined {
131
+ const definition = this.findByNode(node);
132
+ if (!definition) {
133
+ return undefined;
134
+ }
135
+ return {
136
+ label: definition.treeDisplay.getLabel(node),
137
+ icon: definition.treeDisplay.icon,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Get default node for a type
143
+ */
144
+ createDefault(type: string): ComponentNode | undefined {
145
+ const definition = this.registry.get(type);
146
+ if (!definition) {
147
+ return undefined;
148
+ }
149
+ return definition.defaultFactory();
150
+ }
151
+
152
+ /**
153
+ * Remove a node type by type string
154
+ */
155
+ remove(type: string): boolean {
156
+ if (this.has(type)) {
157
+ this.registry.delete(type);
158
+ this.nodeTypeCache = new WeakMap(); // Clear cache
159
+ this.notify();
160
+ return true;
161
+ }
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * Clear all registered node types
167
+ */
168
+ clear(): void {
169
+ this.registry.clear();
170
+ this.nodeTypeCache = new WeakMap();
171
+ this.notify();
172
+ }
173
+
174
+ /**
175
+ * Subscribe to registry changes. Returns an unsubscribe function.
176
+ */
177
+ subscribe(listener: () => void): () => void {
178
+ this.listeners.add(listener);
179
+ return () => {
180
+ this.listeners.delete(listener);
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Get the number of registered node types
186
+ */
187
+ get size(): number {
188
+ return this.registry.size;
189
+ }
190
+
191
+ protected notify(): void {
192
+ for (const listener of this.listeners) {
193
+ try {
194
+ listener();
195
+ } catch {
196
+ // Silent fail - don't let one listener break others
197
+ }
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Client Node Type Registry
3
+ * Registry specialized for client-side (React) rendering
4
+ */
5
+
6
+ import type { ReactElement } from 'react';
7
+ import type { ComponentNode } from '../types/nodes';
8
+ import { BaseNodeTypeRegistry } from './BaseNodeTypeRegistry';
9
+ import type { ClientRenderContext } from './NodeTypeDefinition';
10
+
11
+ /**
12
+ * Client-side node type registry
13
+ * Extends base registry with client rendering capabilities
14
+ */
15
+ export class ClientNodeTypeRegistry extends BaseNodeTypeRegistry {
16
+ /**
17
+ * Render a node using the registered renderer
18
+ * Returns null if no renderer is found for the node type
19
+ */
20
+ renderNode(node: ComponentNode, context: ClientRenderContext): ReactElement | null {
21
+ const definition = this.findByNode(node);
22
+ if (!definition) {
23
+ return null;
24
+ }
25
+ return definition.clientRenderer(node as any, context);
26
+ }
27
+
28
+ /**
29
+ * Check if a node can be rendered by this registry
30
+ */
31
+ canRender(node: ComponentNode): boolean {
32
+ return this.findByNode(node) !== undefined;
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { ClientRegistry } from './ClientRegistry';
3
+
4
+ describe('ClientRegistry', () => {
5
+ test('can be instantiated', () => {
6
+ const registry = new ClientRegistry();
7
+ expect(registry).toBeDefined();
8
+ });
9
+
10
+ test('inherits from BaseComponentRegistry', () => {
11
+ const registry = new ClientRegistry();
12
+ const def = { component: { structure: { type: 'node', tag: 'div' } } };
13
+
14
+ registry.register('Test', def);
15
+ expect(registry.get('Test')).toEqual(def);
16
+ });
17
+
18
+ test('supports all base registry methods', () => {
19
+ const registry = new ClientRegistry();
20
+ expect(typeof registry.register).toBe('function');
21
+ expect(typeof registry.get).toBe('function');
22
+ expect(typeof registry.has).toBe('function');
23
+ expect(typeof registry.getAll).toBe('function');
24
+ expect(typeof registry.clear).toBe('function');
25
+ });
26
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Client Component Registry
3
+ * Extends base registry with client-specific functionality
4
+ */
5
+
6
+ import { BaseComponentRegistry } from './ComponentRegistry';
7
+ import type { ComponentDefinition } from '../types';
8
+
9
+ export class ClientRegistry extends BaseComponentRegistry {
10
+ /**
11
+ * Client-specific methods can be added here
12
+ * For now, it's the same as base registry
13
+ */
14
+ }
15
+