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,1005 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
2
+ import {
3
+ generateSSRHTML,
4
+ renderPageSSR,
5
+ extractPageMeta,
6
+ generateMetaTags
7
+ } from "./ssrRenderer";
8
+ import type { ComponentNode, ComponentDefinition, JSONPage } from "../shared/types";
9
+
10
+ // Mock jsonLoader
11
+ const mockLoadBreakpointConfig = async () => ({
12
+ tablet: 1024,
13
+ mobile: 540
14
+ });
15
+
16
+ // Mock the jsonLoader module
17
+ const originalJsonLoader = await import("./jsonLoader");
18
+ if (originalJsonLoader) {
19
+ // We'll need to test with actual imports, but can provide mocks where needed
20
+ }
21
+
22
+ describe("SSR Renderer - extractPageMeta", () => {
23
+ test("should extract basic meta information", () => {
24
+ const pageData: JSONPage = {
25
+ meta: {
26
+ title: "Test Page",
27
+ description: "Test description",
28
+ keywords: "test, page"
29
+ }
30
+ };
31
+
32
+ const meta = extractPageMeta(pageData);
33
+
34
+ expect(meta.title).toBe("Test Page");
35
+ expect(meta.description).toBe("Test description");
36
+ expect(meta.keywords).toBe("test, page");
37
+ });
38
+
39
+ test("should extract Open Graph meta", () => {
40
+ const pageData: JSONPage = {
41
+ meta: {
42
+ title: "Test Page",
43
+ ogTitle: "OG Title",
44
+ ogDescription: "OG Description",
45
+ ogImage: "https://example.com/image.jpg",
46
+ ogType: "article"
47
+ }
48
+ };
49
+
50
+ const meta = extractPageMeta(pageData);
51
+
52
+ expect(meta.ogTitle).toBe("OG Title");
53
+ expect(meta.ogDescription).toBe("OG Description");
54
+ expect(meta.ogImage).toBe("https://example.com/image.jpg");
55
+ expect(meta.ogType).toBe("article");
56
+ });
57
+
58
+ test("should use title as fallback for ogTitle", () => {
59
+ const pageData: JSONPage = {
60
+ meta: {
61
+ title: "Test Page"
62
+ }
63
+ };
64
+
65
+ const meta = extractPageMeta(pageData);
66
+
67
+ expect(meta.ogTitle).toBe("Test Page");
68
+ });
69
+
70
+ test("should use description as fallback for ogDescription", () => {
71
+ const pageData: JSONPage = {
72
+ meta: {
73
+ description: "Test description"
74
+ }
75
+ };
76
+
77
+ const meta = extractPageMeta(pageData);
78
+
79
+ expect(meta.ogDescription).toBe("Test description");
80
+ });
81
+
82
+ test("should default ogType to 'website'", () => {
83
+ const pageData: JSONPage = {
84
+ meta: {}
85
+ };
86
+
87
+ const meta = extractPageMeta(pageData);
88
+
89
+ expect(meta.ogType).toBe("website");
90
+ });
91
+
92
+ test("should handle missing meta object", () => {
93
+ const pageData: JSONPage = {};
94
+
95
+ const meta = extractPageMeta(pageData);
96
+
97
+ expect(Object.keys(meta).length).toBe(0);
98
+ });
99
+ });
100
+
101
+ describe("SSR Renderer - generateMetaTags", () => {
102
+ test("should generate title tag", () => {
103
+ const meta = {
104
+ title: "Test Page"
105
+ };
106
+
107
+ const tags = generateMetaTags(meta);
108
+
109
+ expect(tags).toContain("<title>Test Page</title>");
110
+ });
111
+
112
+ test("should generate description meta tag", () => {
113
+ const meta = {
114
+ description: "Test description"
115
+ };
116
+
117
+ const tags = generateMetaTags(meta);
118
+
119
+ expect(tags).toContain(`<meta name="description" content="Test description" />`);
120
+ });
121
+
122
+ test("should generate keywords meta tag", () => {
123
+ const meta = {
124
+ keywords: "test, keywords"
125
+ };
126
+
127
+ const tags = generateMetaTags(meta);
128
+
129
+ expect(tags).toContain(`<meta name="keywords" content="test, keywords" />`);
130
+ });
131
+
132
+ test("should generate Open Graph tags", () => {
133
+ const meta = {
134
+ ogTitle: "OG Title",
135
+ ogDescription: "OG Description",
136
+ ogImage: "https://example.com/image.jpg",
137
+ ogType: "article"
138
+ };
139
+
140
+ const tags = generateMetaTags(meta);
141
+
142
+ expect(tags).toContain(`<meta property="og:title" content="OG Title" />`);
143
+ expect(tags).toContain(`<meta property="og:description" content="OG Description" />`);
144
+ expect(tags).toContain(`<meta property="og:image" content="https://example.com/image.jpg" />`);
145
+ expect(tags).toContain(`<meta property="og:type" content="article" />`);
146
+ });
147
+
148
+ test("should generate canonical URL", () => {
149
+ const meta = {};
150
+ const url = "https://example.com/page";
151
+
152
+ const tags = generateMetaTags(meta, url);
153
+
154
+ expect(tags).toContain(`<link rel="canonical" href="https://example.com/page" />`);
155
+ expect(tags).toContain(`<meta property="og:url" content="https://example.com/page" />`);
156
+ });
157
+
158
+ test("should escape HTML in meta content", () => {
159
+ const meta = {
160
+ title: "Test & Page",
161
+ description: "Test <description>"
162
+ };
163
+
164
+ const tags = generateMetaTags(meta);
165
+
166
+ expect(tags).toContain("<title>Test &amp; Page</title>");
167
+ expect(tags).toContain(`content="Test &lt;description&gt;"`);
168
+ });
169
+
170
+ test("should handle empty meta", () => {
171
+ const meta = {};
172
+
173
+ const tags = generateMetaTags(meta);
174
+
175
+ expect(tags).toBe("");
176
+ });
177
+ });
178
+
179
+ describe("SSR Renderer - renderPageSSR", () => {
180
+ describe("HTML generation", () => {
181
+ test("should render simple div element", async () => {
182
+ const pageData: JSONPage = {
183
+ root: {
184
+ type: "node",
185
+ tag: "div",
186
+ children: ["Hello World"]
187
+ }
188
+ };
189
+
190
+ const result = await renderPageSSR(pageData);
191
+
192
+ expect(result.html).toContain("<div");
193
+ expect(result.html).toContain("Hello World");
194
+ expect(result.html).toContain("</div>");
195
+ });
196
+
197
+ test("should render nested component structures", async () => {
198
+ const pageData: JSONPage = {
199
+ root: {
200
+ type: "node",
201
+ tag: "div",
202
+ children: [
203
+ {
204
+ type: "node",
205
+ tag: "div",
206
+ children: [
207
+ {
208
+ type: "node",
209
+ tag: "span",
210
+ children: ["Nested"]
211
+ }
212
+ ]
213
+ }
214
+ ]
215
+ }
216
+ };
217
+
218
+ const result = await renderPageSSR(pageData);
219
+
220
+ expect(result.html).toContain("<div");
221
+ expect(result.html).toContain("<span");
222
+ expect(result.html).toContain("Nested");
223
+ });
224
+
225
+ test("should render children arrays", async () => {
226
+ const pageData: JSONPage = {
227
+ root: {
228
+ type: "node",
229
+ tag: "div",
230
+ children: [
231
+ { type: "node", tag: "p", children: ["First"] },
232
+ { type: "node", tag: "p", children: ["Second"] }
233
+ ]
234
+ }
235
+ };
236
+
237
+ const result = await renderPageSSR(pageData);
238
+
239
+ expect(result.html).toContain("First");
240
+ expect(result.html).toContain("Second");
241
+ });
242
+
243
+ test("should render string children", async () => {
244
+ const pageData: JSONPage = {
245
+ root: {
246
+ type: "node",
247
+ tag: "p",
248
+ children: ["Simple text"]
249
+ }
250
+ };
251
+
252
+ const result = await renderPageSSR(pageData);
253
+
254
+ expect(result.html).toContain("Simple text");
255
+ });
256
+
257
+ test("should render number children", async () => {
258
+ const pageData: JSONPage = {
259
+ root: {
260
+ type: "node",
261
+ tag: "span",
262
+ children: [42]
263
+ }
264
+ };
265
+
266
+ const result = await renderPageSSR(pageData);
267
+
268
+ expect(result.html).toContain("42");
269
+ });
270
+
271
+ test("should escape HTML in text content", async () => {
272
+ const pageData: JSONPage = {
273
+ root: {
274
+ type: "node",
275
+ tag: "div",
276
+ children: ["<script>alert('xss')</script>"]
277
+ }
278
+ };
279
+
280
+ const result = await renderPageSSR(pageData);
281
+
282
+ expect(result.html).toContain("&lt;script&gt;");
283
+ expect(result.html).not.toContain("<script>");
284
+ });
285
+
286
+ test("should render anchor tag", async () => {
287
+ const pageData: JSONPage = {
288
+ root: {
289
+ type: "node",
290
+ tag: "a",
291
+ children: ["Go to About"]
292
+ }
293
+ };
294
+
295
+ const result = await renderPageSSR(pageData);
296
+
297
+ expect(result.html).toContain(`<a`);
298
+ expect(result.html).toContain("Go to About");
299
+ expect(result.html).toContain("</a>");
300
+ });
301
+ });
302
+
303
+ describe("CSS generation", () => {
304
+ test("should generate CSS for responsive styles", async () => {
305
+ const pageData: JSONPage = {
306
+ root: {
307
+ type: "node",
308
+ tag: "div",
309
+ style: {
310
+ base: {
311
+ fontSize: "16px",
312
+ color: "red"
313
+ },
314
+ tablet: {
315
+ fontSize: "14px"
316
+ },
317
+ mobile: {
318
+ fontSize: "12px"
319
+ }
320
+ }
321
+ }
322
+ };
323
+
324
+ const result = await renderPageSSR(pageData);
325
+
326
+ // SSR renders HTML with styles (may be inline or via classes)
327
+ expect(result.html).toBeDefined();
328
+ expect(result.html).toContain("<div");
329
+ });
330
+
331
+ test("should generate CSS class names for responsive styles", async () => {
332
+ const pageData: JSONPage = {
333
+ root: {
334
+ type: "node",
335
+ tag: "div",
336
+ style: {
337
+ base: {
338
+ color: "red"
339
+ }
340
+ }
341
+ }
342
+ };
343
+
344
+ const result = await renderPageSSR(pageData);
345
+
346
+ // HTML should be generated with styles
347
+ expect(result.html).toBeDefined();
348
+ expect(result.html).toContain("<div");
349
+ });
350
+
351
+ test("should handle empty responsive styles", async () => {
352
+ const pageData: JSONPage = {
353
+ root: {
354
+ type: "node",
355
+ tag: "div",
356
+ style: {
357
+ base: {},
358
+ tablet: {},
359
+ mobile: {}
360
+ }
361
+ }
362
+ };
363
+
364
+ const result = await renderPageSSR(pageData);
365
+
366
+ // Should still render HTML
367
+ expect(result.html).toContain("<div");
368
+ // componentCSS might be empty or minimal
369
+ expect(result.componentCSS).toBeDefined();
370
+ });
371
+ });
372
+
373
+ describe("Component rendering", () => {
374
+ test("should render Button component (from Button.json)", async () => {
375
+ const buttonComponent: ComponentDefinition = {
376
+ type: "Button",
377
+ component: {
378
+ interface: {
379
+ variant: {
380
+ type: "select",
381
+ default: "primary"
382
+ },
383
+ children: {
384
+ type: "slot",
385
+ default: "Click me"
386
+ }
387
+ },
388
+ structure: {
389
+ type: "node",
390
+ tag: "button",
391
+ style: {
392
+ base: {
393
+ background: {
394
+ _mapping: true,
395
+ prop: "variant",
396
+ values: {
397
+ primary: "blue",
398
+ secondary: "gray"
399
+ }
400
+ } as any,
401
+ color: "white"
402
+ }
403
+ },
404
+ children: "{{children}}"
405
+ }
406
+ }
407
+ };
408
+
409
+ const pageData: JSONPage = {
410
+ root: {
411
+ type: "component",
412
+ component: "Button",
413
+ props: {
414
+ variant: "primary"
415
+ },
416
+ children: ["Custom Button"]
417
+ }
418
+ };
419
+
420
+ const globalComponents = {
421
+ Button: buttonComponent
422
+ };
423
+
424
+ const result = await renderPageSSR(pageData, globalComponents);
425
+
426
+ // Should render button element
427
+ expect(result.html).toContain("<button");
428
+ // Should render children
429
+ expect(result.html).toContain("Custom Button");
430
+ });
431
+
432
+ test("should render BasicCard component with template evaluation", async () => {
433
+ const cardComponent: ComponentDefinition = {
434
+ type: "BasicCard",
435
+ component: {
436
+ interface: {
437
+ title: {
438
+ type: "string",
439
+ default: "Card Title"
440
+ }
441
+ },
442
+ structure: {
443
+ type: "node",
444
+ tag: "div",
445
+ children: [
446
+ {
447
+ type: "node",
448
+ tag: "h3",
449
+ children: ["{{title}}"]
450
+ }
451
+ ]
452
+ }
453
+ }
454
+ };
455
+
456
+ const pageData: JSONPage = {
457
+ root: {
458
+ type: "component",
459
+ component: "BasicCard",
460
+ props: {
461
+ title: "My Card"
462
+ }
463
+ }
464
+ };
465
+
466
+ const globalComponents = {
467
+ BasicCard: cardComponent
468
+ };
469
+
470
+ const result = await renderPageSSR(pageData, globalComponents);
471
+
472
+ expect(result.html).toContain("<div");
473
+ expect(result.html).toContain("<h3");
474
+ expect(result.html).toContain("My Card");
475
+ });
476
+ });
477
+
478
+ describe("Template evaluation in SSR", () => {
479
+ test("should evaluate template expressions in structure", async () => {
480
+ const pageData: JSONPage = {
481
+ root: {
482
+ type: "node",
483
+ tag: "div",
484
+ children: ["Hello {{name}}"]
485
+ }
486
+ };
487
+
488
+ // Note: This test may need adjustment based on actual template evaluation in SSR
489
+ const result = await renderPageSSR(pageData);
490
+
491
+ // Template should be evaluated or passed through
492
+ expect(result.html).toBeDefined();
493
+ });
494
+
495
+ test("should handle style mappings in component structures", async () => {
496
+ const buttonComponent: ComponentDefinition = {
497
+ type: "Button",
498
+ component: {
499
+ interface: {
500
+ variant: {
501
+ type: "select",
502
+ default: "primary"
503
+ }
504
+ },
505
+ structure: {
506
+ type: "node",
507
+ tag: "button",
508
+ style: {
509
+ base: {
510
+ background: {
511
+ _mapping: true,
512
+ prop: "variant",
513
+ values: {
514
+ primary: "blue",
515
+ secondary: "gray"
516
+ }
517
+ } as any
518
+ }
519
+ }
520
+ }
521
+ }
522
+ };
523
+
524
+ const pageData: JSONPage = {
525
+ root: {
526
+ type: "component",
527
+ component: "Button",
528
+ props: {
529
+ variant: "primary"
530
+ }
531
+ }
532
+ };
533
+
534
+ const globalComponents = {
535
+ Button: buttonComponent
536
+ };
537
+
538
+ const result = await renderPageSSR(pageData, globalComponents);
539
+
540
+ // Should resolve style mapping based on variant prop
541
+ expect(result.html).toBeDefined();
542
+ // CSS should include resolved background color or inline style
543
+ expect(result.html.length).toBeGreaterThan(0);
544
+ });
545
+ });
546
+
547
+ describe("Error handling", () => {
548
+ test("should handle missing root node", async () => {
549
+ const pageData: JSONPage = {};
550
+
551
+ // Implementation throws error for missing root
552
+ await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
553
+ });
554
+
555
+ test("should handle null root node", async () => {
556
+ const pageData: JSONPage = {
557
+ root: null as any
558
+ };
559
+
560
+ // Implementation throws error for null root
561
+ await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
562
+ });
563
+
564
+ test("should handle component with missing structure", async () => {
565
+ const invalidComponent: ComponentDefinition = {
566
+ type: "Invalid",
567
+ component: {} as any
568
+ };
569
+
570
+ const pageData: JSONPage = {
571
+ root: {
572
+ type: "Invalid"
573
+ }
574
+ };
575
+
576
+ const globalComponents = {
577
+ Invalid: invalidComponent
578
+ };
579
+
580
+ // Should not throw, but return empty HTML for invalid component
581
+ const result = await renderPageSSR(pageData, globalComponents);
582
+
583
+ expect(result.html).toBe("");
584
+ });
585
+
586
+ test("should handle malformed JSON structures gracefully", async () => {
587
+ const pageData: JSONPage = {
588
+ root: {
589
+ type: "node",
590
+ tag: "div",
591
+ children: [
592
+ null as any,
593
+ undefined as any
594
+ ]
595
+ }
596
+ };
597
+
598
+ const result = await renderPageSSR(pageData);
599
+
600
+ // Should skip null/undefined children
601
+ expect(result.html).toBeDefined();
602
+ expect(result.html).toContain("<div");
603
+ });
604
+ });
605
+
606
+ describe("Meta information in SSR", () => {
607
+ test("should extract and return meta information", async () => {
608
+ const pageData: JSONPage = {
609
+ meta: {
610
+ title: "Test Page",
611
+ description: "Test description"
612
+ },
613
+ root: {
614
+ type: "node",
615
+ tag: "div",
616
+ children: ["Content"]
617
+ }
618
+ };
619
+
620
+ const result = await renderPageSSR(pageData);
621
+
622
+ expect(result.title).toBe("Test Page");
623
+ expect(result.meta).toContain("Test Page");
624
+ expect(result.meta).toContain("Test description");
625
+ });
626
+
627
+ test("should use default title when meta not provided", async () => {
628
+ const pageData: JSONPage = {
629
+ root: {
630
+ type: "node",
631
+ tag: "div"
632
+ }
633
+ };
634
+
635
+ const result = await renderPageSSR(pageData);
636
+
637
+ expect(result.title).toBe("UPLO");
638
+ });
639
+ });
640
+ });
641
+
642
+ describe("SSR Renderer - Security (XSS Prevention)", () => {
643
+ describe("Text content sanitization", () => {
644
+ test("should escape script tags in text content", async () => {
645
+ const pageData: JSONPage = {
646
+ root: {
647
+ type: "node",
648
+ tag: "div",
649
+ children: ["<script>alert('xss')</script>"]
650
+ }
651
+ };
652
+
653
+ const result = await renderPageSSR(pageData);
654
+
655
+ expect(result.html).toContain("&lt;script&gt;");
656
+ expect(result.html).not.toContain("<script>alert");
657
+ });
658
+
659
+ test("should escape event handlers in text content", async () => {
660
+ const pageData: JSONPage = {
661
+ root: {
662
+ type: "node",
663
+ tag: "div",
664
+ children: ['<img onerror="alert(1)" src="x">']
665
+ }
666
+ };
667
+
668
+ const result = await renderPageSSR(pageData);
669
+
670
+ expect(result.html).not.toContain('onerror="alert');
671
+ expect(result.html).toContain("&lt;img");
672
+ });
673
+
674
+ test("should escape javascript: URLs in text content", async () => {
675
+ const pageData: JSONPage = {
676
+ root: {
677
+ type: "node",
678
+ tag: "div",
679
+ children: ['<a href="javascript:alert(1)">click</a>']
680
+ }
681
+ };
682
+
683
+ const result = await renderPageSSR(pageData);
684
+
685
+ expect(result.html).not.toContain('href="javascript:');
686
+ expect(result.html).toContain("&lt;a");
687
+ });
688
+
689
+ test("should escape HTML entities in text content", async () => {
690
+ const pageData: JSONPage = {
691
+ root: {
692
+ type: "node",
693
+ tag: "p",
694
+ children: ['Test & <special> "quotes" \'apostrophe\'']
695
+ }
696
+ };
697
+
698
+ const result = await renderPageSSR(pageData);
699
+
700
+ expect(result.html).toContain("&amp;");
701
+ expect(result.html).toContain("&lt;special&gt;");
702
+ expect(result.html).toContain("&quot;quotes&quot;");
703
+ });
704
+
705
+ test("should escape nested malicious content", async () => {
706
+ const pageData: JSONPage = {
707
+ root: {
708
+ type: "node",
709
+ tag: "div",
710
+ children: [
711
+ {
712
+ type: "node",
713
+ tag: "p",
714
+ children: ["<script>document.cookie</script>"]
715
+ }
716
+ ]
717
+ }
718
+ };
719
+
720
+ const result = await renderPageSSR(pageData);
721
+
722
+ expect(result.html).not.toContain("<script>document");
723
+ expect(result.html).toContain("&lt;script&gt;");
724
+ });
725
+ });
726
+
727
+ describe("Meta tag sanitization", () => {
728
+ test("should escape HTML in page title", () => {
729
+ const meta = {
730
+ title: "Test <script>alert('xss')</script> Page"
731
+ };
732
+
733
+ const tags = generateMetaTags(meta);
734
+
735
+ expect(tags).toContain("<title>Test &lt;script&gt;");
736
+ expect(tags).not.toContain("<script>alert");
737
+ });
738
+
739
+ test("should escape HTML in meta description", () => {
740
+ const meta = {
741
+ description: 'Description with "quotes" and <tags>'
742
+ };
743
+
744
+ const tags = generateMetaTags(meta);
745
+
746
+ expect(tags).toContain("&quot;quotes&quot;");
747
+ expect(tags).toContain("&lt;tags&gt;");
748
+ });
749
+
750
+ test("should escape HTML in Open Graph tags", () => {
751
+ const meta = {
752
+ ogTitle: "<script>alert('og')</script>",
753
+ ogDescription: "Description <img src=x onerror=alert(1)>"
754
+ };
755
+
756
+ const tags = generateMetaTags(meta);
757
+
758
+ // Script tags should be escaped
759
+ expect(tags).not.toContain("<script>alert");
760
+ expect(tags).toContain("&lt;script&gt;");
761
+ // IMG tag should be escaped, preventing onerror from executing
762
+ expect(tags).not.toContain("<img src=x");
763
+ expect(tags).toContain("&lt;img");
764
+ });
765
+ });
766
+
767
+ describe("Attribute sanitization", () => {
768
+ test("should not allow malicious attributes through style", async () => {
769
+ const pageData: JSONPage = {
770
+ root: {
771
+ type: "node",
772
+ tag: "div",
773
+ style: {
774
+ base: {
775
+ // Attempting to inject via style properties
776
+ color: 'red; background-image: url("javascript:alert(1)")'
777
+ }
778
+ }
779
+ }
780
+ };
781
+
782
+ const result = await renderPageSSR(pageData);
783
+
784
+ // Style should be processed safely via utility classes
785
+ expect(result.html).toBeDefined();
786
+ expect(result.html).toContain("<div");
787
+ });
788
+
789
+ test("should handle unicode escape sequences safely", async () => {
790
+ const pageData: JSONPage = {
791
+ root: {
792
+ type: "node",
793
+ tag: "div",
794
+ children: ["\u003cscript\u003ealert('unicode')\u003c/script\u003e"]
795
+ }
796
+ };
797
+
798
+ const result = await renderPageSSR(pageData);
799
+
800
+ // Unicode-encoded script tags should still be escaped
801
+ expect(result.html).not.toContain("<script>alert");
802
+ });
803
+ });
804
+
805
+ describe("Component prop sanitization", () => {
806
+ test("should sanitize component props with HTML content", async () => {
807
+ const cardComponent: ComponentDefinition = {
808
+ type: "Card",
809
+ component: {
810
+ interface: {
811
+ title: { type: "string", default: "" }
812
+ },
813
+ structure: {
814
+ type: "node",
815
+ tag: "div",
816
+ children: [
817
+ {
818
+ type: "node",
819
+ tag: "h3",
820
+ children: ["{{title}}"]
821
+ }
822
+ ]
823
+ }
824
+ }
825
+ };
826
+
827
+ const pageData: JSONPage = {
828
+ root: {
829
+ type: "component",
830
+ component: "Card",
831
+ props: {
832
+ title: "<script>alert('props')</script>"
833
+ }
834
+ }
835
+ };
836
+
837
+ const globalComponents = {
838
+ Card: cardComponent
839
+ };
840
+
841
+ const result = await renderPageSSR(pageData, globalComponents);
842
+
843
+ // Script tags should be escaped when rendering
844
+ expect(result.html).not.toContain("<script>alert");
845
+ });
846
+ });
847
+
848
+ describe("Image sanitization", () => {
849
+ test("should escape src attribute in image nodes", async () => {
850
+ const pageData: JSONPage = {
851
+ root: {
852
+ type: "image",
853
+ tag: "img",
854
+ src: 'javascript:alert("img")',
855
+ alt: "test"
856
+ } as any
857
+ };
858
+
859
+ const result = await renderPageSSR(pageData);
860
+
861
+ // Implementation should handle this safely
862
+ expect(result.html).toBeDefined();
863
+ });
864
+
865
+ test("should escape alt attribute in image nodes", async () => {
866
+ const pageData: JSONPage = {
867
+ root: {
868
+ type: "image",
869
+ tag: "img",
870
+ src: "/test.jpg",
871
+ alt: '"><script>alert("alt")</script><img x="'
872
+ } as any
873
+ };
874
+
875
+ const result = await renderPageSSR(pageData);
876
+
877
+ // Alt should be escaped
878
+ expect(result.html).not.toContain('<script>alert("alt")');
879
+ });
880
+ });
881
+
882
+ describe("Embed content sanitization", () => {
883
+ test("should sanitize embed HTML with DOMPurify", async () => {
884
+ const pageData: JSONPage = {
885
+ root: {
886
+ type: "embed",
887
+ tag: "div",
888
+ html: '<div onclick="alert(1)"><script>alert(2)</script><p>safe</p></div>'
889
+ } as any
890
+ };
891
+
892
+ const result = await renderPageSSR(pageData);
893
+
894
+ // onclick should be stripped
895
+ expect(result.html).not.toContain('onclick="alert');
896
+ // script tags should be stripped
897
+ expect(result.html).not.toContain('<script>alert');
898
+ // Safe content should remain
899
+ expect(result.html).toContain("safe");
900
+ });
901
+
902
+ test("should allow safe SVG in embeds", async () => {
903
+ const pageData: JSONPage = {
904
+ root: {
905
+ type: "embed",
906
+ tag: "div",
907
+ html: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="red"/></svg>'
908
+ } as any
909
+ };
910
+
911
+ const result = await renderPageSSR(pageData);
912
+
913
+ // Safe SVG should be preserved
914
+ expect(result.html).toContain("<svg");
915
+ expect(result.html).toContain("<circle");
916
+ });
917
+ });
918
+ });
919
+
920
+ describe("SSR Renderer - generateSSRHTML", () => {
921
+ test("should generate complete HTML document", async () => {
922
+ const pageData: JSONPage = {
923
+ meta: {
924
+ title: "Test Page"
925
+ },
926
+ root: {
927
+ type: "node",
928
+ tag: "div",
929
+ children: ["Hello World"]
930
+ }
931
+ };
932
+
933
+ const html = await generateSSRHTML(pageData, {}, "/", "");
934
+
935
+ expect(html).toContain("<!DOCTYPE html>");
936
+ expect(html).toContain("<html");
937
+ expect(html).toContain("<head");
938
+ expect(html).toContain("<body");
939
+ expect(html).toContain('<div id="root">');
940
+ expect(html).toContain("Hello World");
941
+ expect(html).toContain("Test Page");
942
+ });
943
+
944
+ test("should include CSS in style tag", async () => {
945
+ const pageData: JSONPage = {
946
+ root: {
947
+ type: "node",
948
+ tag: "div",
949
+ style: {
950
+ base: {
951
+ color: "red"
952
+ }
953
+ }
954
+ }
955
+ };
956
+
957
+ const html = await generateSSRHTML(pageData);
958
+
959
+ // HTML should be generated with styles (either inline or in style tag)
960
+ expect(html).toContain("<div");
961
+ expect(html).toBeDefined();
962
+ });
963
+
964
+ test("should include client script", async () => {
965
+ const pageData: JSONPage = {
966
+ root: {
967
+ type: "node",
968
+ tag: "div"
969
+ }
970
+ };
971
+
972
+ const html = await generateSSRHTML(pageData, {}, "/", "", false);
973
+
974
+ expect(html).toContain('src="/client-router.tsx"');
975
+ });
976
+
977
+ test("should use built bundle when specified", async () => {
978
+ const pageData: JSONPage = {
979
+ root: {
980
+ type: "node",
981
+ tag: "div"
982
+ }
983
+ };
984
+
985
+ const html = await generateSSRHTML(pageData, {}, "/", "", true);
986
+
987
+ // When useBuiltBundle is true, implementation generates static HTML without client script
988
+ expect(html).toBeDefined();
989
+ expect(html).toContain("<div");
990
+ });
991
+
992
+ test("should include viewport meta tag", async () => {
993
+ const pageData: JSONPage = {
994
+ root: {
995
+ type: "node",
996
+ tag: "div"
997
+ }
998
+ };
999
+
1000
+ const html = await generateSSRHTML(pageData);
1001
+
1002
+ expect(html).toContain('name="viewport"');
1003
+ expect(html).toContain('content="width=device-width, initial-scale=1.0"');
1004
+ });
1005
+ });