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,121 @@
1
+ /**
2
+ * Content Provider Interfaces
3
+ * Abstractions for data sources (file system, CMS, API, etc.)
4
+ */
5
+
6
+ import type { ComponentDefinition } from '../types/components';
7
+ import type { CMSSchema, CMSItem } from '../types/cms';
8
+
9
+ /**
10
+ * Page Provider Interface
11
+ * Abstracts page data loading from the underlying storage mechanism
12
+ */
13
+ export interface PageProvider {
14
+ /**
15
+ * Load all pages from the data source
16
+ * @returns Map of page path to raw JSON string content
17
+ */
18
+ loadAll(): Promise<Map<string, string>>;
19
+
20
+ /**
21
+ * Get a single page by path
22
+ * @param path - Page path (e.g., "/" or "/about")
23
+ * @returns Raw JSON string content, or null if not found
24
+ */
25
+ get(path: string): Promise<string | null>;
26
+
27
+ /**
28
+ * Save page content
29
+ * @param path - Page path
30
+ * @param content - Raw JSON string content
31
+ */
32
+ save(path: string, content: string): Promise<void>;
33
+
34
+ /**
35
+ * Delete a page
36
+ * @param path - Page path to delete
37
+ */
38
+ delete(path: string): Promise<void>;
39
+
40
+ /**
41
+ * Check if a page exists
42
+ * @param path - Page path
43
+ * @returns True if page exists
44
+ */
45
+ exists(path: string): Promise<boolean>;
46
+ }
47
+
48
+ /**
49
+ * Component Provider Interface
50
+ * Abstracts component definition loading
51
+ */
52
+ export interface ComponentProvider {
53
+ /**
54
+ * Load all component definitions
55
+ * @returns Map of component name to definition
56
+ */
57
+ loadAll(): Promise<Map<string, ComponentDefinition>>;
58
+
59
+ /**
60
+ * Get a single component by name
61
+ * @param name - Component name
62
+ * @returns Component definition or null if not found
63
+ */
64
+ get(name: string): Promise<ComponentDefinition | null>;
65
+
66
+ /**
67
+ * Get JavaScript code for a component
68
+ * @param name - Component name
69
+ * @returns JavaScript string or null if not found
70
+ */
71
+ getJS(name: string): Promise<string | null>;
72
+
73
+ /**
74
+ * Get CSS code for a component
75
+ * @param name - Component name
76
+ * @returns CSS string or null if not found
77
+ */
78
+ getCSS(name: string): Promise<string | null>;
79
+
80
+ /**
81
+ * Save component definition
82
+ * @param name - Component name
83
+ * @param definition - Component definition
84
+ */
85
+ save(name: string, definition: ComponentDefinition): Promise<void>;
86
+ }
87
+
88
+ // ============================================================================
89
+ // CMS Provider
90
+ // ============================================================================
91
+
92
+ /** Schema info extracted from a page file */
93
+ export interface CMSSchemaInfo {
94
+ schema: CMSSchema;
95
+ pagePath: string; // Path to the page file containing this schema
96
+ }
97
+
98
+ /**
99
+ * CMS Provider Interface
100
+ * Abstracts CMS data loading from the underlying storage mechanism
101
+ * (follows same pattern as PageProvider)
102
+ */
103
+ export interface CMSProvider {
104
+ /** Load all CMS schemas (extracted from page files with source: 'cms') */
105
+ getAllSchemas(): Promise<Map<string, CMSSchemaInfo>>;
106
+
107
+ /** Load all items for a collection */
108
+ getItems(collection: string): Promise<CMSItem[]>;
109
+
110
+ /** Get single item by slug */
111
+ getItemBySlug(collection: string, slug: string): Promise<CMSItem | null>;
112
+
113
+ /** Get single item by ID */
114
+ getItemById(collection: string, id: string): Promise<CMSItem | null>;
115
+
116
+ /** Save item (for editor) - saves to cms/<collection>/<slug>.json */
117
+ saveItem(collection: string, item: CMSItem): Promise<void>;
118
+
119
+ /** Delete item by slug */
120
+ deleteItem(collection: string, slug: string): Promise<void>;
121
+ }
@@ -0,0 +1,320 @@
1
+ import { describe, test, expect, beforeAll } from 'bun:test';
2
+ import {
3
+ isComponentNode,
4
+ isHtmlNode,
5
+ getComponentName,
6
+ getTagName,
7
+ getDisplayName,
8
+ extractNodeProperties,
9
+ isLikelyComponentTag,
10
+ isSlotMarker,
11
+ isEmbedNode,
12
+ isObjectLinkNode,
13
+ isLocaleListNode,
14
+ isValidNodeType,
15
+ canHaveChildren,
16
+ canHaveStyle,
17
+ isStructuredComponentDefinition,
18
+ hasComponentStructure,
19
+ hasComponentCss,
20
+ } from './nodeUtils';
21
+ import { NODE_TYPE } from './constants';
22
+ import type { ComponentNode, ComponentInstanceNode, HtmlNode, SlotMarker, EmbedNode, ObjectLinkNode, LocaleListNode } from './types';
23
+ import { registerBuiltInNodeTypes } from './registry';
24
+
25
+ describe('nodeUtils', () => {
26
+ beforeAll(() => {
27
+ registerBuiltInNodeTypes();
28
+ });
29
+ describe('isComponentNode', () => {
30
+ test('returns true for component instance nodes', () => {
31
+ const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
32
+ expect(isComponentNode(node)).toBe(true);
33
+ });
34
+
35
+ test('returns false for HTML nodes', () => {
36
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
37
+ expect(isComponentNode(node)).toBe(false);
38
+ });
39
+
40
+ test('returns false for null or undefined', () => {
41
+ expect(isComponentNode(null)).toBe(false);
42
+ expect(isComponentNode(undefined)).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe('isHtmlNode', () => {
47
+ test('returns true for HTML element nodes', () => {
48
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
49
+ expect(isHtmlNode(node)).toBe(true);
50
+ });
51
+
52
+ test('returns false for component nodes', () => {
53
+ const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
54
+ expect(isHtmlNode(node)).toBe(false);
55
+ });
56
+
57
+ test('returns false for null or undefined', () => {
58
+ expect(isHtmlNode(null)).toBe(false);
59
+ expect(isHtmlNode(undefined)).toBe(false);
60
+ });
61
+ });
62
+
63
+ describe('getComponentName', () => {
64
+ test('returns component name for component nodes', () => {
65
+ const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
66
+ expect(getComponentName(node)).toBe('Button');
67
+ });
68
+
69
+ test('returns undefined for HTML nodes', () => {
70
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
71
+ expect(getComponentName(node)).toBeUndefined();
72
+ });
73
+ });
74
+
75
+ describe('getTagName', () => {
76
+ test('returns tag name for HTML nodes', () => {
77
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
78
+ expect(getTagName(node)).toBe('div');
79
+ });
80
+
81
+ test('returns undefined for component nodes', () => {
82
+ const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
83
+ expect(getTagName(node)).toBeUndefined();
84
+ });
85
+ });
86
+
87
+ describe('getDisplayName', () => {
88
+ test('returns component name for component nodes', () => {
89
+ const node: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
90
+ expect(getDisplayName(node)).toBe('Button');
91
+ });
92
+
93
+ test('returns tag name for HTML nodes', () => {
94
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
95
+ expect(getDisplayName(node)).toBe('div');
96
+ });
97
+
98
+ test('returns "Embed" for embed nodes', () => {
99
+ const node: EmbedNode = { type: NODE_TYPE.EMBED, html: '<div>test</div>' };
100
+ expect(getDisplayName(node)).toBe('Embed');
101
+ });
102
+
103
+ test('returns "Link" for object link nodes', () => {
104
+ const node: ObjectLinkNode = { type: NODE_TYPE.OBJECT_LINK, object: 'page', id: '123' };
105
+ expect(getDisplayName(node)).toBe('Link');
106
+ });
107
+
108
+ test('returns "LocaleList" for locale list nodes', () => {
109
+ const node: LocaleListNode = { type: NODE_TYPE.LOCALE_LIST, locales: {} };
110
+ expect(getDisplayName(node)).toBe('LocaleList');
111
+ });
112
+
113
+ test('returns "Unknown" for unrecognized nodes', () => {
114
+ const node = { type: 'unknown' } as any;
115
+ expect(getDisplayName(node)).toBe('Unknown');
116
+ });
117
+ });
118
+
119
+ describe('extractNodeProperties', () => {
120
+ test('extracts component name and props from component node', () => {
121
+ const node: ComponentInstanceNode = {
122
+ type: NODE_TYPE.COMPONENT,
123
+ component: 'Button',
124
+ props: { text: 'Click me' }
125
+ };
126
+ expect(extractNodeProperties(node)).toEqual({
127
+ componentName: 'Button',
128
+ props: { text: 'Click me' }
129
+ });
130
+ });
131
+
132
+ test('extracts tag from HTML node', () => {
133
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
134
+ expect(extractNodeProperties(node)).toEqual({
135
+ tag: 'div',
136
+ props: {}
137
+ });
138
+ });
139
+
140
+ test('returns empty props for unknown node types', () => {
141
+ const node = { type: 'unknown' } as any;
142
+ expect(extractNodeProperties(node)).toEqual({ props: {} });
143
+ });
144
+ });
145
+
146
+ describe('isLikelyComponentTag', () => {
147
+ test('returns true for capitalized tags', () => {
148
+ expect(isLikelyComponentTag('Button')).toBe(true);
149
+ expect(isLikelyComponentTag('Card')).toBe(true);
150
+ expect(isLikelyComponentTag('MyComponent')).toBe(true);
151
+ });
152
+
153
+ test('returns false for lowercase tags', () => {
154
+ expect(isLikelyComponentTag('div')).toBe(false);
155
+ expect(isLikelyComponentTag('span')).toBe(false);
156
+ expect(isLikelyComponentTag('button')).toBe(false);
157
+ });
158
+
159
+ test('returns false for empty string', () => {
160
+ expect(isLikelyComponentTag('')).toBe(false);
161
+ });
162
+ });
163
+
164
+ describe('isSlotMarker', () => {
165
+ test('returns true for slot marker nodes', () => {
166
+ const node: SlotMarker = { type: NODE_TYPE.SLOT, name: 'default' };
167
+ expect(isSlotMarker(node)).toBe(true);
168
+ });
169
+
170
+ test('returns false for non-slot nodes', () => {
171
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
172
+ expect(isSlotMarker(node)).toBe(false);
173
+ });
174
+
175
+ test('returns false for null or undefined', () => {
176
+ expect(isSlotMarker(null)).toBe(false);
177
+ expect(isSlotMarker(undefined)).toBe(false);
178
+ });
179
+ });
180
+
181
+ describe('isEmbedNode', () => {
182
+ test('returns true for embed nodes', () => {
183
+ const node: EmbedNode = { type: NODE_TYPE.EMBED, url: 'https://example.com' };
184
+ expect(isEmbedNode(node)).toBe(true);
185
+ });
186
+
187
+ test('returns false for non-embed nodes', () => {
188
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
189
+ expect(isEmbedNode(node)).toBe(false);
190
+ });
191
+ });
192
+
193
+ describe('isObjectLinkNode', () => {
194
+ test('returns true for object link nodes', () => {
195
+ const node: ObjectLinkNode = { type: NODE_TYPE.OBJECT_LINK, object: 'page', id: '123' };
196
+ expect(isObjectLinkNode(node)).toBe(true);
197
+ });
198
+
199
+ test('returns false for non-object-link nodes', () => {
200
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
201
+ expect(isObjectLinkNode(node)).toBe(false);
202
+ });
203
+ });
204
+
205
+ describe('isLocaleListNode', () => {
206
+ test('returns true for locale list nodes', () => {
207
+ const node: LocaleListNode = { type: NODE_TYPE.LOCALE_LIST, locales: {} };
208
+ expect(isLocaleListNode(node)).toBe(true);
209
+ });
210
+
211
+ test('returns false for non-locale-list nodes', () => {
212
+ const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
213
+ expect(isLocaleListNode(node)).toBe(false);
214
+ });
215
+ });
216
+
217
+ describe('isValidNodeType', () => {
218
+ test('returns true for valid node types', () => {
219
+ expect(isValidNodeType(NODE_TYPE.NODE)).toBe(true);
220
+ expect(isValidNodeType(NODE_TYPE.COMPONENT)).toBe(true);
221
+ expect(isValidNodeType(NODE_TYPE.SLOT)).toBe(true);
222
+ expect(isValidNodeType(NODE_TYPE.EMBED)).toBe(true);
223
+ expect(isValidNodeType(NODE_TYPE.OBJECT_LINK)).toBe(true);
224
+ expect(isValidNodeType(NODE_TYPE.LOCALE_LIST)).toBe(true);
225
+ });
226
+
227
+ test('returns false for invalid node types', () => {
228
+ expect(isValidNodeType('invalid')).toBe(false);
229
+ expect(isValidNodeType('unknown')).toBe(false);
230
+ });
231
+ });
232
+
233
+ describe('canHaveChildren', () => {
234
+ test('returns true for nodes that can have children', () => {
235
+ const htmlNode: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
236
+ expect(canHaveChildren(htmlNode)).toBe(true);
237
+
238
+ const componentNode: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
239
+ expect(canHaveChildren(componentNode)).toBe(true);
240
+ });
241
+
242
+ test('returns false for slot markers', () => {
243
+ const slotNode: SlotMarker = { type: NODE_TYPE.SLOT, name: 'default' };
244
+ expect(canHaveChildren(slotNode)).toBe(false);
245
+ });
246
+
247
+ test('returns false for null or undefined', () => {
248
+ expect(canHaveChildren(null)).toBe(false);
249
+ expect(canHaveChildren(undefined)).toBe(false);
250
+ });
251
+ });
252
+
253
+ describe('canHaveStyle', () => {
254
+ test('returns true for nodes that can have style', () => {
255
+ const htmlNode: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
256
+ expect(canHaveStyle(htmlNode)).toBe(true);
257
+
258
+ const componentNode: ComponentInstanceNode = { type: NODE_TYPE.COMPONENT, component: 'Button', props: {} };
259
+ expect(canHaveStyle(componentNode)).toBe(true);
260
+ });
261
+
262
+ test('returns false for slot markers', () => {
263
+ const slotNode: SlotMarker = { type: NODE_TYPE.SLOT, name: 'default' };
264
+ expect(canHaveStyle(slotNode)).toBe(false);
265
+ });
266
+
267
+ test('returns false for null or undefined', () => {
268
+ expect(canHaveStyle(null)).toBe(false);
269
+ expect(canHaveStyle(undefined)).toBe(false);
270
+ });
271
+ });
272
+
273
+ describe('isStructuredComponentDefinition', () => {
274
+ test('returns true for structured component definitions', () => {
275
+ expect(isStructuredComponentDefinition({ interface: [] })).toBe(true);
276
+ expect(isStructuredComponentDefinition({ structure: {} })).toBe(true);
277
+ expect(isStructuredComponentDefinition({ javascript: 'code' })).toBe(true);
278
+ expect(isStructuredComponentDefinition({ css: 'styles' })).toBe(true);
279
+ expect(isStructuredComponentDefinition({ interface: [], structure: {} })).toBe(true);
280
+ });
281
+
282
+ test('returns false for invalid structures', () => {
283
+ expect(isStructuredComponentDefinition(null)).toBe(false);
284
+ expect(isStructuredComponentDefinition(undefined)).toBe(false);
285
+ expect(isStructuredComponentDefinition('string')).toBe(false);
286
+ expect(isStructuredComponentDefinition(123)).toBe(false);
287
+ expect(isStructuredComponentDefinition({})).toBe(false);
288
+ expect(isStructuredComponentDefinition({ structure: 'invalid' })).toBe(false);
289
+ });
290
+ });
291
+
292
+ describe('hasComponentStructure', () => {
293
+ test('returns true if component has structure', () => {
294
+ const comp = { component: { structure: {} } };
295
+ expect(hasComponentStructure(comp as any)).toBe(true);
296
+ });
297
+
298
+ test('returns false if component has no structure', () => {
299
+ const comp = { component: {} };
300
+ expect(hasComponentStructure(comp as any)).toBe(false);
301
+ });
302
+ });
303
+
304
+ describe('hasComponentCss', () => {
305
+ test('returns true if component has CSS string', () => {
306
+ const comp = { component: { css: 'body { color: red; }' } };
307
+ expect(hasComponentCss(comp as any)).toBe(true);
308
+ });
309
+
310
+ test('returns false if component has no CSS', () => {
311
+ const comp = { component: {} };
312
+ expect(hasComponentCss(comp as any)).toBe(false);
313
+ });
314
+
315
+ test('returns false if CSS is not a string', () => {
316
+ const comp = { component: { css: 123 } };
317
+ expect(hasComponentCss(comp as any)).toBe(false);
318
+ });
319
+ });
320
+ });
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Node type utilities and type guards
3
+ * Provides helper functions for working with ComponentNode types
4
+ */
5
+
6
+ import type { ComponentNode, ComponentInstanceNode, HtmlNode, SlotMarker, EmbedNode, ObjectLinkNode, LocaleListNode, ComponentDefinition, StructuredComponentDefinition } from './types';
7
+ import { NODE_TYPE } from './constants';
8
+
9
+ /**
10
+ * Type guard to check if a node is a component instance
11
+ */
12
+ export function isComponentNode(node: ComponentNode | null | undefined): node is ComponentInstanceNode {
13
+ return node?.type === NODE_TYPE.COMPONENT;
14
+ }
15
+
16
+ /**
17
+ * Type guard to check if a node is an HTML element
18
+ */
19
+ export function isHtmlNode(node: ComponentNode | null | undefined): node is HtmlNode {
20
+ return node?.type === NODE_TYPE.NODE;
21
+ }
22
+
23
+ /**
24
+ * Get the component name from a ComponentNode (for component instances)
25
+ */
26
+ export function getComponentName(node: ComponentNode): string | undefined {
27
+ return isComponentNode(node) ? node.component : undefined;
28
+ }
29
+
30
+ /**
31
+ * Get the tag name from a ComponentNode (for HTML nodes)
32
+ */
33
+ export function getTagName(node: ComponentNode): string | undefined {
34
+ return isHtmlNode(node) ? node.tag : undefined;
35
+ }
36
+
37
+ /**
38
+ * Get the display name (tag or component name) from a ComponentNode
39
+ * Uses registry for automatic display name resolution
40
+ */
41
+ export function getDisplayName(node: ComponentNode): string {
42
+ // Dynamic names for component/html nodes
43
+ if (isComponentNode(node)) {
44
+ return node.component;
45
+ }
46
+ if (isHtmlNode(node)) {
47
+ return node.tag;
48
+ }
49
+
50
+ // Use registry for all other node types
51
+ const { globalNodeTypeManager } = require('./registry');
52
+ const definition = globalNodeTypeManager.getClient().findByNode(node);
53
+ return definition?.displayName || 'Unknown';
54
+ }
55
+
56
+ /**
57
+ * Extract node properties (tag/component, props) based on node type
58
+ * Returns an object with tag, componentName, and props
59
+ */
60
+ export function extractNodeProperties(node: ComponentNode): {
61
+ tag?: string;
62
+ componentName?: string;
63
+ props: Record<string, unknown>;
64
+ } {
65
+ if (isComponentNode(node)) {
66
+ return {
67
+ componentName: node.component,
68
+ props: node.props || {}
69
+ };
70
+ }
71
+ if (isHtmlNode(node)) {
72
+ return {
73
+ tag: node.tag,
74
+ props: {}
75
+ };
76
+ }
77
+ return { props: {} };
78
+ }
79
+
80
+ /**
81
+ * Check if a tag name is likely a component (as opposed to HTML element)
82
+ * This is a heuristic based on naming conventions:
83
+ * - Components typically start with uppercase (e.g., "Card", "Button")
84
+ * - HTML elements are lowercase (e.g., "div", "span")
85
+ *
86
+ * Note: This is not foolproof, but works for most cases.
87
+ * For accurate detection, use component registry lookup.
88
+ */
89
+ export function isLikelyComponentTag(tag: string): boolean {
90
+ if (!tag) return false;
91
+ // Components typically start with uppercase letter
92
+ return tag.length > 0 && tag[0] === tag[0].toUpperCase() && tag[0] !== tag[0].toLowerCase();
93
+ }
94
+
95
+ /**
96
+ * Type guard to check if a node is a slot marker
97
+ */
98
+ export function isSlotMarker(node: ComponentNode | null | undefined): node is SlotMarker {
99
+ return node?.type === NODE_TYPE.SLOT;
100
+ }
101
+
102
+ /**
103
+ * Type guard to check if a node is an embed node
104
+ */
105
+ export function isEmbedNode(node: ComponentNode | null | undefined): node is EmbedNode {
106
+ return node?.type === NODE_TYPE.EMBED;
107
+ }
108
+
109
+ /**
110
+ * Type guard to check if a node is an object link node
111
+ */
112
+ export function isObjectLinkNode(node: ComponentNode | null | undefined): node is ObjectLinkNode {
113
+ return node?.type === NODE_TYPE.OBJECT_LINK;
114
+ }
115
+
116
+ /**
117
+ * Type guard to check if a node is a locale list node
118
+ */
119
+ export function isLocaleListNode(node: ComponentNode | null | undefined): node is LocaleListNode {
120
+ return node?.type === NODE_TYPE.LOCALE_LIST;
121
+ }
122
+
123
+ /**
124
+ * Validate that a node type is valid
125
+ */
126
+ export function isValidNodeType(type: string): type is typeof NODE_TYPE[keyof typeof NODE_TYPE] {
127
+ return type === NODE_TYPE.NODE || type === NODE_TYPE.COMPONENT || type === NODE_TYPE.SLOT ||
128
+ type === NODE_TYPE.EMBED || type === NODE_TYPE.OBJECT_LINK || type === NODE_TYPE.LOCALE_LIST;
129
+ }
130
+
131
+ /**
132
+ * Check if a node can have children property (excludes SlotMarker)
133
+ */
134
+ export function canHaveChildren(node: ComponentNode | null | undefined): node is Exclude<ComponentNode, SlotMarker> {
135
+ return node !== null && node !== undefined && !isSlotMarker(node);
136
+ }
137
+
138
+ /**
139
+ * Check if a node can have style property (excludes SlotMarker)
140
+ */
141
+ export function canHaveStyle(node: ComponentNode | null | undefined): node is Exclude<ComponentNode, SlotMarker> {
142
+ return node !== null && node !== undefined && !isSlotMarker(node);
143
+ }
144
+
145
+ /**
146
+ * Type guard to check if a value is a StructuredComponentDefinition
147
+ */
148
+ export function isStructuredComponentDefinition(comp: unknown): comp is StructuredComponentDefinition {
149
+ if (!comp || typeof comp !== 'object') {
150
+ return false;
151
+ }
152
+ // Check for required structure properties (interface, structure are optional but structure should be an object if present)
153
+ const obj = comp as Record<string, unknown>;
154
+ // Must have at least one of: interface, structure, javascript, css
155
+ return (
156
+ ('interface' in obj || 'structure' in obj || 'javascript' in obj || 'css' in obj) &&
157
+ (obj.structure === undefined || (typeof obj.structure === 'object' && obj.structure !== null))
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Check if a ComponentDefinition has a structure property
163
+ */
164
+ export function hasComponentStructure(comp: ComponentDefinition): boolean {
165
+ return comp.component?.structure !== undefined;
166
+ }
167
+
168
+ /**
169
+ * Check if a ComponentDefinition has a css property
170
+ */
171
+ export function hasComponentCss(comp: ComponentDefinition): boolean {
172
+ return comp.component?.css !== undefined && typeof comp.component.css === 'string';
173
+ }
174
+
175
+ /**
176
+ * Get the node type definition from the registry
177
+ * This is a convenience function that uses the global node type registry
178
+ * to find the type definition for a node.
179
+ *
180
+ * @param node - The node to get the type definition for
181
+ * @returns The node type definition, or undefined if not found
182
+ *
183
+ * @example
184
+ * const node = { type: 'image', src: 'test.jpg' };
185
+ * const definition = getNodeTypeDefinition(node);
186
+ * if (definition) {
187
+ * console.log(definition.displayName); // "Image"
188
+ * }
189
+ */
190
+ export function getNodeTypeDefinition(node: ComponentNode | null | undefined) {
191
+ if (!node) return undefined;
192
+ // Import dynamically to avoid circular dependencies
193
+ // The globalNodeTypeManager will be initialized by the app entry point
194
+ const { globalNodeTypeManager } = require('./registry');
195
+ return globalNodeTypeManager.getClient().findByNode(node);
196
+ }
197
+
198
+ /**
199
+ * Check if a node matches a specific type using the registry
200
+ * This is an alternative to the individual type guards that uses
201
+ * the centralized node type registry.
202
+ *
203
+ * @param node - The node to check
204
+ * @param nodeType - The node type constant (e.g., NODE_TYPE.EMBED)
205
+ * @returns true if the node matches the type
206
+ *
207
+ * @example
208
+ * import { NODE_TYPE } from './constants';
209
+ * const node = { type: 'embed', html: '<div>test</div>' };
210
+ * if (isNodeType(node, NODE_TYPE.EMBED)) {
211
+ * // node is an EmbedNode
212
+ * }
213
+ */
214
+ export function isNodeType(node: ComponentNode | null | undefined, nodeType: string): boolean {
215
+ if (!node) return false;
216
+ // Import dynamically to avoid circular dependencies
217
+ const { globalNodeTypeManager } = require('./registry');
218
+ return globalNodeTypeManager.getClient().isType(node, nodeType);
219
+ }
220
+