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,103 @@
1
+ /**
2
+ * Client-side Form Handler
3
+ * Handles form submissions via fetch for forms with data-submit-handler="fetch"
4
+ *
5
+ * This script is designed to be included inline in SSR output.
6
+ * It's a self-executing module that attaches to forms automatically.
7
+ */
8
+
9
+ export const formHandlerScript = `
10
+ (function() {
11
+ // Find all forms with fetch handler
12
+ const forms = document.querySelectorAll('form[data-submit-handler="fetch"]');
13
+
14
+ forms.forEach(function(form) {
15
+ form.addEventListener('submit', async function(e) {
16
+ e.preventDefault();
17
+
18
+ const action = form.getAttribute('action');
19
+ const method = form.getAttribute('method') || 'POST';
20
+ const successMessage = form.getAttribute('data-success-message') || 'Form submitted successfully!';
21
+ const errorMessage = form.getAttribute('data-error-message') || 'Something went wrong. Please try again.';
22
+
23
+ // Find or create message element
24
+ let messageEl = form.querySelector('[data-form-message]');
25
+ if (!messageEl) {
26
+ messageEl = document.createElement('div');
27
+ messageEl.setAttribute('data-form-message', 'true');
28
+ form.appendChild(messageEl);
29
+ }
30
+
31
+ // Find submit button and disable it
32
+ const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
33
+ const originalBtnText = submitBtn ? submitBtn.textContent || submitBtn.value : '';
34
+
35
+ if (submitBtn) {
36
+ submitBtn.disabled = true;
37
+ if (submitBtn.tagName === 'BUTTON') {
38
+ submitBtn.textContent = 'Sending...';
39
+ } else {
40
+ submitBtn.value = 'Sending...';
41
+ }
42
+ }
43
+
44
+ // Hide any previous message
45
+ messageEl.style.display = 'none';
46
+
47
+ try {
48
+ const formData = new FormData(form);
49
+
50
+ const response = await fetch(action, {
51
+ method: method.toUpperCase(),
52
+ body: formData,
53
+ });
54
+
55
+ const data = await response.json();
56
+
57
+ if (response.ok && data.success) {
58
+ // Success
59
+ messageEl.textContent = data.message || successMessage;
60
+ messageEl.style.display = 'block';
61
+ messageEl.style.backgroundColor = '#d4edda';
62
+ messageEl.style.color = '#155724';
63
+ messageEl.style.border = '1px solid #c3e6cb';
64
+
65
+ // Optionally reset form
66
+ form.reset();
67
+ } else {
68
+ // Error from server
69
+ messageEl.textContent = data.error || errorMessage;
70
+ messageEl.style.display = 'block';
71
+ messageEl.style.backgroundColor = '#f8d7da';
72
+ messageEl.style.color = '#721c24';
73
+ messageEl.style.border = '1px solid #f5c6cb';
74
+ }
75
+ } catch (error) {
76
+ // Network or other error
77
+ messageEl.textContent = errorMessage;
78
+ messageEl.style.display = 'block';
79
+ messageEl.style.backgroundColor = '#f8d7da';
80
+ messageEl.style.color = '#721c24';
81
+ messageEl.style.border = '1px solid #f5c6cb';
82
+ } finally {
83
+ // Re-enable submit button
84
+ if (submitBtn) {
85
+ submitBtn.disabled = false;
86
+ if (submitBtn.tagName === 'BUTTON') {
87
+ submitBtn.textContent = originalBtnText;
88
+ } else {
89
+ submitBtn.value = originalBtnText;
90
+ }
91
+ }
92
+ }
93
+ });
94
+ });
95
+ })();
96
+ `;
97
+
98
+ /**
99
+ * Check if page HTML contains forms that need the fetch handler
100
+ */
101
+ export function needsFormHandler(html: string): boolean {
102
+ return html.includes('data-submit-handler="fetch"');
103
+ }
@@ -0,0 +1,126 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { mergeStyles } from './styleProcessor';
3
+ import type { StyleObject } from '../shared/types';
4
+
5
+ describe('styleProcessor', () => {
6
+ describe('mergeStyles', () => {
7
+ test('merges multiple style objects', () => {
8
+ const style1: StyleObject = { color: 'red', fontSize: '16px' };
9
+ const style2: StyleObject = { padding: '10px' };
10
+ const result = mergeStyles(style1, style2);
11
+ expect(result).toEqual({
12
+ color: 'red',
13
+ fontSize: '16px',
14
+ padding: '10px'
15
+ });
16
+ });
17
+
18
+ test('later styles override earlier ones', () => {
19
+ const style1: StyleObject = { color: 'red', fontSize: '16px' };
20
+ const style2: StyleObject = { color: 'blue', padding: '10px' };
21
+ const result = mergeStyles(style1, style2);
22
+ expect(result).toEqual({
23
+ color: 'blue',
24
+ fontSize: '16px',
25
+ padding: '10px'
26
+ });
27
+ });
28
+
29
+ test('handles undefined styles', () => {
30
+ const style1: StyleObject = { color: 'red' };
31
+ const result = mergeStyles(style1, undefined, { fontSize: '16px' });
32
+ expect(result).toEqual({
33
+ color: 'red',
34
+ fontSize: '16px'
35
+ });
36
+ });
37
+
38
+ test('returns empty object when no styles provided', () => {
39
+ const result = mergeStyles();
40
+ expect(result).toEqual({});
41
+ });
42
+
43
+ test('handles all undefined styles', () => {
44
+ const result = mergeStyles(undefined, undefined, undefined);
45
+ expect(result).toEqual({});
46
+ });
47
+
48
+ test('merges complex style properties', () => {
49
+ const style1: StyleObject = {
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ padding: '20px'
53
+ };
54
+ const style2: StyleObject = {
55
+ backgroundColor: '#f0f0f0',
56
+ border: '1px solid #ccc'
57
+ };
58
+ const style3: StyleObject = {
59
+ padding: '30px',
60
+ margin: '10px'
61
+ };
62
+ const result = mergeStyles(style1, style2, style3);
63
+ expect(result).toEqual({
64
+ display: 'flex',
65
+ flexDirection: 'column',
66
+ padding: '30px',
67
+ backgroundColor: '#f0f0f0',
68
+ border: '1px solid #ccc',
69
+ margin: '10px'
70
+ });
71
+ });
72
+
73
+ test('handles single style object', () => {
74
+ const style: StyleObject = { color: 'blue' };
75
+ const result = mergeStyles(style);
76
+ expect(result).toEqual({ color: 'blue' });
77
+ });
78
+
79
+ test('does not mutate input styles', () => {
80
+ const style1: StyleObject = { color: 'red' };
81
+ const style2: StyleObject = { fontSize: '16px' };
82
+ const original1 = { ...style1 };
83
+ const original2 = { ...style2 };
84
+
85
+ mergeStyles(style1, style2);
86
+
87
+ expect(style1).toEqual(original1);
88
+ expect(style2).toEqual(original2);
89
+ });
90
+
91
+ test('handles empty style objects', () => {
92
+ const result = mergeStyles({}, {}, {});
93
+ expect(result).toEqual({});
94
+ });
95
+
96
+ test('merges with mix of empty and non-empty styles', () => {
97
+ const style1: StyleObject = {};
98
+ const style2: StyleObject = { color: 'red' };
99
+ const style3: StyleObject = {};
100
+ const result = mergeStyles(style1, style2, style3);
101
+ expect(result).toEqual({ color: 'red' });
102
+ });
103
+
104
+ test('handles numeric values', () => {
105
+ const style1: StyleObject = { opacity: 0.5, zIndex: 10 };
106
+ const style2: StyleObject = { zIndex: 20, fontSize: 16 };
107
+ const result = mergeStyles(style1, style2);
108
+ expect(result).toEqual({
109
+ opacity: 0.5,
110
+ zIndex: 20,
111
+ fontSize: 16
112
+ });
113
+ });
114
+
115
+ test('preserves falsy values', () => {
116
+ const style1: StyleObject = { opacity: 0, margin: '0' };
117
+ const style2: StyleObject = { padding: '0px' };
118
+ const result = mergeStyles(style1, style2);
119
+ expect(result).toEqual({
120
+ opacity: 0,
121
+ margin: '0',
122
+ padding: '0px'
123
+ });
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Style Processor
3
+ * Handles component style merging from structured component definitions
4
+ */
5
+
6
+ import type { ComponentDefinition, StyleObject, StyleValue } from '../shared/types';
7
+ import { processStructure, normalizeStyle } from './templateEngine';
8
+
9
+ type ComponentProps = Record<string, unknown>;
10
+
11
+ /**
12
+ * Merge component styles from structured component definition
13
+ * Returns merged props with all applicable styles
14
+ */
15
+ export function mergeComponentStyles(
16
+ componentDef: ComponentDefinition,
17
+ props: ComponentProps
18
+ ): ComponentProps {
19
+ try {
20
+ // Start with base props from component definition
21
+ let mergedProps: ComponentProps = { ...(componentDef.props || {}) };
22
+
23
+ const structuredComponentDef = componentDef.component;
24
+ if (!structuredComponentDef) {
25
+ return props || {};
26
+ }
27
+
28
+ // Build default props from structured component prop definitions (from interface)
29
+ const defaultProps: ComponentProps = {};
30
+ if (structuredComponentDef.interface) {
31
+ for (const [key, def] of Object.entries(structuredComponentDef.interface)) {
32
+ if (def && typeof def === 'object' && 'default' in def) {
33
+ defaultProps[key] = def.default;
34
+ }
35
+ }
36
+ }
37
+ const effectiveInputProps = { ...defaultProps, ...(props || {}) };
38
+
39
+ // Merge resolved props (with defaults) into mergedProps so they're available in PropsPanel
40
+ mergedProps = { ...mergedProps, ...effectiveInputProps };
41
+
42
+ // Compute base style from structure with templates evaluated
43
+ // processStructure now reads from structure.style and merges into props.style
44
+ try {
45
+ if (structuredComponentDef.structure) {
46
+ const processed = processStructure(structuredComponentDef.structure, { props: effectiveInputProps, componentDef: structuredComponentDef });
47
+ // Type guard: ensure processed is a ComponentNode
48
+ if (processed && typeof processed === 'object' && !Array.isArray(processed) && 'props' in processed) {
49
+ const structureStyle = (processed.props?.style && typeof processed.props.style === 'object' && processed.props.style !== null && !Array.isArray(processed.props.style))
50
+ ? processed.props.style as Record<string, unknown>
51
+ : {};
52
+ const currentStyle = typeof mergedProps.style === 'object' && mergedProps.style !== null && !Array.isArray(mergedProps.style)
53
+ ? { ...mergedProps.style as Record<string, unknown> }
54
+ : {};
55
+ mergedProps.style = { ...currentStyle, ...structureStyle };
56
+ }
57
+ }
58
+ } catch (e) {
59
+ }
60
+
61
+ // Merge with instance props (allows overrides)
62
+ // Normalize responsive styles to flat StyleObject
63
+ const restProps = props || {};
64
+ const normalizedMergedStyle = normalizeStyle(mergedProps.style as StyleValue) || {};
65
+ const normalizedRestStyle = normalizeStyle(restProps?.style as StyleValue) || {};
66
+
67
+ mergedProps = {
68
+ ...mergedProps,
69
+ ...restProps,
70
+ style: {
71
+ ...normalizedMergedStyle,
72
+ ...normalizedRestStyle,
73
+ },
74
+ };
75
+
76
+ return mergedProps;
77
+ } catch (error) {
78
+ // Return props as-is if merging fails
79
+ return props || {};
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Merge styles from multiple sources
85
+ */
86
+ export function mergeStyles(...styles: (StyleObject | undefined)[]): StyleObject {
87
+ return styles.reduce<StyleObject>((acc, style) => {
88
+ if (!style) return acc;
89
+ return { ...acc, ...style };
90
+ }, {});
91
+ }
92
+
@@ -0,0 +1,354 @@
1
+ import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";
2
+ import { StyleInjector } from "./StyleInjector";
3
+ import { ComponentRegistry } from "../componentRegistry";
4
+ import { ElementRegistry } from "../elementRegistry";
5
+ import type { ComponentDefinition } from "../../shared/types";
6
+ import {
7
+ createTypedMockHTMLElement,
8
+ createTypedMockDocument,
9
+ type TypedMockDocument,
10
+ type TypedMockHTMLElement,
11
+ } from "../../test-utils/factories/DomMockFactory";
12
+
13
+ describe("StyleInjector", () => {
14
+ let componentRegistry: ComponentRegistry;
15
+ let elementRegistry: ElementRegistry;
16
+ let styleInjector: StyleInjector;
17
+ let mockDocument: TypedMockDocument;
18
+ let mockStyleTag: TypedMockHTMLElement;
19
+ let originalDocument: typeof document;
20
+
21
+ beforeEach(() => {
22
+ // Save original document
23
+ originalDocument = global.document;
24
+
25
+ componentRegistry = new ComponentRegistry();
26
+ elementRegistry = new ElementRegistry();
27
+ styleInjector = new StyleInjector({
28
+ componentRegistry,
29
+ elementRegistry,
30
+ styleId: "component-css",
31
+ });
32
+
33
+ // Create typed mock style tag
34
+ mockStyleTag = createTypedMockHTMLElement({
35
+ tagName: "STYLE",
36
+ id: "component-css",
37
+ });
38
+
39
+ // Create typed mock document
40
+ mockDocument = createTypedMockDocument();
41
+
42
+ // Configure createElement to return our mock style tag
43
+ mockDocument.createElement.mockImplementation((tag: string) => {
44
+ if (tag === "style") {
45
+ return mockStyleTag;
46
+ }
47
+ return createTypedMockHTMLElement({ tagName: tag });
48
+ });
49
+
50
+ // Setup global document with typed mock
51
+ global.document = mockDocument as unknown as Document;
52
+ });
53
+
54
+ afterEach(() => {
55
+ // Clean up any style tags
56
+ styleInjector.clear();
57
+ // Restore original document
58
+ global.document = originalDocument;
59
+ });
60
+
61
+ describe("constructor", () => {
62
+ test("should create instance with default styleId", () => {
63
+ const injector = new StyleInjector({
64
+ componentRegistry,
65
+ elementRegistry,
66
+ });
67
+ expect(injector).toBeInstanceOf(StyleInjector);
68
+ });
69
+
70
+ test("should create instance with custom styleId", () => {
71
+ const injector = new StyleInjector({
72
+ componentRegistry,
73
+ elementRegistry,
74
+ styleId: "custom-style-id",
75
+ });
76
+ expect(injector).toBeInstanceOf(StyleInjector);
77
+ });
78
+ });
79
+
80
+ describe("inject", () => {
81
+ test("should inject CSS without templates", () => {
82
+ const componentDef: ComponentDefinition = {
83
+ type: "component",
84
+ component: {
85
+ interface: {},
86
+ structure: { type: "node", tag: "div" },
87
+ css: ".test { color: red; }",
88
+ },
89
+ };
90
+
91
+ componentRegistry.register("TestComponent", componentDef);
92
+
93
+ styleInjector.inject();
94
+
95
+ expect(mockDocument.createElement).toHaveBeenCalledWith("style");
96
+ expect(mockDocument.head.appendChild).toHaveBeenCalled();
97
+ expect(mockStyleTag.textContent).toContain(".test { color: red; }");
98
+ expect(mockStyleTag.textContent).toContain("Component: TestComponent");
99
+ });
100
+
101
+ // Template-based tests require full ElementRegistry from @meno/studio
102
+ // These are integration tests that should be run in the studio package
103
+ test.skip("should inject CSS with templates per instance (requires @meno/studio ElementRegistry)", () => {
104
+ const componentDef: ComponentDefinition = {
105
+ type: "component",
106
+ component: {
107
+ interface: {},
108
+ structure: { type: "node", tag: "div" },
109
+ css: ".test { color: {{color}}; }",
110
+ },
111
+ };
112
+
113
+ componentRegistry.register("TestComponent", componentDef);
114
+
115
+ // Register component instances with props using typed mocks
116
+ const mockElement1 = createTypedMockHTMLElement({ tagName: "DIV" });
117
+ const mockElement2 = createTypedMockHTMLElement({ tagName: "DIV" });
118
+ elementRegistry.register([0], mockElement1 as unknown as HTMLElement, "TestComponent", true, {
119
+ color: "red",
120
+ });
121
+ elementRegistry.register([1], mockElement2 as unknown as HTMLElement, "TestComponent", true, {
122
+ color: "blue",
123
+ });
124
+
125
+ styleInjector.inject();
126
+
127
+ expect(mockDocument.createElement).toHaveBeenCalledWith("style");
128
+ expect(mockDocument.head.appendChild).toHaveBeenCalled();
129
+ expect(mockStyleTag.textContent).toContain("color: red");
130
+ expect(mockStyleTag.textContent).toContain("color: blue");
131
+ expect(mockStyleTag.textContent).toContain("(instance)");
132
+ });
133
+
134
+ test.skip("should handle multiple components with mixed template/non-template CSS (requires @meno/studio ElementRegistry)", () => {
135
+ const componentDef1: ComponentDefinition = {
136
+ type: "component",
137
+ component: {
138
+ interface: {},
139
+ structure: { type: "node", tag: "div" },
140
+ css: ".test1 { color: red; }",
141
+ },
142
+ };
143
+
144
+ const componentDef2: ComponentDefinition = {
145
+ type: "component",
146
+ component: {
147
+ interface: {},
148
+ structure: { type: "node", tag: "div" },
149
+ css: ".test2 { color: {{color}}; }",
150
+ },
151
+ };
152
+
153
+ componentRegistry.register("Component1", componentDef1);
154
+ componentRegistry.register("Component2", componentDef2);
155
+
156
+ const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
157
+ elementRegistry.register([0], mockElement as unknown as HTMLElement, "Component2", true, {
158
+ color: "blue",
159
+ });
160
+
161
+ styleInjector.inject();
162
+
163
+ expect(mockStyleTag.textContent).toContain("Component: Component1");
164
+ expect(mockStyleTag.textContent).toContain("Component: Component2");
165
+ expect(mockStyleTag.textContent).toContain(".test1 { color: red; }");
166
+ expect(mockStyleTag.textContent).toContain("color: blue");
167
+ });
168
+
169
+ test("should handle existing style tag before injecting new one", () => {
170
+ const existingStyle = createTypedMockHTMLElement({
171
+ tagName: "STYLE",
172
+ id: "component-css",
173
+ });
174
+
175
+ mockDocument.getElementById.mockReturnValue(existingStyle);
176
+
177
+ const componentDef: ComponentDefinition = {
178
+ type: "component",
179
+ component: {
180
+ interface: {},
181
+ structure: { type: "node", tag: "div" },
182
+ css: ".test { color: red; }",
183
+ },
184
+ };
185
+
186
+ componentRegistry.register("TestComponent", componentDef);
187
+
188
+ styleInjector.inject();
189
+
190
+ expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
191
+ expect(mockDocument.createElement).toHaveBeenCalledWith("style");
192
+ });
193
+
194
+ test("should not inject CSS if no components have CSS", () => {
195
+ const componentDef: ComponentDefinition = {
196
+ type: "component",
197
+ component: {
198
+ interface: {},
199
+ structure: { type: "node", tag: "div" },
200
+ },
201
+ };
202
+
203
+ componentRegistry.register("TestComponent", componentDef);
204
+
205
+ styleInjector.inject();
206
+
207
+ expect(mockDocument.createElement).not.toHaveBeenCalled();
208
+ expect(mockDocument.head.appendChild).not.toHaveBeenCalled();
209
+ });
210
+
211
+ test.skip("should handle template processing errors gracefully (requires @meno/studio ElementRegistry)", () => {
212
+ const componentDef: ComponentDefinition = {
213
+ type: "component",
214
+ component: {
215
+ interface: {},
216
+ structure: { type: "node", tag: "div" },
217
+ css: ".test { color: {{invalid.prop}}; }",
218
+ },
219
+ };
220
+
221
+ componentRegistry.register("TestComponent", componentDef);
222
+
223
+ const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
224
+ elementRegistry.register([0], mockElement as unknown as HTMLElement, "TestComponent", true, {
225
+ color: "red",
226
+ });
227
+
228
+ // Should not throw, should fallback to original CSS
229
+ expect(() => styleInjector.inject()).not.toThrow();
230
+ expect(mockStyleTag.textContent).toContain("Component: TestComponent");
231
+ });
232
+
233
+ test("should handle missing component props gracefully", () => {
234
+ const componentDef: ComponentDefinition = {
235
+ type: "component",
236
+ component: {
237
+ interface: {},
238
+ structure: { type: "node", tag: "div" },
239
+ css: ".test { color: {{color}}; }",
240
+ },
241
+ };
242
+
243
+ componentRegistry.register("TestComponent", componentDef);
244
+
245
+ // Register element without props
246
+ const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
247
+ elementRegistry.register([0], mockElement as unknown as HTMLElement, "TestComponent", true);
248
+
249
+ // Should not throw
250
+ expect(() => styleInjector.inject()).not.toThrow();
251
+ });
252
+
253
+ test("should handle errors during injection gracefully", () => {
254
+ // Force an error by making getAll throw
255
+ const originalGetAll = componentRegistry.getAll.bind(componentRegistry);
256
+ componentRegistry.getAll = mock(() => {
257
+ throw new Error("Test error");
258
+ });
259
+
260
+ // Should not throw - should handle error gracefully
261
+ expect(() => styleInjector.inject()).not.toThrow();
262
+
263
+ // Restore
264
+ componentRegistry.getAll = originalGetAll;
265
+ });
266
+ });
267
+
268
+ describe("clear", () => {
269
+ test("should remove style tag if it exists", () => {
270
+ const existingStyle = createTypedMockHTMLElement({
271
+ tagName: "STYLE",
272
+ id: "component-css",
273
+ });
274
+
275
+ mockDocument.getElementById.mockReturnValueOnce(existingStyle);
276
+
277
+ styleInjector.clear();
278
+
279
+ expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
280
+ expect(existingStyle.remove).toHaveBeenCalled();
281
+ });
282
+
283
+ test("should not throw if style tag does not exist", () => {
284
+ mockDocument.getElementById.mockReturnValueOnce(null);
285
+
286
+ expect(() => styleInjector.clear()).not.toThrow();
287
+ });
288
+ });
289
+
290
+ describe("exists", () => {
291
+ test("should return true if style tag exists", () => {
292
+ const existingStyle = createTypedMockHTMLElement({
293
+ tagName: "STYLE",
294
+ id: "component-css",
295
+ });
296
+
297
+ mockDocument.getElementById.mockReturnValueOnce(existingStyle);
298
+
299
+ expect(styleInjector.exists()).toBe(true);
300
+ expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
301
+ });
302
+
303
+ test("should return false if style tag does not exist", () => {
304
+ mockDocument.getElementById.mockReturnValueOnce(null);
305
+
306
+ expect(styleInjector.exists()).toBe(false);
307
+ expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
308
+ });
309
+ });
310
+
311
+ describe("integration", () => {
312
+ test.skip("should handle full injection cycle (requires @meno/studio ElementRegistry)", () => {
313
+ const componentDef: ComponentDefinition = {
314
+ type: "component",
315
+ component: {
316
+ interface: {},
317
+ structure: { type: "node", tag: "div" },
318
+ css: ".test { color: {{color}}; }",
319
+ },
320
+ };
321
+
322
+ componentRegistry.register("TestComponent", componentDef);
323
+
324
+ const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
325
+ elementRegistry.register([0], mockElement as unknown as HTMLElement, "TestComponent", true, {
326
+ color: "green",
327
+ });
328
+
329
+ // Inject - should create style tag
330
+ mockDocument.getElementById.mockReturnValueOnce(null); // No existing tag
331
+ styleInjector.inject();
332
+ expect(mockDocument.createElement).toHaveBeenCalledWith("style");
333
+ expect(mockDocument.head.appendChild).toHaveBeenCalled();
334
+
335
+ // Verify exists() works after injection
336
+ mockDocument.getElementById.mockReturnValueOnce(mockStyleTag);
337
+ expect(styleInjector.exists()).toBe(true);
338
+
339
+ // Clear - should remove style tag
340
+ const styleTagWithRemove = createTypedMockHTMLElement({
341
+ tagName: "STYLE",
342
+ id: "component-css",
343
+ });
344
+ mockDocument.getElementById.mockReturnValueOnce(styleTagWithRemove);
345
+ styleInjector.clear();
346
+ expect(styleTagWithRemove.remove).toHaveBeenCalled();
347
+
348
+ // Verify it's gone
349
+ mockDocument.getElementById.mockReturnValueOnce(null);
350
+ expect(styleInjector.exists()).toBe(false);
351
+ });
352
+ });
353
+ });
354
+