meno-core 1.0.53 → 1.1.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 (415) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +66 -261
  3. package/build-astro.ts +150 -128
  4. package/dist/bin/cli.js +42 -185
  5. package/dist/bin/cli.js.map +2 -2
  6. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  7. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  8. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
  9. package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
  10. package/dist/chunks/chunk-4ZRU52J2.js +169 -0
  11. package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
  12. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
  13. package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
  14. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
  15. package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
  16. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
  17. package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
  18. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
  19. package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
  20. package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
  21. package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
  22. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
  23. package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
  24. package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
  25. package/dist/lib/client/index.js +1003 -475
  26. package/dist/lib/client/index.js.map +3 -3
  27. package/dist/lib/server/index.js +14393 -6371
  28. package/dist/lib/server/index.js.map +4 -4
  29. package/dist/lib/shared/index.js +342 -101
  30. package/dist/lib/shared/index.js.map +4 -4
  31. package/dist/lib/shared/richtext/index.js +1 -1
  32. package/dist/lib/test-utils/index.js +64 -80
  33. package/dist/lib/test-utils/index.js.map +2 -2
  34. package/entries/client-router.tsx +10 -8
  35. package/lib/client/ClientInitializer.ts +8 -8
  36. package/lib/client/ErrorBoundary.test.tsx +246 -186
  37. package/lib/client/ErrorBoundary.tsx +183 -132
  38. package/lib/client/componentRegistry.test.ts +96 -108
  39. package/lib/client/componentRegistry.ts +1 -3
  40. package/lib/client/contexts/ThemeContext.tsx +3 -2
  41. package/lib/client/core/ComponentBuilder.test.ts +663 -560
  42. package/lib/client/core/ComponentBuilder.ts +427 -155
  43. package/lib/client/core/ComponentRenderer.test.tsx +2 -4
  44. package/lib/client/core/ComponentRenderer.tsx +46 -33
  45. package/lib/client/core/builders/embedBuilder.ts +242 -54
  46. package/lib/client/core/builders/linkBuilder.ts +72 -45
  47. package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
  48. package/lib/client/core/builders/listBuilder.ts +155 -89
  49. package/lib/client/core/builders/localeListBuilder.ts +97 -62
  50. package/lib/client/core/builders/types.ts +5 -5
  51. package/lib/client/core/cmsTemplateProcessor.ts +8 -9
  52. package/lib/client/elementRegistry.ts +3 -3
  53. package/lib/client/fontFamiliesService.test.ts +2 -10
  54. package/lib/client/fontFamiliesService.ts +3 -3
  55. package/lib/client/hmr/HMRManager.tsx +47 -30
  56. package/lib/client/hmrCssReload.ts +49 -14
  57. package/lib/client/hmrWebSocket.ts +14 -19
  58. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  59. package/lib/client/hooks/useColorVariables.ts +23 -18
  60. package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
  61. package/lib/client/hooks/useVariables.ts +6 -4
  62. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  63. package/lib/client/hydration/HydrationUtils.ts +3 -4
  64. package/lib/client/i18nConfigService.test.ts +2 -7
  65. package/lib/client/i18nConfigService.ts +2 -2
  66. package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
  67. package/lib/client/meno-filter/MenoFilter.ts +14 -15
  68. package/lib/client/meno-filter/bindings.ts +18 -43
  69. package/lib/client/meno-filter/init.ts +4 -4
  70. package/lib/client/meno-filter/renderer.ts +30 -32
  71. package/lib/client/meno-filter/script.generated.ts +1 -3
  72. package/lib/client/meno-filter/ui.ts +4 -6
  73. package/lib/client/meno-filter/updates.ts +18 -23
  74. package/lib/client/meno-filter/utils.ts +1 -1
  75. package/lib/client/navigation.test.ts +174 -178
  76. package/lib/client/navigation.ts +0 -1
  77. package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
  78. package/lib/client/responsiveStyleResolver.test.ts +234 -232
  79. package/lib/client/responsiveStyleResolver.ts +74 -20
  80. package/lib/client/routing/RouteLoader.test.ts +24 -25
  81. package/lib/client/routing/RouteLoader.ts +27 -35
  82. package/lib/client/routing/Router.tsx +210 -153
  83. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  84. package/lib/client/scripts/ScriptExecutor.ts +18 -36
  85. package/lib/client/services/PrefetchService.test.ts +3 -3
  86. package/lib/client/services/PrefetchService.ts +23 -29
  87. package/lib/client/styleProcessor.test.ts +9 -9
  88. package/lib/client/styleProcessor.ts +24 -15
  89. package/lib/client/styles/StyleInjector.test.ts +122 -115
  90. package/lib/client/styles/StyleInjector.ts +10 -7
  91. package/lib/client/styles/UtilityClassCollector.ts +62 -28
  92. package/lib/client/styles/cspNonce.test.ts +2 -5
  93. package/lib/client/templateEngine.test.ts +554 -488
  94. package/lib/client/templateEngine.ts +203 -73
  95. package/lib/client/theme.test.ts +1 -1
  96. package/lib/client/theme.ts +0 -1
  97. package/lib/client/utils/toast.ts +0 -1
  98. package/lib/server/__integration__/api-routes.test.ts +8 -4
  99. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  100. package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
  101. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  102. package/lib/server/__integration__/static-assets.test.ts +3 -3
  103. package/lib/server/__integration__/test-helpers.ts +87 -74
  104. package/lib/server/ab/generateFunctions.ts +12 -10
  105. package/lib/server/astro/cmsPageEmitter.ts +26 -42
  106. package/lib/server/astro/componentEmitter.ts +35 -43
  107. package/lib/server/astro/cssCollector.ts +10 -26
  108. package/lib/server/astro/nodeToAstro.test.ts +1754 -34
  109. package/lib/server/astro/nodeToAstro.ts +232 -216
  110. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
  111. package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
  112. package/lib/server/astro/pageEmitter.ts +9 -13
  113. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  114. package/lib/server/astro/tailwindMapper.ts +33 -40
  115. package/lib/server/astro/templateTransformer.ts +17 -20
  116. package/lib/server/createServer.ts +7 -19
  117. package/lib/server/cssGenerator.test.ts +35 -44
  118. package/lib/server/cssGenerator.ts +8 -19
  119. package/lib/server/fileWatcher.test.ts +164 -13
  120. package/lib/server/fileWatcher.ts +209 -124
  121. package/lib/server/index.ts +14 -17
  122. package/lib/server/jsonLoader.test.ts +39 -2
  123. package/lib/server/jsonLoader.ts +39 -34
  124. package/lib/server/middleware/cors.test.ts +21 -21
  125. package/lib/server/middleware/cors.ts +28 -4
  126. package/lib/server/middleware/errorHandler.test.ts +7 -5
  127. package/lib/server/middleware/errorHandler.ts +3 -8
  128. package/lib/server/middleware/index.ts +0 -1
  129. package/lib/server/middleware/logger.test.ts +10 -8
  130. package/lib/server/middleware/logger.ts +17 -29
  131. package/lib/server/migrateTemplates.ts +3 -3
  132. package/lib/server/pageCache.test.ts +76 -77
  133. package/lib/server/pageCache.ts +0 -1
  134. package/lib/server/projectContext.ts +5 -4
  135. package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
  136. package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
  137. package/lib/server/routes/api/cms.test.ts +26 -14
  138. package/lib/server/routes/api/cms.ts +9 -14
  139. package/lib/server/routes/api/colors.test.ts +2 -2
  140. package/lib/server/routes/api/components.ts +31 -35
  141. package/lib/server/routes/api/config.ts +3 -4
  142. package/lib/server/routes/api/core-routes.ts +49 -76
  143. package/lib/server/routes/api/enums.test.ts +2 -2
  144. package/lib/server/routes/api/functions.ts +9 -17
  145. package/lib/server/routes/api/index.ts +3 -6
  146. package/lib/server/routes/api/pages.ts +8 -11
  147. package/lib/server/routes/api/shared.test.ts +1 -1
  148. package/lib/server/routes/api/shared.ts +12 -9
  149. package/lib/server/routes/api/variables.test.ts +3 -5
  150. package/lib/server/routes/api/variables.ts +1 -1
  151. package/lib/server/routes/index.ts +25 -28
  152. package/lib/server/routes/pages.ts +35 -31
  153. package/lib/server/routes/static.ts +17 -6
  154. package/lib/server/runtime/bundler.ts +98 -61
  155. package/lib/server/runtime/fs.ts +16 -26
  156. package/lib/server/runtime/httpServer.ts +8 -12
  157. package/lib/server/services/ColorService.test.ts +3 -3
  158. package/lib/server/services/ColorService.ts +37 -29
  159. package/lib/server/services/EnumService.test.ts +4 -8
  160. package/lib/server/services/EnumService.ts +11 -3
  161. package/lib/server/services/VariableService.test.ts +8 -12
  162. package/lib/server/services/VariableService.ts +11 -3
  163. package/lib/server/services/cmsService.test.ts +158 -120
  164. package/lib/server/services/cmsService.ts +36 -146
  165. package/lib/server/services/componentService.test.ts +125 -45
  166. package/lib/server/services/componentService.ts +70 -46
  167. package/lib/server/services/configService.test.ts +22 -45
  168. package/lib/server/services/configService.ts +50 -27
  169. package/lib/server/services/fileWatcherService.ts +42 -8
  170. package/lib/server/services/index.ts +0 -1
  171. package/lib/server/services/pageService.test.ts +8 -10
  172. package/lib/server/services/pageService.ts +32 -18
  173. package/lib/server/ssr/attributeBuilder.ts +18 -8
  174. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  175. package/lib/server/ssr/clientDataInjector.ts +7 -21
  176. package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
  177. package/lib/server/ssr/cssCollector.ts +1 -1
  178. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  179. package/lib/server/ssr/errorOverlay.ts +3 -9
  180. package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
  181. package/lib/server/ssr/htmlGenerator.test.ts +120 -44
  182. package/lib/server/ssr/htmlGenerator.ts +126 -91
  183. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  184. package/lib/server/ssr/imageMetadata.ts +27 -21
  185. package/lib/server/ssr/jsCollector.test.ts +8 -18
  186. package/lib/server/ssr/jsCollector.ts +3 -8
  187. package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
  188. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  189. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  190. package/lib/server/ssr/ssrRenderer.test.ts +316 -256
  191. package/lib/server/ssr/ssrRenderer.ts +795 -243
  192. package/lib/server/ssrRenderer.test.ts +1048 -954
  193. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  194. package/lib/server/utils/jsonLineMapper.ts +7 -7
  195. package/lib/server/validateStyleCoverage.ts +18 -21
  196. package/lib/server/websocketManager.ts +28 -19
  197. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  198. package/lib/shared/attributeNodeUtils.ts +5 -12
  199. package/lib/shared/breakpoints.test.ts +5 -5
  200. package/lib/shared/breakpoints.ts +4 -11
  201. package/lib/shared/cmsQuery.test.ts +81 -0
  202. package/lib/shared/cmsQuery.ts +92 -0
  203. package/lib/shared/cmsQueryParser.test.ts +50 -42
  204. package/lib/shared/cmsQueryParser.ts +60 -38
  205. package/lib/shared/colorConversions.ts +9 -9
  206. package/lib/shared/colorVariableUtils.test.ts +27 -5
  207. package/lib/shared/colorVariableUtils.ts +21 -12
  208. package/lib/shared/componentRefs.ts +1 -5
  209. package/lib/shared/constants.test.ts +3 -3
  210. package/lib/shared/constants.ts +8 -8
  211. package/lib/shared/cssGeneration.test.ts +285 -144
  212. package/lib/shared/cssGeneration.ts +217 -531
  213. package/lib/shared/cssNamedColors.ts +152 -30
  214. package/lib/shared/cssProperties.test.ts +69 -70
  215. package/lib/shared/cssProperties.ts +298 -114
  216. package/lib/shared/elementClassName.test.ts +109 -109
  217. package/lib/shared/elementClassName.ts +3 -3
  218. package/lib/shared/elementUtils.ts +12 -16
  219. package/lib/shared/errorLogger.test.ts +12 -12
  220. package/lib/shared/errorLogger.ts +2 -10
  221. package/lib/shared/errors.test.ts +2 -13
  222. package/lib/shared/errors.ts +2 -8
  223. package/lib/shared/expressionEvaluator.test.ts +145 -0
  224. package/lib/shared/expressionEvaluator.ts +52 -24
  225. package/lib/shared/fontLoader.test.ts +32 -18
  226. package/lib/shared/fontLoader.ts +2 -2
  227. package/lib/shared/friendlyError.ts +2 -3
  228. package/lib/shared/gradientUtils.test.ts +3 -7
  229. package/lib/shared/gradientUtils.ts +17 -14
  230. package/lib/shared/hrefRefs.test.ts +2 -2
  231. package/lib/shared/hrefRefs.ts +2 -10
  232. package/lib/shared/i18n.test.ts +4 -4
  233. package/lib/shared/i18n.ts +18 -38
  234. package/lib/shared/index.ts +27 -0
  235. package/lib/shared/inlineSvgStyleRules.ts +14 -13
  236. package/lib/shared/interactiveStyleMappings.test.ts +32 -54
  237. package/lib/shared/interactiveStyleMappings.ts +11 -24
  238. package/lib/shared/interactiveStyles.test.ts +165 -188
  239. package/lib/shared/interfaces/contentProvider.ts +1 -1
  240. package/lib/shared/itemTemplateUtils.test.ts +11 -17
  241. package/lib/shared/itemTemplateUtils.ts +27 -39
  242. package/lib/shared/jsonRepair.ts +16 -10
  243. package/lib/shared/libraryLoader.test.ts +21 -55
  244. package/lib/shared/libraryLoader.ts +7 -22
  245. package/lib/shared/linkUtils.ts +1 -1
  246. package/lib/shared/logger.test.ts +66 -0
  247. package/lib/shared/logger.ts +94 -0
  248. package/lib/shared/markdown.ts +25 -0
  249. package/lib/shared/netlifyLocale404.test.ts +179 -0
  250. package/lib/shared/netlifyLocale404.ts +110 -0
  251. package/lib/shared/nodeUtils.test.ts +28 -16
  252. package/lib/shared/nodeUtils.ts +76 -23
  253. package/lib/shared/pathArrayUtils.test.ts +1 -2
  254. package/lib/shared/pathArrayUtils.ts +1 -1
  255. package/lib/shared/pathSecurity.ts +2 -2
  256. package/lib/shared/pathUtils.test.ts +4 -6
  257. package/lib/shared/pathUtils.ts +42 -49
  258. package/lib/shared/paths/Path.test.ts +2 -2
  259. package/lib/shared/paths/Path.ts +0 -1
  260. package/lib/shared/paths/PathConverter.test.ts +1 -1
  261. package/lib/shared/paths/PathConverter.ts +17 -20
  262. package/lib/shared/paths/PathUtils.ts +13 -13
  263. package/lib/shared/paths/PathValidator.test.ts +2 -15
  264. package/lib/shared/paths/PathValidator.ts +12 -10
  265. package/lib/shared/paths/index.ts +1 -2
  266. package/lib/shared/permissions.test.ts +168 -0
  267. package/lib/shared/permissions.ts +162 -0
  268. package/lib/shared/propResolver.test.ts +240 -244
  269. package/lib/shared/propResolver.ts +15 -26
  270. package/lib/shared/pxToRem.test.ts +7 -6
  271. package/lib/shared/pxToRem.ts +2 -5
  272. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
  273. package/lib/shared/registry/ClientRegistry.ts +0 -2
  274. package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
  275. package/lib/shared/registry/ComponentRegistry.ts +12 -9
  276. package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
  277. package/lib/shared/registry/RegistryManager.ts +1 -2
  278. package/lib/shared/registry/SSRRegistry.ts +0 -2
  279. package/lib/shared/registry/createNodeType.ts +7 -9
  280. package/lib/shared/registry/defineNodeType.ts +2 -6
  281. package/lib/shared/registry/fieldPresets.ts +3 -1
  282. package/lib/shared/registry/index.ts +0 -1
  283. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  284. package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
  285. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  286. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
  287. package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
  288. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  289. package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
  290. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  291. package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
  292. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
  293. package/lib/shared/registry/nodeTypes/index.ts +26 -3
  294. package/lib/shared/responsiveScaling.test.ts +16 -34
  295. package/lib/shared/responsiveScaling.ts +61 -42
  296. package/lib/shared/responsiveStyleUtils.test.ts +0 -1
  297. package/lib/shared/responsiveStyleUtils.ts +11 -13
  298. package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
  299. package/lib/shared/richtext/htmlToTiptap.ts +2 -4
  300. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  301. package/lib/shared/richtext/types.ts +1 -8
  302. package/lib/shared/slugTranslator.test.ts +13 -13
  303. package/lib/shared/slugTranslator.ts +12 -16
  304. package/lib/shared/slugify.ts +9 -15
  305. package/lib/shared/styleNodeUtils.test.ts +8 -8
  306. package/lib/shared/styleNodeUtils.ts +18 -20
  307. package/lib/shared/styleUtils.test.ts +87 -61
  308. package/lib/shared/styleUtils.ts +5 -6
  309. package/lib/shared/themeDefaults.test.ts +11 -14
  310. package/lib/shared/themeDefaults.ts +3 -4
  311. package/lib/shared/tree/PathBuilder.test.ts +63 -145
  312. package/lib/shared/tree/PathBuilder.ts +59 -47
  313. package/lib/shared/treePathUtils.test.ts +51 -12
  314. package/lib/shared/treePathUtils.ts +91 -60
  315. package/lib/shared/types/api.ts +105 -8
  316. package/lib/shared/types/cms.test.ts +34 -0
  317. package/lib/shared/types/cms.ts +90 -34
  318. package/lib/shared/types/comment.ts +49 -16
  319. package/lib/shared/types/components.ts +51 -25
  320. package/lib/shared/types/errors.test.ts +1 -6
  321. package/lib/shared/types/errors.ts +3 -7
  322. package/lib/shared/types/experiments.ts +28 -28
  323. package/lib/shared/types/index.ts +25 -2
  324. package/lib/shared/types/permissions.ts +139 -0
  325. package/lib/shared/types/styles.ts +0 -1
  326. package/lib/shared/types/variables.test.ts +4 -13
  327. package/lib/shared/types/variables.ts +51 -30
  328. package/lib/shared/types.ts +1 -2
  329. package/lib/shared/utilityClassConfig.ts +751 -321
  330. package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
  331. package/lib/shared/utilityClassMapper.test.ts +433 -95
  332. package/lib/shared/utilityClassMapper.ts +322 -249
  333. package/lib/shared/utilityClassNames.test.ts +38 -0
  334. package/lib/shared/utilityClassNames.ts +458 -0
  335. package/lib/shared/utils.test.ts +2 -10
  336. package/lib/shared/utils.ts +19 -10
  337. package/lib/shared/validation/cmsValidators.ts +2 -1
  338. package/lib/shared/validation/commentValidators.test.ts +53 -0
  339. package/lib/shared/validation/commentValidators.ts +12 -1
  340. package/lib/shared/validation/index.ts +1 -0
  341. package/lib/shared/validation/permissionsValidators.test.ts +49 -0
  342. package/lib/shared/validation/permissionsValidators.ts +49 -0
  343. package/lib/shared/validation/propValidator.test.ts +19 -21
  344. package/lib/shared/validation/propValidator.ts +13 -18
  345. package/lib/shared/validation/schemas.test.ts +64 -32
  346. package/lib/shared/validation/schemas.ts +616 -345
  347. package/lib/shared/validation/validators.test.ts +3 -8
  348. package/lib/shared/validation/validators.ts +89 -68
  349. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  350. package/lib/shared/viewportUnits.test.ts +24 -23
  351. package/lib/shared/viewportUnits.ts +12 -5
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
  354. package/lib/test-utils/factories/DomMockFactory.ts +17 -23
  355. package/lib/test-utils/factories/EventMockFactory.ts +7 -13
  356. package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
  357. package/lib/test-utils/factories/ServerMockFactory.ts +6 -10
  358. package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
  359. package/lib/test-utils/fixtures.ts +45 -45
  360. package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
  361. package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
  362. package/lib/test-utils/helpers.ts +1 -5
  363. package/lib/test-utils/index.ts +0 -4
  364. package/lib/test-utils/mockFactories.ts +13 -19
  365. package/lib/test-utils/mocks.ts +6 -4
  366. package/package.json +3 -5
  367. package/scripts/build-for-publish.mjs +2 -4
  368. package/scripts/build-meno-filter.ts +4 -7
  369. package/tsconfig.json +1 -0
  370. package/vite.config.ts +4 -4
  371. package/build-next.ts +0 -1361
  372. package/build-static.test.ts +0 -424
  373. package/build-static.ts +0 -1074
  374. package/dist/build-static.js +0 -36
  375. package/dist/chunks/chunk-2MHDV5BF.js +0 -251
  376. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  377. package/dist/chunks/chunk-AZQYF6KE.js +0 -559
  378. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  379. package/dist/chunks/chunk-GZHGVVW3.js +0 -322
  380. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  381. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  382. package/dist/chunks/chunk-I7YIGZXT.js +0 -467
  383. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  384. package/dist/chunks/chunk-IGYR22T6.js +0 -6192
  385. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  386. package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
  387. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  388. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  389. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  390. package/dist/chunks/chunk-QB2LNO4W.js +0 -77
  391. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  392. package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
  393. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  394. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  395. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  396. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  397. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  398. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  399. package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
  400. package/dist/chunks/constants-STK2YBIW.js +0 -46
  401. package/dist/chunks/constants-STK2YBIW.js.map +0 -7
  402. package/dist/chunks/fs-JGINUXGL.js.map +0 -7
  403. package/dist/entries/server-router.js +0 -67
  404. package/dist/entries/server-router.js.map +0 -7
  405. package/entries/server-router.tsx +0 -81
  406. package/lib/server/providers/fileSystemPageProvider.ts +0 -160
  407. package/lib/server/webflow/buildWebflow.ts +0 -635
  408. package/lib/server/webflow/index.ts +0 -23
  409. package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
  410. package/lib/server/webflow/nodeToWebflow.ts +0 -2435
  411. package/lib/server/webflow/styleMapper.test.ts +0 -389
  412. package/lib/server/webflow/styleMapper.ts +0 -695
  413. package/lib/server/webflow/templateWrapper.ts +0 -49
  414. package/lib/server/webflow/types.ts +0 -396
  415. /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
@@ -1,141 +1,136 @@
1
- import { test, expect, describe, beforeEach, afterEach, mock, spyOn } from "bun:test";
2
- import {
3
- generateSSRHTML,
4
- renderPageSSR,
5
- extractPageMeta,
6
- generateMetaTags
7
- } from "./ssrRenderer";
8
- import type { ComponentNode, ComponentDefinition, JSONPage, CMSListNode, CMSItem } from "../shared/types";
9
- import { CMSService } from "./services/cmsService";
1
+ import { test, expect, describe, spyOn } from 'bun:test';
2
+ import { generateSSRHTML, renderPageSSR, extractPageMeta, generateMetaTags } from './ssrRenderer';
3
+ import type { ComponentNode, ComponentDefinition, JSONPage, CMSItem } from '../shared/types';
4
+ import { CMSService } from './services/cmsService';
10
5
 
11
6
  // Mock jsonLoader
12
- const mockLoadBreakpointConfig = async () => ({
7
+ const _mockLoadBreakpointConfig = async () => ({
13
8
  tablet: 1024,
14
- mobile: 540
9
+ mobile: 540,
15
10
  });
16
11
 
17
12
  // Mock the jsonLoader module
18
- const originalJsonLoader = await import("./jsonLoader");
13
+ const originalJsonLoader = await import('./jsonLoader');
19
14
  if (originalJsonLoader) {
20
15
  // We'll need to test with actual imports, but can provide mocks where needed
21
16
  }
22
17
 
23
- describe("SSR Renderer - extractPageMeta", () => {
24
- test("should extract basic meta information", () => {
18
+ describe('SSR Renderer - extractPageMeta', () => {
19
+ test('should extract basic meta information', () => {
25
20
  const pageData: JSONPage = {
26
21
  meta: {
27
- title: "Test Page",
28
- description: "Test description",
29
- keywords: "test, page"
30
- }
22
+ title: 'Test Page',
23
+ description: 'Test description',
24
+ keywords: 'test, page',
25
+ },
31
26
  };
32
-
27
+
33
28
  const meta = extractPageMeta(pageData);
34
-
35
- expect(meta.title).toBe("Test Page");
36
- expect(meta.description).toBe("Test description");
37
- expect(meta.keywords).toBe("test, page");
29
+
30
+ expect(meta.title).toBe('Test Page');
31
+ expect(meta.description).toBe('Test description');
32
+ expect(meta.keywords).toBe('test, page');
38
33
  });
39
34
 
40
- test("should extract Open Graph meta", () => {
35
+ test('should extract Open Graph meta', () => {
41
36
  const pageData: JSONPage = {
42
37
  meta: {
43
- title: "Test Page",
44
- ogTitle: "OG Title",
45
- ogDescription: "OG Description",
46
- ogImage: "https://example.com/image.jpg",
47
- ogType: "article"
48
- }
38
+ title: 'Test Page',
39
+ ogTitle: 'OG Title',
40
+ ogDescription: 'OG Description',
41
+ ogImage: 'https://example.com/image.jpg',
42
+ ogType: 'article',
43
+ },
49
44
  };
50
-
45
+
51
46
  const meta = extractPageMeta(pageData);
52
-
53
- expect(meta.ogTitle).toBe("OG Title");
54
- expect(meta.ogDescription).toBe("OG Description");
55
- expect(meta.ogImage).toBe("https://example.com/image.jpg");
56
- expect(meta.ogType).toBe("article");
47
+
48
+ expect(meta.ogTitle).toBe('OG Title');
49
+ expect(meta.ogDescription).toBe('OG Description');
50
+ expect(meta.ogImage).toBe('https://example.com/image.jpg');
51
+ expect(meta.ogType).toBe('article');
57
52
  });
58
53
 
59
- test("should use title as fallback for ogTitle", () => {
54
+ test('should use title as fallback for ogTitle', () => {
60
55
  const pageData: JSONPage = {
61
56
  meta: {
62
- title: "Test Page"
63
- }
57
+ title: 'Test Page',
58
+ },
64
59
  };
65
-
60
+
66
61
  const meta = extractPageMeta(pageData);
67
-
68
- expect(meta.ogTitle).toBe("Test Page");
62
+
63
+ expect(meta.ogTitle).toBe('Test Page');
69
64
  });
70
65
 
71
- test("should use description as fallback for ogDescription", () => {
66
+ test('should use description as fallback for ogDescription', () => {
72
67
  const pageData: JSONPage = {
73
68
  meta: {
74
- description: "Test description"
75
- }
69
+ description: 'Test description',
70
+ },
76
71
  };
77
-
72
+
78
73
  const meta = extractPageMeta(pageData);
79
-
80
- expect(meta.ogDescription).toBe("Test description");
74
+
75
+ expect(meta.ogDescription).toBe('Test description');
81
76
  });
82
77
 
83
78
  test("should default ogType to 'website'", () => {
84
79
  const pageData: JSONPage = {
85
- meta: {}
80
+ meta: {},
86
81
  };
87
-
82
+
88
83
  const meta = extractPageMeta(pageData);
89
-
90
- expect(meta.ogType).toBe("website");
84
+
85
+ expect(meta.ogType).toBe('website');
91
86
  });
92
87
 
93
- test("should handle missing meta object", () => {
88
+ test('should handle missing meta object', () => {
94
89
  const pageData: JSONPage = {};
95
-
90
+
96
91
  const meta = extractPageMeta(pageData);
97
-
92
+
98
93
  expect(Object.keys(meta).length).toBe(0);
99
94
  });
100
95
  });
101
96
 
102
- describe("SSR Renderer - generateMetaTags", () => {
103
- test("should generate title tag", () => {
97
+ describe('SSR Renderer - generateMetaTags', () => {
98
+ test('should generate title tag', () => {
104
99
  const meta = {
105
- title: "Test Page"
100
+ title: 'Test Page',
106
101
  };
107
-
102
+
108
103
  const tags = generateMetaTags(meta);
109
-
110
- expect(tags).toContain("<title>Test Page</title>");
104
+
105
+ expect(tags).toContain('<title>Test Page</title>');
111
106
  });
112
107
 
113
- test("should generate description meta tag", () => {
108
+ test('should generate description meta tag', () => {
114
109
  const meta = {
115
- description: "Test description"
110
+ description: 'Test description',
116
111
  };
117
-
112
+
118
113
  const tags = generateMetaTags(meta);
119
-
114
+
120
115
  expect(tags).toContain(`<meta name="description" content="Test description" />`);
121
116
  });
122
117
 
123
- test("should generate keywords meta tag", () => {
118
+ test('should generate keywords meta tag', () => {
124
119
  const meta = {
125
- keywords: "test, keywords"
120
+ keywords: 'test, keywords',
126
121
  };
127
-
122
+
128
123
  const tags = generateMetaTags(meta);
129
-
124
+
130
125
  expect(tags).toContain(`<meta name="keywords" content="test, keywords" />`);
131
126
  });
132
127
 
133
- test("should generate Open Graph tags", () => {
128
+ test('should generate Open Graph tags', () => {
134
129
  const meta = {
135
- ogTitle: "OG Title",
136
- ogDescription: "OG Description",
137
- ogImage: "https://example.com/image.jpg",
138
- ogType: "article"
130
+ ogTitle: 'OG Title',
131
+ ogDescription: 'OG Description',
132
+ ogImage: 'https://example.com/image.jpg',
133
+ ogType: 'article',
139
134
  };
140
135
 
141
136
  const tags = generateMetaTags(meta);
@@ -146,11 +141,11 @@ describe("SSR Renderer - generateMetaTags", () => {
146
141
  expect(tags).toContain(`<meta property="og:type" content="article" />`);
147
142
  });
148
143
 
149
- test("should emit twitter:card=summary_large_image when ogImage present", () => {
144
+ test('should emit twitter:card=summary_large_image when ogImage present', () => {
150
145
  const meta = {
151
- ogTitle: "OG Title",
152
- ogDescription: "OG Description",
153
- ogImage: "https://example.com/image.jpg",
146
+ ogTitle: 'OG Title',
147
+ ogDescription: 'OG Description',
148
+ ogImage: 'https://example.com/image.jpg',
154
149
  };
155
150
 
156
151
  const tags = generateMetaTags(meta);
@@ -160,10 +155,10 @@ describe("SSR Renderer - generateMetaTags", () => {
160
155
  expect(tags).toContain(`<meta name="twitter:description" content="OG Description" />`);
161
156
  });
162
157
 
163
- test("should emit twitter:card=summary when no ogImage", () => {
158
+ test('should emit twitter:card=summary when no ogImage', () => {
164
159
  const meta = {
165
- title: "Plain Title",
166
- description: "Plain Description",
160
+ title: 'Plain Title',
161
+ description: 'Plain Description',
167
162
  };
168
163
 
169
164
  const tags = generateMetaTags(meta);
@@ -171,10 +166,10 @@ describe("SSR Renderer - generateMetaTags", () => {
171
166
  expect(tags).toContain(`<meta name="twitter:card" content="summary" />`);
172
167
  });
173
168
 
174
- test("twitter:title falls back to title when ogTitle is absent", () => {
169
+ test('twitter:title falls back to title when ogTitle is absent', () => {
175
170
  const meta = {
176
- title: "Plain Title",
177
- ogImage: "https://example.com/image.jpg",
171
+ title: 'Plain Title',
172
+ ogImage: 'https://example.com/image.jpg',
178
173
  };
179
174
 
180
175
  const tags = generateMetaTags(meta);
@@ -182,10 +177,10 @@ describe("SSR Renderer - generateMetaTags", () => {
182
177
  expect(tags).toContain(`<meta name="twitter:title" content="Plain Title" />`);
183
178
  });
184
179
 
185
- test("twitter:description falls back to description when ogDescription is absent", () => {
180
+ test('twitter:description falls back to description when ogDescription is absent', () => {
186
181
  const meta = {
187
- description: "Plain Description",
188
- ogImage: "https://example.com/image.jpg",
182
+ description: 'Plain Description',
183
+ ogImage: 'https://example.com/image.jpg',
189
184
  };
190
185
 
191
186
  const tags = generateMetaTags(meta);
@@ -193,8 +188,8 @@ describe("SSR Renderer - generateMetaTags", () => {
193
188
  expect(tags).toContain(`<meta name="twitter:description" content="Plain Description" />`);
194
189
  });
195
190
 
196
- test("should emit twitter:site and twitter:creator with @-normalized handle", () => {
197
- const meta = { title: "Page" };
191
+ test('should emit twitter:site and twitter:creator with @-normalized handle', () => {
192
+ const meta = { title: 'Page' };
198
193
 
199
194
  const tags = generateMetaTags(meta, '', 'en', undefined, {
200
195
  social: { twitterHandle: 'meno' },
@@ -204,8 +199,8 @@ describe("SSR Renderer - generateMetaTags", () => {
204
199
  expect(tags).toContain(`<meta name="twitter:creator" content="@meno" />`);
205
200
  });
206
201
 
207
- test("should not double-prefix handle that already starts with @", () => {
208
- const meta = { title: "Page" };
202
+ test('should not double-prefix handle that already starts with @', () => {
203
+ const meta = { title: 'Page' };
209
204
 
210
205
  const tags = generateMetaTags(meta, '', 'en', undefined, {
211
206
  social: { twitterHandle: '@meno' },
@@ -215,15 +210,15 @@ describe("SSR Renderer - generateMetaTags", () => {
215
210
  expect(tags).not.toContain(`@@meno`);
216
211
  });
217
212
 
218
- test("should emit no twitter tags when meta is empty", () => {
213
+ test('should emit no twitter tags when meta is empty', () => {
219
214
  const tags = generateMetaTags({});
220
215
 
221
216
  expect(tags).not.toContain('twitter:');
222
217
  });
223
218
 
224
- test("should escape HTML in twitter content", () => {
219
+ test('should escape HTML in twitter content', () => {
225
220
  const meta = {
226
- ogTitle: "Title & <stuff>",
221
+ ogTitle: 'Title & <stuff>',
227
222
  };
228
223
 
229
224
  const tags = generateMetaTags(meta);
@@ -231,24 +226,24 @@ describe("SSR Renderer - generateMetaTags", () => {
231
226
  expect(tags).toContain(`<meta name="twitter:title" content="Title &amp; &lt;stuff&gt;" />`);
232
227
  });
233
228
 
234
- test("og:image swaps /images/*.webp → .jpg", () => {
235
- const meta = { ogImage: "/images/photo.webp" };
229
+ test('og:image swaps /images/*.webp → .jpg', () => {
230
+ const meta = { ogImage: '/images/photo.webp' };
236
231
 
237
232
  const tags = generateMetaTags(meta);
238
233
 
239
234
  expect(tags).toContain(`<meta property="og:image" content="/images/photo.jpg" />`);
240
235
  });
241
236
 
242
- test("og:image swaps /images/*.avif → .jpg", () => {
243
- const meta = { ogImage: "/images/photo.avif" };
237
+ test('og:image swaps /images/*.avif → .jpg', () => {
238
+ const meta = { ogImage: '/images/photo.avif' };
244
239
 
245
240
  const tags = generateMetaTags(meta);
246
241
 
247
242
  expect(tags).toContain(`<meta property="og:image" content="/images/photo.jpg" />`);
248
243
  });
249
244
 
250
- test("og:image is made absolute when options.baseUrl is provided", () => {
251
- const meta = { ogImage: "/images/photo.webp" };
245
+ test('og:image is made absolute when options.baseUrl is provided', () => {
246
+ const meta = { ogImage: '/images/photo.webp' };
252
247
 
253
248
  const tags = generateMetaTags(meta, '', 'en', undefined, {
254
249
  baseUrl: 'https://example.com',
@@ -257,8 +252,8 @@ describe("SSR Renderer - generateMetaTags", () => {
257
252
  expect(tags).toContain(`<meta property="og:image" content="https://example.com/images/photo.jpg" />`);
258
253
  });
259
254
 
260
- test("og:image strips trailing slash from baseUrl", () => {
261
- const meta = { ogImage: "/images/photo.webp" };
255
+ test('og:image strips trailing slash from baseUrl', () => {
256
+ const meta = { ogImage: '/images/photo.webp' };
262
257
 
263
258
  const tags = generateMetaTags(meta, '', 'en', undefined, {
264
259
  baseUrl: 'https://example.com/',
@@ -267,32 +262,32 @@ describe("SSR Renderer - generateMetaTags", () => {
267
262
  expect(tags).toContain(`<meta property="og:image" content="https://example.com/images/photo.jpg" />`);
268
263
  });
269
264
 
270
- test("og:image preserves .png unchanged (other than baseUrl absolutization)", () => {
271
- const meta = { ogImage: "/images/photo.png" };
265
+ test('og:image preserves .png unchanged (other than baseUrl absolutization)', () => {
266
+ const meta = { ogImage: '/images/photo.png' };
272
267
 
273
268
  const tags = generateMetaTags(meta);
274
269
 
275
270
  expect(tags).toContain(`<meta property="og:image" content="/images/photo.png" />`);
276
271
  });
277
272
 
278
- test("og:image preserves .jpg unchanged", () => {
279
- const meta = { ogImage: "/images/photo.jpg" };
273
+ test('og:image preserves .jpg unchanged', () => {
274
+ const meta = { ogImage: '/images/photo.jpg' };
280
275
 
281
276
  const tags = generateMetaTags(meta);
282
277
 
283
278
  expect(tags).toContain(`<meta property="og:image" content="/images/photo.jpg" />`);
284
279
  });
285
280
 
286
- test("og:image preserves external absolute URL unchanged", () => {
287
- const meta = { ogImage: "https://cdn.example.com/photo.webp" };
281
+ test('og:image preserves external absolute URL unchanged', () => {
282
+ const meta = { ogImage: 'https://cdn.example.com/photo.webp' };
288
283
 
289
284
  const tags = generateMetaTags(meta);
290
285
 
291
286
  expect(tags).toContain(`<meta property="og:image" content="https://cdn.example.com/photo.webp" />`);
292
287
  });
293
288
 
294
- test("og:image does not rewrite non-/images/ local paths but still absolutizes them", () => {
295
- const meta = { ogImage: "/cdn-cgi/photo.webp" };
289
+ test('og:image does not rewrite non-/images/ local paths but still absolutizes them', () => {
290
+ const meta = { ogImage: '/cdn-cgi/photo.webp' };
296
291
 
297
292
  const tags = generateMetaTags(meta, '', 'en', undefined, {
298
293
  baseUrl: 'https://example.com',
@@ -301,38 +296,38 @@ describe("SSR Renderer - generateMetaTags", () => {
301
296
  expect(tags).toContain(`<meta property="og:image" content="https://example.com/cdn-cgi/photo.webp" />`);
302
297
  });
303
298
 
304
- test("should generate canonical URL", () => {
299
+ test('should generate canonical URL', () => {
305
300
  const meta = {};
306
- const url = "https://example.com/page";
307
-
301
+ const url = 'https://example.com/page';
302
+
308
303
  const tags = generateMetaTags(meta, url);
309
-
304
+
310
305
  expect(tags).toContain(`<link rel="canonical" href="https://example.com/page" />`);
311
306
  expect(tags).toContain(`<meta property="og:url" content="https://example.com/page" />`);
312
307
  });
313
308
 
314
- test("should escape HTML in meta content", () => {
309
+ test('should escape HTML in meta content', () => {
315
310
  const meta = {
316
- title: "Test & Page",
317
- description: "Test <description>"
311
+ title: 'Test & Page',
312
+ description: 'Test <description>',
318
313
  };
319
-
314
+
320
315
  const tags = generateMetaTags(meta);
321
-
322
- expect(tags).toContain("<title>Test &amp; Page</title>");
316
+
317
+ expect(tags).toContain('<title>Test &amp; Page</title>');
323
318
  expect(tags).toContain(`content="Test &lt;description&gt;"`);
324
319
  });
325
320
 
326
- test("should handle empty meta", () => {
321
+ test('should handle empty meta', () => {
327
322
  const meta = {};
328
323
 
329
324
  const tags = generateMetaTags(meta);
330
325
 
331
- expect(tags).toBe("");
326
+ expect(tags).toBe('');
332
327
  });
333
328
 
334
- test("should generate hreflang tags when slugMappings provided", () => {
335
- const meta = { title: "About Us" };
329
+ test('should generate hreflang tags when slugMappings provided', () => {
330
+ const meta = { title: 'About Us' };
336
331
  const i18nConfig = {
337
332
  defaultLocale: 'en',
338
333
  locales: [
@@ -341,9 +336,7 @@ describe("SSR Renderer - generateMetaTags", () => {
341
336
  { code: 'de', name: 'German', nativeName: 'Deutsch', langTag: 'de-DE' },
342
337
  ],
343
338
  };
344
- const slugMappings = [
345
- { pageId: 'about', slugs: { en: 'about', pl: 'o-nas', de: 'uber-uns' } },
346
- ];
339
+ const slugMappings = [{ pageId: 'about', slugs: { en: 'about', pl: 'o-nas', de: 'uber-uns' } }];
347
340
 
348
341
  const tags = generateMetaTags(meta, 'https://example.com/about', 'en', i18nConfig, {
349
342
  slugMappings,
@@ -357,17 +350,13 @@ describe("SSR Renderer - generateMetaTags", () => {
357
350
  expect(tags).toContain('<link rel="alternate" hreflang="x-default" href="https://example.com/about" />');
358
351
  });
359
352
 
360
- test("should not generate hreflang tags for single locale", () => {
361
- const meta = { title: "About Us" };
353
+ test('should not generate hreflang tags for single locale', () => {
354
+ const meta = { title: 'About Us' };
362
355
  const i18nConfig = {
363
356
  defaultLocale: 'en',
364
- locales: [
365
- { code: 'en', name: 'English', nativeName: 'English', langTag: 'en-US' },
366
- ],
357
+ locales: [{ code: 'en', name: 'English', nativeName: 'English', langTag: 'en-US' }],
367
358
  };
368
- const slugMappings = [
369
- { pageId: 'about', slugs: { en: 'about' } },
370
- ];
359
+ const slugMappings = [{ pageId: 'about', slugs: { en: 'about' } }];
371
360
 
372
361
  const tags = generateMetaTags(meta, 'https://example.com/about', 'en', i18nConfig, {
373
362
  slugMappings,
@@ -378,8 +367,8 @@ describe("SSR Renderer - generateMetaTags", () => {
378
367
  expect(tags).not.toContain('hreflang');
379
368
  });
380
369
 
381
- test("should not generate hreflang tags without slugMappings", () => {
382
- const meta = { title: "About Us" };
370
+ test('should not generate hreflang tags without slugMappings', () => {
371
+ const meta = { title: 'About Us' };
383
372
  const i18nConfig = {
384
373
  defaultLocale: 'en',
385
374
  locales: [
@@ -393,8 +382,8 @@ describe("SSR Renderer - generateMetaTags", () => {
393
382
  expect(tags).not.toContain('hreflang');
394
383
  });
395
384
 
396
- test("should escape special characters in hreflang URLs", () => {
397
- const meta = { title: "Test" };
385
+ test('should escape special characters in hreflang URLs', () => {
386
+ const meta = { title: 'Test' };
398
387
  const i18nConfig = {
399
388
  defaultLocale: 'en',
400
389
  locales: [
@@ -402,9 +391,7 @@ describe("SSR Renderer - generateMetaTags", () => {
402
391
  { code: 'pl', name: 'Polish', nativeName: 'Polski', langTag: 'pl-PL' },
403
392
  ],
404
393
  };
405
- const slugMappings = [
406
- { pageId: 'test', slugs: { en: 'test&page', pl: 'test-strona' } },
407
- ];
394
+ const slugMappings = [{ pageId: 'test', slugs: { en: 'test&page', pl: 'test-strona' } }];
408
395
 
409
396
  const tags = generateMetaTags(meta, 'https://example.com/test&page', 'en', i18nConfig, {
410
397
  slugMappings,
@@ -416,363 +403,363 @@ describe("SSR Renderer - generateMetaTags", () => {
416
403
  });
417
404
  });
418
405
 
419
- describe("SSR Renderer - renderPageSSR", () => {
420
- describe("HTML generation", () => {
421
- test("should render simple div element", async () => {
406
+ describe('SSR Renderer - renderPageSSR', () => {
407
+ describe('HTML generation', () => {
408
+ test('should render simple div element', async () => {
422
409
  const pageData: JSONPage = {
423
410
  root: {
424
- type: "node",
425
- tag: "div",
426
- children: ["Hello World"]
427
- }
411
+ type: 'node',
412
+ tag: 'div',
413
+ children: ['Hello World'],
414
+ },
428
415
  };
429
-
416
+
430
417
  const result = await renderPageSSR(pageData);
431
-
432
- expect(result.html).toContain("<div");
433
- expect(result.html).toContain("Hello World");
434
- expect(result.html).toContain("</div>");
418
+
419
+ expect(result.html).toContain('<div');
420
+ expect(result.html).toContain('Hello World');
421
+ expect(result.html).toContain('</div>');
435
422
  });
436
423
 
437
- test("should render nested component structures", async () => {
424
+ test('should render nested component structures', async () => {
438
425
  const pageData: JSONPage = {
439
426
  root: {
440
- type: "node",
441
- tag: "div",
427
+ type: 'node',
428
+ tag: 'div',
442
429
  children: [
443
430
  {
444
- type: "node",
445
- tag: "div",
431
+ type: 'node',
432
+ tag: 'div',
446
433
  children: [
447
434
  {
448
- type: "node",
449
- tag: "span",
450
- children: ["Nested"]
451
- }
452
- ]
453
- }
454
- ]
455
- }
456
- };
457
-
435
+ type: 'node',
436
+ tag: 'span',
437
+ children: ['Nested'],
438
+ },
439
+ ],
440
+ },
441
+ ],
442
+ },
443
+ };
444
+
458
445
  const result = await renderPageSSR(pageData);
459
-
460
- expect(result.html).toContain("<div");
461
- expect(result.html).toContain("<span");
462
- expect(result.html).toContain("Nested");
446
+
447
+ expect(result.html).toContain('<div');
448
+ expect(result.html).toContain('<span');
449
+ expect(result.html).toContain('Nested');
463
450
  });
464
451
 
465
- test("should render children arrays", async () => {
452
+ test('should render children arrays', async () => {
466
453
  const pageData: JSONPage = {
467
454
  root: {
468
- type: "node",
469
- tag: "div",
455
+ type: 'node',
456
+ tag: 'div',
470
457
  children: [
471
- { type: "node", tag: "p", children: ["First"] },
472
- { type: "node", tag: "p", children: ["Second"] }
473
- ]
474
- }
458
+ { type: 'node', tag: 'p', children: ['First'] },
459
+ { type: 'node', tag: 'p', children: ['Second'] },
460
+ ],
461
+ },
475
462
  };
476
463
 
477
464
  const result = await renderPageSSR(pageData);
478
465
 
479
- expect(result.html).toContain("First");
480
- expect(result.html).toContain("Second");
466
+ expect(result.html).toContain('First');
467
+ expect(result.html).toContain('Second');
481
468
  });
482
469
 
483
- test("should render string children", async () => {
470
+ test('should render string children', async () => {
484
471
  const pageData: JSONPage = {
485
472
  root: {
486
- type: "node",
487
- tag: "p",
488
- children: ["Simple text"]
489
- }
473
+ type: 'node',
474
+ tag: 'p',
475
+ children: ['Simple text'],
476
+ },
490
477
  };
491
478
 
492
479
  const result = await renderPageSSR(pageData);
493
480
 
494
- expect(result.html).toContain("Simple text");
481
+ expect(result.html).toContain('Simple text');
495
482
  });
496
483
 
497
- test("should render number children", async () => {
484
+ test('should render number children', async () => {
498
485
  const pageData: JSONPage = {
499
486
  root: {
500
- type: "node",
501
- tag: "span",
502
- children: [42]
503
- }
487
+ type: 'node',
488
+ tag: 'span',
489
+ children: [42],
490
+ },
504
491
  };
505
492
 
506
493
  const result = await renderPageSSR(pageData);
507
494
 
508
- expect(result.html).toContain("42");
495
+ expect(result.html).toContain('42');
509
496
  });
510
497
 
511
- test("should escape HTML in text content", async () => {
498
+ test('should escape HTML in text content', async () => {
512
499
  const pageData: JSONPage = {
513
500
  root: {
514
- type: "node",
515
- tag: "div",
516
- children: ["<script>alert('xss')</script>"]
517
- }
501
+ type: 'node',
502
+ tag: 'div',
503
+ children: ["<script>alert('xss')</script>"],
504
+ },
518
505
  };
519
-
506
+
520
507
  const result = await renderPageSSR(pageData);
521
-
522
- expect(result.html).toContain("&lt;script&gt;");
523
- expect(result.html).not.toContain("<script>");
508
+
509
+ expect(result.html).toContain('&lt;script&gt;');
510
+ expect(result.html).not.toContain('<script>');
524
511
  });
525
512
 
526
- test("should render anchor tag", async () => {
513
+ test('should render anchor tag', async () => {
527
514
  const pageData: JSONPage = {
528
515
  root: {
529
- type: "node",
530
- tag: "a",
531
- children: ["Go to About"]
532
- }
516
+ type: 'node',
517
+ tag: 'a',
518
+ children: ['Go to About'],
519
+ },
533
520
  };
534
521
 
535
522
  const result = await renderPageSSR(pageData);
536
523
 
537
524
  expect(result.html).toContain(`<a`);
538
- expect(result.html).toContain("Go to About");
539
- expect(result.html).toContain("</a>");
525
+ expect(result.html).toContain('Go to About');
526
+ expect(result.html).toContain('</a>');
540
527
  });
541
528
  });
542
529
 
543
- describe("CSS generation", () => {
544
- test("should generate CSS for responsive styles", async () => {
530
+ describe('CSS generation', () => {
531
+ test('should generate CSS for responsive styles', async () => {
545
532
  const pageData: JSONPage = {
546
533
  root: {
547
- type: "node",
548
- tag: "div",
534
+ type: 'node',
535
+ tag: 'div',
549
536
  style: {
550
537
  base: {
551
- fontSize: "16px",
552
- color: "red"
538
+ fontSize: '16px',
539
+ color: 'red',
553
540
  },
554
541
  tablet: {
555
- fontSize: "14px"
542
+ fontSize: '14px',
556
543
  },
557
544
  mobile: {
558
- fontSize: "12px"
559
- }
560
- }
561
- }
545
+ fontSize: '12px',
546
+ },
547
+ },
548
+ },
562
549
  };
563
550
 
564
551
  const result = await renderPageSSR(pageData);
565
552
 
566
553
  // SSR renders HTML with styles (may be inline or via classes)
567
554
  expect(result.html).toBeDefined();
568
- expect(result.html).toContain("<div");
555
+ expect(result.html).toContain('<div');
569
556
  });
570
557
 
571
- test("should generate CSS class names for responsive styles", async () => {
558
+ test('should generate CSS class names for responsive styles', async () => {
572
559
  const pageData: JSONPage = {
573
560
  root: {
574
- type: "node",
575
- tag: "div",
561
+ type: 'node',
562
+ tag: 'div',
576
563
  style: {
577
564
  base: {
578
- color: "red"
579
- }
580
- }
581
- }
565
+ color: 'red',
566
+ },
567
+ },
568
+ },
582
569
  };
583
570
 
584
571
  const result = await renderPageSSR(pageData);
585
572
 
586
573
  // HTML should be generated with styles
587
574
  expect(result.html).toBeDefined();
588
- expect(result.html).toContain("<div");
575
+ expect(result.html).toContain('<div');
589
576
  });
590
577
 
591
- test("should handle empty responsive styles", async () => {
578
+ test('should handle empty responsive styles', async () => {
592
579
  const pageData: JSONPage = {
593
580
  root: {
594
- type: "node",
595
- tag: "div",
581
+ type: 'node',
582
+ tag: 'div',
596
583
  style: {
597
584
  base: {},
598
585
  tablet: {},
599
- mobile: {}
600
- }
601
- }
586
+ mobile: {},
587
+ },
588
+ },
602
589
  };
603
590
 
604
591
  const result = await renderPageSSR(pageData);
605
592
 
606
593
  // Should still render HTML
607
- expect(result.html).toContain("<div");
594
+ expect(result.html).toContain('<div');
608
595
  // componentCSS might be empty or minimal
609
596
  expect(result.componentCSS).toBeDefined();
610
597
  });
611
598
  });
612
599
 
613
- describe("Component rendering", () => {
614
- test("should render Button component (from Button.json)", async () => {
600
+ describe('Component rendering', () => {
601
+ test('should render Button component (from Button.json)', async () => {
615
602
  const buttonComponent: ComponentDefinition = {
616
- type: "Button",
603
+ type: 'Button',
617
604
  component: {
618
605
  interface: {
619
606
  variant: {
620
- type: "select",
621
- default: "primary"
607
+ type: 'select',
608
+ default: 'primary',
622
609
  },
623
610
  children: {
624
- type: "string",
625
- default: "Click me"
626
- }
611
+ type: 'string',
612
+ default: 'Click me',
613
+ },
627
614
  },
628
615
  structure: {
629
- type: "node",
630
- tag: "button",
616
+ type: 'node',
617
+ tag: 'button',
631
618
  style: {
632
619
  base: {
633
620
  background: {
634
621
  _mapping: true,
635
- prop: "variant",
622
+ prop: 'variant',
636
623
  values: {
637
- primary: "blue",
638
- secondary: "gray"
639
- }
624
+ primary: 'blue',
625
+ secondary: 'gray',
626
+ },
640
627
  } as any,
641
- color: "white"
642
- }
628
+ color: 'white',
629
+ },
643
630
  },
644
- children: "{{children}}"
645
- }
646
- }
631
+ children: '{{children}}',
632
+ },
633
+ },
647
634
  };
648
-
635
+
649
636
  const pageData: JSONPage = {
650
637
  root: {
651
- type: "component",
652
- component: "Button",
638
+ type: 'component',
639
+ component: 'Button',
653
640
  props: {
654
- variant: "primary"
641
+ variant: 'primary',
655
642
  },
656
- children: ["Custom Button"]
657
- }
643
+ children: ['Custom Button'],
644
+ },
658
645
  };
659
646
 
660
647
  const globalComponents = {
661
- Button: buttonComponent
648
+ Button: buttonComponent,
662
649
  };
663
650
 
664
651
  const result = await renderPageSSR(pageData, globalComponents);
665
652
 
666
653
  // Should render button element
667
- expect(result.html).toContain("<button");
654
+ expect(result.html).toContain('<button');
668
655
  // Should render children
669
- expect(result.html).toContain("Custom Button");
656
+ expect(result.html).toContain('Custom Button');
670
657
  });
671
658
 
672
- test("should render BasicCard component with template evaluation", async () => {
659
+ test('should render BasicCard component with template evaluation', async () => {
673
660
  const cardComponent: ComponentDefinition = {
674
- type: "BasicCard",
661
+ type: 'BasicCard',
675
662
  component: {
676
663
  interface: {
677
664
  title: {
678
- type: "string",
679
- default: "Card Title"
680
- }
665
+ type: 'string',
666
+ default: 'Card Title',
667
+ },
681
668
  },
682
669
  structure: {
683
- type: "node",
684
- tag: "div",
670
+ type: 'node',
671
+ tag: 'div',
685
672
  children: [
686
673
  {
687
- type: "node",
688
- tag: "h3",
689
- children: ["{{title}}"]
690
- }
691
- ]
692
- }
693
- }
694
- };
695
-
674
+ type: 'node',
675
+ tag: 'h3',
676
+ children: ['{{title}}'],
677
+ },
678
+ ],
679
+ },
680
+ },
681
+ };
682
+
696
683
  const pageData: JSONPage = {
697
684
  root: {
698
- type: "component",
699
- component: "BasicCard",
685
+ type: 'component',
686
+ component: 'BasicCard',
700
687
  props: {
701
- title: "My Card"
702
- }
703
- }
688
+ title: 'My Card',
689
+ },
690
+ },
704
691
  };
705
692
 
706
693
  const globalComponents = {
707
- BasicCard: cardComponent
694
+ BasicCard: cardComponent,
708
695
  };
709
696
 
710
697
  const result = await renderPageSSR(pageData, globalComponents);
711
698
 
712
- expect(result.html).toContain("<div");
713
- expect(result.html).toContain("<h3");
714
- expect(result.html).toContain("My Card");
699
+ expect(result.html).toContain('<div');
700
+ expect(result.html).toContain('<h3');
701
+ expect(result.html).toContain('My Card');
715
702
  });
716
703
  });
717
704
 
718
- describe("Template evaluation in SSR", () => {
719
- test("should evaluate template expressions in structure", async () => {
705
+ describe('Template evaluation in SSR', () => {
706
+ test('should evaluate template expressions in structure', async () => {
720
707
  const pageData: JSONPage = {
721
708
  root: {
722
- type: "node",
723
- tag: "div",
724
- children: ["Hello {{name}}"]
725
- }
709
+ type: 'node',
710
+ tag: 'div',
711
+ children: ['Hello {{name}}'],
712
+ },
726
713
  };
727
-
714
+
728
715
  // Note: This test may need adjustment based on actual template evaluation in SSR
729
716
  const result = await renderPageSSR(pageData);
730
-
717
+
731
718
  // Template should be evaluated or passed through
732
719
  expect(result.html).toBeDefined();
733
720
  });
734
721
 
735
- test("should handle style mappings in component structures", async () => {
722
+ test('should handle style mappings in component structures', async () => {
736
723
  const buttonComponent: ComponentDefinition = {
737
- type: "Button",
724
+ type: 'Button',
738
725
  component: {
739
726
  interface: {
740
727
  variant: {
741
- type: "select",
742
- default: "primary"
743
- }
728
+ type: 'select',
729
+ default: 'primary',
730
+ },
744
731
  },
745
732
  structure: {
746
- type: "node",
747
- tag: "button",
733
+ type: 'node',
734
+ tag: 'button',
748
735
  style: {
749
736
  base: {
750
737
  background: {
751
738
  _mapping: true,
752
- prop: "variant",
739
+ prop: 'variant',
753
740
  values: {
754
- primary: "blue",
755
- secondary: "gray"
756
- }
757
- } as any
758
- }
759
- }
760
- }
761
- }
762
- };
763
-
741
+ primary: 'blue',
742
+ secondary: 'gray',
743
+ },
744
+ } as any,
745
+ },
746
+ },
747
+ },
748
+ },
749
+ };
750
+
764
751
  const pageData: JSONPage = {
765
752
  root: {
766
- type: "component",
767
- component: "Button",
753
+ type: 'component',
754
+ component: 'Button',
768
755
  props: {
769
- variant: "primary"
770
- }
771
- }
756
+ variant: 'primary',
757
+ },
758
+ },
772
759
  };
773
760
 
774
761
  const globalComponents = {
775
- Button: buttonComponent
762
+ Button: buttonComponent,
776
763
  };
777
764
 
778
765
  const result = await renderPageSSR(pageData, globalComponents);
@@ -784,319 +771,316 @@ describe("SSR Renderer - renderPageSSR", () => {
784
771
  });
785
772
  });
786
773
 
787
- describe("Error handling", () => {
788
- test("should handle missing root node", async () => {
774
+ describe('Error handling', () => {
775
+ test('should handle missing root node', async () => {
789
776
  const pageData: JSONPage = {};
790
777
 
791
778
  // Implementation throws error for missing root
792
779
  await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
793
780
  });
794
781
 
795
- test("should handle null root node", async () => {
782
+ test('should handle null root node', async () => {
796
783
  const pageData: JSONPage = {
797
- root: null as any
784
+ root: null as any,
798
785
  };
799
786
 
800
787
  // Implementation throws error for null root
801
788
  await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
802
789
  });
803
790
 
804
- test("should handle component with missing structure", async () => {
791
+ test('should handle component with missing structure', async () => {
805
792
  const invalidComponent: ComponentDefinition = {
806
- type: "Invalid",
807
- component: {} as any
793
+ type: 'Invalid',
794
+ component: {} as any,
808
795
  };
809
796
 
810
797
  const pageData: JSONPage = {
811
798
  root: {
812
- type: "node" as const,
813
- tag: "div",
814
- children: []
815
- }
799
+ type: 'node' as const,
800
+ tag: 'div',
801
+ children: [],
802
+ },
816
803
  };
817
804
 
818
805
  const globalComponents = {
819
- Invalid: invalidComponent
806
+ Invalid: invalidComponent,
820
807
  };
821
808
 
822
809
  // Should not throw - page has valid root structure even if component is invalid
823
810
  const result = await renderPageSSR(pageData, globalComponents);
824
811
 
825
812
  // The page's root div renders normally since it's valid
826
- expect(result.html).toBe("<div></div>");
813
+ expect(result.html).toBe('<div></div>');
827
814
  });
828
815
 
829
- test("should handle malformed JSON structures gracefully", async () => {
816
+ test('should handle malformed JSON structures gracefully', async () => {
830
817
  const pageData: JSONPage = {
831
818
  root: {
832
- type: "node",
833
- tag: "div",
834
- children: [
835
- null as any,
836
- undefined as any
837
- ]
838
- }
819
+ type: 'node',
820
+ tag: 'div',
821
+ children: [null as any, undefined as any],
822
+ },
839
823
  };
840
-
824
+
841
825
  const result = await renderPageSSR(pageData);
842
-
826
+
843
827
  // Should skip null/undefined children
844
828
  expect(result.html).toBeDefined();
845
- expect(result.html).toContain("<div");
829
+ expect(result.html).toContain('<div');
846
830
  });
847
831
  });
848
832
 
849
- describe("Meta information in SSR", () => {
850
- test("should extract and return meta information", async () => {
833
+ describe('Meta information in SSR', () => {
834
+ test('should extract and return meta information', async () => {
851
835
  const pageData: JSONPage = {
852
836
  meta: {
853
- title: "Test Page",
854
- description: "Test description"
837
+ title: 'Test Page',
838
+ description: 'Test description',
855
839
  },
856
840
  root: {
857
- type: "node",
858
- tag: "div",
859
- children: ["Content"]
860
- }
841
+ type: 'node',
842
+ tag: 'div',
843
+ children: ['Content'],
844
+ },
861
845
  };
862
-
846
+
863
847
  const result = await renderPageSSR(pageData);
864
-
865
- expect(result.title).toBe("Test Page");
866
- expect(result.meta).toContain("Test Page");
867
- expect(result.meta).toContain("Test description");
848
+
849
+ expect(result.title).toBe('Test Page');
850
+ expect(result.meta).toContain('Test Page');
851
+ expect(result.meta).toContain('Test description');
868
852
  });
869
853
 
870
- test("should use default title when meta not provided", async () => {
854
+ test('should use default title when meta not provided', async () => {
871
855
  const pageData: JSONPage = {
872
856
  root: {
873
- type: "node",
874
- tag: "div"
875
- }
857
+ type: 'node',
858
+ tag: 'div',
859
+ },
876
860
  };
877
861
 
878
862
  const result = await renderPageSSR(pageData);
879
863
 
880
- expect(result.title).toBe("UPLO");
864
+ expect(result.title).toBe('UPLO');
881
865
  });
882
866
  });
883
867
  });
884
868
 
885
- describe("SSR Renderer - Security (XSS Prevention)", () => {
886
- describe("Text content sanitization", () => {
887
- test("should escape script tags in text content", async () => {
869
+ describe('SSR Renderer - Security (XSS Prevention)', () => {
870
+ describe('Text content sanitization', () => {
871
+ test('should escape script tags in text content', async () => {
888
872
  const pageData: JSONPage = {
889
873
  root: {
890
- type: "node",
891
- tag: "div",
892
- children: ["<script>alert('xss')</script>"]
893
- }
874
+ type: 'node',
875
+ tag: 'div',
876
+ children: ["<script>alert('xss')</script>"],
877
+ },
894
878
  };
895
879
 
896
880
  const result = await renderPageSSR(pageData);
897
881
 
898
- expect(result.html).toContain("&lt;script&gt;");
899
- expect(result.html).not.toContain("<script>alert");
882
+ expect(result.html).toContain('&lt;script&gt;');
883
+ expect(result.html).not.toContain('<script>alert');
900
884
  });
901
885
 
902
- test("should escape event handlers in text content", async () => {
886
+ test('should escape event handlers in text content', async () => {
903
887
  const pageData: JSONPage = {
904
888
  root: {
905
- type: "node",
906
- tag: "div",
907
- children: ['<img onerror="alert(1)" src="x">']
908
- }
889
+ type: 'node',
890
+ tag: 'div',
891
+ children: ['<img onerror="alert(1)" src="x">'],
892
+ },
909
893
  };
910
894
 
911
895
  const result = await renderPageSSR(pageData);
912
896
 
913
897
  expect(result.html).not.toContain('onerror="alert');
914
- expect(result.html).toContain("&lt;img");
898
+ expect(result.html).toContain('&lt;img');
915
899
  });
916
900
 
917
- test("should escape javascript: URLs in text content", async () => {
901
+ test('should escape javascript: URLs in text content', async () => {
918
902
  const pageData: JSONPage = {
919
903
  root: {
920
- type: "node",
921
- tag: "div",
922
- children: ['<a href="javascript:alert(1)">click</a>']
923
- }
904
+ type: 'node',
905
+ tag: 'div',
906
+ children: ['<a href="javascript:alert(1)">click</a>'],
907
+ },
924
908
  };
925
909
 
926
910
  const result = await renderPageSSR(pageData);
927
911
 
928
912
  expect(result.html).not.toContain('href="javascript:');
929
- expect(result.html).toContain("&lt;a");
913
+ expect(result.html).toContain('&lt;a');
930
914
  });
931
915
 
932
- test("should escape HTML entities in text content", async () => {
916
+ test('should escape HTML entities in text content', async () => {
933
917
  const pageData: JSONPage = {
934
918
  root: {
935
- type: "node",
936
- tag: "p",
937
- children: ['Test & <special> "quotes" \'apostrophe\'']
938
- }
919
+ type: 'node',
920
+ tag: 'p',
921
+ children: ['Test & <special> "quotes" \'apostrophe\''],
922
+ },
939
923
  };
940
924
 
941
925
  const result = await renderPageSSR(pageData);
942
926
 
943
- expect(result.html).toContain("&amp;");
944
- expect(result.html).toContain("&lt;special&gt;");
945
- expect(result.html).toContain("&quot;quotes&quot;");
927
+ expect(result.html).toContain('&amp;');
928
+ expect(result.html).toContain('&lt;special&gt;');
929
+ expect(result.html).toContain('&quot;quotes&quot;');
946
930
  });
947
931
 
948
- test("should escape nested malicious content", async () => {
932
+ test('should escape nested malicious content', async () => {
949
933
  const pageData: JSONPage = {
950
934
  root: {
951
- type: "node",
952
- tag: "div",
935
+ type: 'node',
936
+ tag: 'div',
953
937
  children: [
954
938
  {
955
- type: "node",
956
- tag: "p",
957
- children: ["<script>document.cookie</script>"]
958
- }
959
- ]
960
- }
939
+ type: 'node',
940
+ tag: 'p',
941
+ children: ['<script>document.cookie</script>'],
942
+ },
943
+ ],
944
+ },
961
945
  };
962
946
 
963
947
  const result = await renderPageSSR(pageData);
964
948
 
965
- expect(result.html).not.toContain("<script>document");
966
- expect(result.html).toContain("&lt;script&gt;");
949
+ expect(result.html).not.toContain('<script>document');
950
+ expect(result.html).toContain('&lt;script&gt;');
967
951
  });
968
952
  });
969
953
 
970
- describe("Meta tag sanitization", () => {
971
- test("should escape HTML in page title", () => {
954
+ describe('Meta tag sanitization', () => {
955
+ test('should escape HTML in page title', () => {
972
956
  const meta = {
973
- title: "Test <script>alert('xss')</script> Page"
957
+ title: "Test <script>alert('xss')</script> Page",
974
958
  };
975
959
 
976
960
  const tags = generateMetaTags(meta);
977
961
 
978
- expect(tags).toContain("<title>Test &lt;script&gt;");
979
- expect(tags).not.toContain("<script>alert");
962
+ expect(tags).toContain('<title>Test &lt;script&gt;');
963
+ expect(tags).not.toContain('<script>alert');
980
964
  });
981
965
 
982
- test("should escape HTML in meta description", () => {
966
+ test('should escape HTML in meta description', () => {
983
967
  const meta = {
984
- description: 'Description with "quotes" and <tags>'
968
+ description: 'Description with "quotes" and <tags>',
985
969
  };
986
970
 
987
971
  const tags = generateMetaTags(meta);
988
972
 
989
- expect(tags).toContain("&quot;quotes&quot;");
990
- expect(tags).toContain("&lt;tags&gt;");
973
+ expect(tags).toContain('&quot;quotes&quot;');
974
+ expect(tags).toContain('&lt;tags&gt;');
991
975
  });
992
976
 
993
- test("should escape HTML in Open Graph tags", () => {
977
+ test('should escape HTML in Open Graph tags', () => {
994
978
  const meta = {
995
979
  ogTitle: "<script>alert('og')</script>",
996
- ogDescription: "Description <img src=x onerror=alert(1)>"
980
+ ogDescription: 'Description <img src=x onerror=alert(1)>',
997
981
  };
998
982
 
999
983
  const tags = generateMetaTags(meta);
1000
984
 
1001
985
  // Script tags should be escaped
1002
- expect(tags).not.toContain("<script>alert");
1003
- expect(tags).toContain("&lt;script&gt;");
986
+ expect(tags).not.toContain('<script>alert');
987
+ expect(tags).toContain('&lt;script&gt;');
1004
988
  // IMG tag should be escaped, preventing onerror from executing
1005
- expect(tags).not.toContain("<img src=x");
1006
- expect(tags).toContain("&lt;img");
989
+ expect(tags).not.toContain('<img src=x');
990
+ expect(tags).toContain('&lt;img');
1007
991
  });
1008
992
  });
1009
993
 
1010
- describe("Attribute sanitization", () => {
1011
- test("should not allow malicious attributes through style", async () => {
994
+ describe('Attribute sanitization', () => {
995
+ test('should not allow malicious attributes through style', async () => {
1012
996
  const pageData: JSONPage = {
1013
997
  root: {
1014
- type: "node",
1015
- tag: "div",
998
+ type: 'node',
999
+ tag: 'div',
1016
1000
  style: {
1017
1001
  base: {
1018
1002
  // Attempting to inject via style properties
1019
- color: 'red; background-image: url("javascript:alert(1)")'
1020
- }
1021
- }
1022
- }
1003
+ color: 'red; background-image: url("javascript:alert(1)")',
1004
+ },
1005
+ },
1006
+ },
1023
1007
  };
1024
1008
 
1025
1009
  const result = await renderPageSSR(pageData);
1026
1010
 
1027
1011
  // Style should be processed safely via utility classes
1028
1012
  expect(result.html).toBeDefined();
1029
- expect(result.html).toContain("<div");
1013
+ expect(result.html).toContain('<div');
1030
1014
  });
1031
1015
 
1032
- test("should handle unicode escape sequences safely", async () => {
1016
+ test('should handle unicode escape sequences safely', async () => {
1033
1017
  const pageData: JSONPage = {
1034
1018
  root: {
1035
- type: "node",
1036
- tag: "div",
1037
- children: ["\u003cscript\u003ealert('unicode')\u003c/script\u003e"]
1038
- }
1019
+ type: 'node',
1020
+ tag: 'div',
1021
+ children: ["\u003cscript\u003ealert('unicode')\u003c/script\u003e"],
1022
+ },
1039
1023
  };
1040
1024
 
1041
1025
  const result = await renderPageSSR(pageData);
1042
1026
 
1043
1027
  // Unicode-encoded script tags should still be escaped
1044
- expect(result.html).not.toContain("<script>alert");
1028
+ expect(result.html).not.toContain('<script>alert');
1045
1029
  });
1046
1030
  });
1047
1031
 
1048
- describe("Component prop sanitization", () => {
1049
- test("should sanitize component props with HTML content", async () => {
1032
+ describe('Component prop sanitization', () => {
1033
+ test('should sanitize component props with HTML content', async () => {
1050
1034
  const cardComponent: ComponentDefinition = {
1051
- type: "Card",
1035
+ type: 'Card',
1052
1036
  component: {
1053
1037
  interface: {
1054
- title: { type: "string", default: "" }
1038
+ title: { type: 'string', default: '' },
1055
1039
  },
1056
1040
  structure: {
1057
- type: "node",
1058
- tag: "div",
1041
+ type: 'node',
1042
+ tag: 'div',
1059
1043
  children: [
1060
1044
  {
1061
- type: "node",
1062
- tag: "h3",
1063
- children: ["{{title}}"]
1064
- }
1065
- ]
1066
- }
1067
- }
1045
+ type: 'node',
1046
+ tag: 'h3',
1047
+ children: ['{{title}}'],
1048
+ },
1049
+ ],
1050
+ },
1051
+ },
1068
1052
  };
1069
1053
 
1070
1054
  const pageData: JSONPage = {
1071
1055
  root: {
1072
- type: "component",
1073
- component: "Card",
1056
+ type: 'component',
1057
+ component: 'Card',
1074
1058
  props: {
1075
- title: "<script>alert('props')</script>"
1076
- }
1077
- }
1059
+ title: "<script>alert('props')</script>",
1060
+ },
1061
+ },
1078
1062
  };
1079
1063
 
1080
1064
  const globalComponents = {
1081
- Card: cardComponent
1065
+ Card: cardComponent,
1082
1066
  };
1083
1067
 
1084
1068
  const result = await renderPageSSR(pageData, globalComponents);
1085
1069
 
1086
1070
  // Script tags should be escaped when rendering
1087
- expect(result.html).not.toContain("<script>alert");
1071
+ expect(result.html).not.toContain('<script>alert');
1088
1072
  });
1089
1073
  });
1090
1074
 
1091
- describe("Image sanitization", () => {
1092
- test("should escape src attribute in image nodes", async () => {
1075
+ describe('Image sanitization', () => {
1076
+ test('should escape src attribute in image nodes', async () => {
1093
1077
  const pageData: JSONPage = {
1094
1078
  root: {
1095
- type: "image",
1096
- tag: "img",
1079
+ type: 'image',
1080
+ tag: 'img',
1097
1081
  src: 'javascript:alert("img")',
1098
- alt: "test"
1099
- } as any
1082
+ alt: 'test',
1083
+ } as any,
1100
1084
  };
1101
1085
 
1102
1086
  const result = await renderPageSSR(pageData);
@@ -1105,14 +1089,14 @@ describe("SSR Renderer - Security (XSS Prevention)", () => {
1105
1089
  expect(result.html).toBeDefined();
1106
1090
  });
1107
1091
 
1108
- test("should escape alt attribute in image nodes", async () => {
1092
+ test('should escape alt attribute in image nodes', async () => {
1109
1093
  const pageData: JSONPage = {
1110
1094
  root: {
1111
- type: "image",
1112
- tag: "img",
1113
- src: "/test.jpg",
1114
- alt: '"><script>alert("alt")</script><img x="'
1115
- } as any
1095
+ type: 'image',
1096
+ tag: 'img',
1097
+ src: '/test.jpg',
1098
+ alt: '"><script>alert("alt")</script><img x="',
1099
+ } as any,
1116
1100
  };
1117
1101
 
1118
1102
  const result = await renderPageSSR(pageData);
@@ -1122,35 +1106,34 @@ describe("SSR Renderer - Security (XSS Prevention)", () => {
1122
1106
  });
1123
1107
  });
1124
1108
 
1125
- describe("Image LCP optimization", () => {
1126
- test("should render fetchpriority attribute", async () => {
1109
+ describe('Image LCP optimization', () => {
1110
+ test('should render fetchpriority attribute', async () => {
1127
1111
  const pageData: JSONPage = {
1128
1112
  root: {
1129
- type: "node",
1130
- tag: "img",
1113
+ type: 'node',
1114
+ tag: 'img',
1131
1115
  attributes: {
1132
- src: "/test.jpg",
1133
- alt: "test",
1134
- fetchpriority: "high"
1135
- }
1136
- } as any
1116
+ src: '/test.jpg',
1117
+ alt: 'test',
1118
+ fetchpriority: 'high',
1119
+ },
1120
+ } as any,
1137
1121
  };
1138
1122
 
1139
1123
  const result = await renderPageSSR(pageData);
1140
1124
 
1141
1125
  expect(result.html).toContain('fetchpriority="high"');
1142
1126
  });
1143
-
1144
1127
  });
1145
1128
 
1146
- describe("Embed content sanitization", () => {
1147
- test("should sanitize embed HTML with DOMPurify", async () => {
1129
+ describe('Embed content sanitization', () => {
1130
+ test('should sanitize embed HTML with DOMPurify', async () => {
1148
1131
  const pageData: JSONPage = {
1149
1132
  root: {
1150
- type: "embed",
1151
- tag: "div",
1152
- html: '<div onclick="alert(1)"><script>alert(2)</script><p>safe</p></div>'
1153
- } as any
1133
+ type: 'embed',
1134
+ tag: 'div',
1135
+ html: '<div onclick="alert(1)"><script>alert(2)</script><p>safe</p></div>',
1136
+ } as any,
1154
1137
  };
1155
1138
 
1156
1139
  const result = await renderPageSSR(pageData);
@@ -1160,105 +1143,105 @@ describe("SSR Renderer - Security (XSS Prevention)", () => {
1160
1143
  // script tags should be stripped
1161
1144
  expect(result.html).not.toContain('<script>alert');
1162
1145
  // Safe content should remain
1163
- expect(result.html).toContain("safe");
1146
+ expect(result.html).toContain('safe');
1164
1147
  });
1165
1148
 
1166
- test("should allow safe SVG in embeds", async () => {
1149
+ test('should allow safe SVG in embeds', async () => {
1167
1150
  const pageData: JSONPage = {
1168
1151
  root: {
1169
- type: "embed",
1170
- tag: "div",
1171
- html: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="red"/></svg>'
1172
- } as any
1152
+ type: 'embed',
1153
+ tag: 'div',
1154
+ html: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="red"/></svg>',
1155
+ } as any,
1173
1156
  };
1174
1157
 
1175
1158
  const result = await renderPageSSR(pageData);
1176
1159
 
1177
1160
  // Safe SVG should be preserved
1178
- expect(result.html).toContain("<svg");
1179
- expect(result.html).toContain("<circle");
1161
+ expect(result.html).toContain('<svg');
1162
+ expect(result.html).toContain('<circle');
1180
1163
  });
1181
1164
  });
1182
1165
  });
1183
1166
 
1184
- describe("SSR Renderer - generateSSRHTML", () => {
1185
- test("should generate complete HTML document", async () => {
1167
+ describe('SSR Renderer - generateSSRHTML', () => {
1168
+ test('should generate complete HTML document', async () => {
1186
1169
  const pageData: JSONPage = {
1187
1170
  meta: {
1188
- title: "Test Page"
1171
+ title: 'Test Page',
1189
1172
  },
1190
1173
  root: {
1191
- type: "node",
1192
- tag: "div",
1193
- children: ["Hello World"]
1194
- }
1174
+ type: 'node',
1175
+ tag: 'div',
1176
+ children: ['Hello World'],
1177
+ },
1195
1178
  };
1196
-
1197
- const html = await generateSSRHTML(pageData, {}, "/", "");
1198
-
1199
- expect(html).toContain("<!DOCTYPE html>");
1200
- expect(html).toContain("<html");
1201
- expect(html).toContain("<head");
1202
- expect(html).toContain("<body");
1179
+
1180
+ const html = await generateSSRHTML(pageData, {}, '/', '');
1181
+
1182
+ expect(html).toContain('<!DOCTYPE html>');
1183
+ expect(html).toContain('<html');
1184
+ expect(html).toContain('<head');
1185
+ expect(html).toContain('<body');
1203
1186
  expect(html).toContain('<div id="root">');
1204
- expect(html).toContain("Hello World");
1205
- expect(html).toContain("Test Page");
1187
+ expect(html).toContain('Hello World');
1188
+ expect(html).toContain('Test Page');
1206
1189
  });
1207
1190
 
1208
- test("should include CSS in style tag", async () => {
1191
+ test('should include CSS in style tag', async () => {
1209
1192
  const pageData: JSONPage = {
1210
1193
  root: {
1211
- type: "node",
1212
- tag: "div",
1194
+ type: 'node',
1195
+ tag: 'div',
1213
1196
  style: {
1214
1197
  base: {
1215
- color: "red"
1216
- }
1217
- }
1218
- }
1198
+ color: 'red',
1199
+ },
1200
+ },
1201
+ },
1219
1202
  };
1220
1203
 
1221
1204
  const html = await generateSSRHTML(pageData);
1222
1205
 
1223
1206
  // HTML should be generated with styles (either inline or in style tag)
1224
- expect(html).toContain("<div");
1207
+ expect(html).toContain('<div');
1225
1208
  expect(html).toBeDefined();
1226
1209
  });
1227
1210
 
1228
- test("should include client script", async () => {
1211
+ test('should include client script', async () => {
1229
1212
  const pageData: JSONPage = {
1230
1213
  root: {
1231
- type: "node",
1232
- tag: "div"
1233
- }
1214
+ type: 'node',
1215
+ tag: 'div',
1216
+ },
1234
1217
  };
1235
1218
 
1236
- const html = await generateSSRHTML(pageData, {}, "/", "", false);
1219
+ const html = await generateSSRHTML(pageData, {}, '/', '', false);
1237
1220
 
1238
1221
  expect(html).toContain('src="/client-router.tsx"');
1239
1222
  });
1240
1223
 
1241
- test("should use built bundle when specified", async () => {
1224
+ test('should use built bundle when specified', async () => {
1242
1225
  const pageData: JSONPage = {
1243
1226
  root: {
1244
- type: "node",
1245
- tag: "div"
1246
- }
1227
+ type: 'node',
1228
+ tag: 'div',
1229
+ },
1247
1230
  };
1248
1231
 
1249
- const html = await generateSSRHTML(pageData, {}, "/", "", true);
1232
+ const html = await generateSSRHTML(pageData, {}, '/', '', true);
1250
1233
 
1251
1234
  // When useBuiltBundle is true, implementation generates static HTML without client script
1252
1235
  expect(html).toBeDefined();
1253
- expect(html).toContain("<div");
1236
+ expect(html).toContain('<div');
1254
1237
  });
1255
1238
 
1256
- test("should include viewport meta tag", async () => {
1239
+ test('should include viewport meta tag', async () => {
1257
1240
  const pageData: JSONPage = {
1258
1241
  root: {
1259
- type: "node",
1260
- tag: "div"
1261
- }
1242
+ type: 'node',
1243
+ tag: 'div',
1244
+ },
1262
1245
  };
1263
1246
 
1264
1247
  const html = await generateSSRHTML(pageData);
@@ -1268,30 +1251,38 @@ describe("SSR Renderer - generateSSRHTML", () => {
1268
1251
  });
1269
1252
  });
1270
1253
 
1271
- describe("SSR Renderer - CMSList rendering", () => {
1254
+ describe('SSR Renderer - CMSList rendering', () => {
1272
1255
  // Mock CMS items for testing
1273
1256
  const mockCmsItems: CMSItem[] = [
1274
1257
  { _id: '1', slug: 'post-1', title: 'First Post', category: 'Tech' },
1275
1258
  { _id: '2', slug: 'post-2', title: 'Second Post', category: 'Design' },
1276
1259
  ];
1277
1260
 
1278
- test("should render children for each CMS item", async () => {
1261
+ test('should render children for each CMS item', async () => {
1279
1262
  const pageData: JSONPage = {
1280
1263
  root: {
1281
- type: "cms-list",
1282
- collection: "blog-posts",
1264
+ type: 'cms-list',
1265
+ collection: 'blog-posts',
1283
1266
  limit: 2,
1284
- children: [
1285
- { type: "node", tag: "h2", children: ["{{item.title}}"] }
1286
- ]
1287
- } as unknown as ComponentNode
1267
+ children: [{ type: 'node', tag: 'h2', children: ['{{item.title}}'] }],
1268
+ } as unknown as ComponentNode,
1288
1269
  };
1289
1270
 
1290
1271
  // Create a mock CMS service
1291
1272
  const mockCmsService = new CMSService();
1292
1273
  mockCmsService.queryItems = async () => mockCmsItems;
1293
1274
 
1294
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1275
+ const result = await renderPageSSR(
1276
+ pageData,
1277
+ {},
1278
+ '/',
1279
+ '',
1280
+ undefined,
1281
+ undefined,
1282
+ undefined,
1283
+ undefined,
1284
+ mockCmsService,
1285
+ );
1295
1286
 
1296
1287
  expect(result.html).toContain('<h2');
1297
1288
  expect(result.html).toContain('First Post');
@@ -1299,81 +1290,109 @@ describe("SSR Renderer - CMSList rendering", () => {
1299
1290
  expect(result.html).toContain('</h2>');
1300
1291
  });
1301
1292
 
1302
- test("should provide item context variables (itemIndex)", async () => {
1293
+ test('should provide item context variables (itemIndex)', async () => {
1303
1294
  const pageData: JSONPage = {
1304
1295
  root: {
1305
- type: "cms-list",
1306
- collection: "blog-posts",
1307
- children: [
1308
- { type: "node", tag: "span", children: ["{{itemIndex}}-{{item.title}}"] }
1309
- ]
1310
- } as unknown as ComponentNode
1296
+ type: 'cms-list',
1297
+ collection: 'blog-posts',
1298
+ children: [{ type: 'node', tag: 'span', children: ['{{itemIndex}}-{{item.title}}'] }],
1299
+ } as unknown as ComponentNode,
1311
1300
  };
1312
1301
 
1313
1302
  const mockCmsService = new CMSService();
1314
1303
  mockCmsService.queryItems = async () => mockCmsItems;
1315
1304
 
1316
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1305
+ const result = await renderPageSSR(
1306
+ pageData,
1307
+ {},
1308
+ '/',
1309
+ '',
1310
+ undefined,
1311
+ undefined,
1312
+ undefined,
1313
+ undefined,
1314
+ mockCmsService,
1315
+ );
1317
1316
 
1318
1317
  expect(result.html).toContain('0-First Post');
1319
1318
  expect(result.html).toContain('1-Second Post');
1320
1319
  });
1321
1320
 
1322
- test("should provide itemFirst and itemLast flags", async () => {
1321
+ test('should provide itemFirst and itemLast flags', async () => {
1323
1322
  const pageData: JSONPage = {
1324
1323
  root: {
1325
- type: "cms-list",
1326
- collection: "blog-posts",
1324
+ type: 'cms-list',
1325
+ collection: 'blog-posts',
1327
1326
  children: [
1328
1327
  {
1329
- type: "node",
1330
- tag: "div",
1328
+ type: 'node',
1329
+ tag: 'div',
1331
1330
  attributes: { 'data-first': '{{itemFirst}}', 'data-last': '{{itemLast}}' },
1332
- children: ["{{item.title}}"]
1333
- }
1334
- ]
1335
- } as unknown as ComponentNode
1331
+ children: ['{{item.title}}'],
1332
+ },
1333
+ ],
1334
+ } as unknown as ComponentNode,
1336
1335
  };
1337
1336
 
1338
1337
  const mockCmsService = new CMSService();
1339
1338
  mockCmsService.queryItems = async () => mockCmsItems;
1340
1339
 
1341
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1340
+ const result = await renderPageSSR(
1341
+ pageData,
1342
+ {},
1343
+ '/',
1344
+ '',
1345
+ undefined,
1346
+ undefined,
1347
+ undefined,
1348
+ undefined,
1349
+ mockCmsService,
1350
+ );
1342
1351
 
1343
1352
  expect(result.html).toContain('data-first="true"');
1344
1353
  expect(result.html).toContain('data-last="true"');
1345
1354
  });
1346
1355
 
1347
- test("should render empty when no items match", async () => {
1356
+ test('should render empty when no items match', async () => {
1348
1357
  const pageData: JSONPage = {
1349
1358
  root: {
1350
- type: "cms-list",
1351
- collection: "blog-posts",
1359
+ type: 'cms-list',
1360
+ collection: 'blog-posts',
1352
1361
  filter: { category: 'NonExistent' },
1353
- children: [{ type: "node", tag: "div", children: ["{{item.title}}"] }]
1354
- } as unknown as ComponentNode
1362
+ children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
1363
+ } as unknown as ComponentNode,
1355
1364
  };
1356
1365
 
1357
1366
  const mockCmsService = new CMSService();
1358
1367
  mockCmsService.queryItems = async () => [];
1359
1368
 
1360
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1369
+ const result = await renderPageSSR(
1370
+ pageData,
1371
+ {},
1372
+ '/',
1373
+ '',
1374
+ undefined,
1375
+ undefined,
1376
+ undefined,
1377
+ undefined,
1378
+ mockCmsService,
1379
+ );
1361
1380
 
1362
1381
  // Should not contain item content since no items match
1363
1382
  expect(result.html).not.toContain('First Post');
1364
1383
  expect(result.html).not.toContain('Second Post');
1365
1384
  });
1366
1385
 
1367
- test("should warn when CMS service is not available", async () => {
1386
+ test('should warn when CMS service is not available', async () => {
1368
1387
  const consoleSpy = spyOn(console, 'warn').mockImplementation(() => {});
1369
1388
 
1370
1389
  const pageData: JSONPage = {
1371
1390
  root: {
1372
- type: "list",
1373
- sourceType: "collection",
1374
- source: "blog-posts",
1375
- children: []
1376
- } as unknown as ComponentNode
1391
+ type: 'list',
1392
+ sourceType: 'collection',
1393
+ source: 'blog-posts',
1394
+ children: [],
1395
+ } as unknown as ComponentNode,
1377
1396
  };
1378
1397
 
1379
1398
  // No CMS service provided
@@ -1385,15 +1404,13 @@ describe("SSR Renderer - CMSList rendering", () => {
1385
1404
  consoleSpy.mockRestore();
1386
1405
  });
1387
1406
 
1388
- test("should pass through parent context to children", async () => {
1407
+ test('should pass through parent context to children', async () => {
1389
1408
  const pageData: JSONPage = {
1390
1409
  root: {
1391
- type: "cms-list",
1392
- collection: "blog-posts",
1393
- children: [
1394
- { type: "node", tag: "span", children: ["{{cms.siteTitle}}-{{item.title}}"] }
1395
- ]
1396
- } as unknown as ComponentNode
1410
+ type: 'cms-list',
1411
+ collection: 'blog-posts',
1412
+ children: [{ type: 'node', tag: 'span', children: ['{{cms.siteTitle}}-{{item.title}}'] }],
1413
+ } as unknown as ComponentNode,
1397
1414
  };
1398
1415
 
1399
1416
  const mockCmsService = new CMSService();
@@ -1402,34 +1419,54 @@ describe("SSR Renderer - CMSList rendering", () => {
1402
1419
  // Pass CMS context with parent variables
1403
1420
  const cmsContext = { cms: { siteTitle: 'MySite' } };
1404
1421
 
1405
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, cmsContext, mockCmsService);
1422
+ const result = await renderPageSSR(
1423
+ pageData,
1424
+ {},
1425
+ '/',
1426
+ '',
1427
+ undefined,
1428
+ undefined,
1429
+ undefined,
1430
+ cmsContext,
1431
+ mockCmsService,
1432
+ );
1406
1433
 
1407
1434
  expect(result.html).toContain('MySite-First Post');
1408
1435
  expect(result.html).toContain('MySite-Second Post');
1409
1436
  });
1410
1437
 
1411
- test("should support nested HTML structures in children", async () => {
1438
+ test('should support nested HTML structures in children', async () => {
1412
1439
  const pageData: JSONPage = {
1413
1440
  root: {
1414
- type: "cms-list",
1415
- collection: "blog-posts",
1441
+ type: 'cms-list',
1442
+ collection: 'blog-posts',
1416
1443
  children: [
1417
1444
  {
1418
- type: "node",
1419
- tag: "article",
1445
+ type: 'node',
1446
+ tag: 'article',
1420
1447
  children: [
1421
- { type: "node", tag: "h2", children: ["{{item.title}}"] },
1422
- { type: "node", tag: "p", children: ["Category: {{item.category}}"] }
1423
- ]
1424
- }
1425
- ]
1426
- } as unknown as ComponentNode
1448
+ { type: 'node', tag: 'h2', children: ['{{item.title}}'] },
1449
+ { type: 'node', tag: 'p', children: ['Category: {{item.category}}'] },
1450
+ ],
1451
+ },
1452
+ ],
1453
+ } as unknown as ComponentNode,
1427
1454
  };
1428
1455
 
1429
1456
  const mockCmsService = new CMSService();
1430
1457
  mockCmsService.queryItems = async () => mockCmsItems;
1431
1458
 
1432
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1459
+ const result = await renderPageSSR(
1460
+ pageData,
1461
+ {},
1462
+ '/',
1463
+ '',
1464
+ undefined,
1465
+ undefined,
1466
+ undefined,
1467
+ undefined,
1468
+ mockCmsService,
1469
+ );
1433
1470
 
1434
1471
  expect(result.html).toContain('<article');
1435
1472
  expect(result.html).toContain('<h2');
@@ -1439,11 +1476,15 @@ describe("SSR Renderer - CMSList rendering", () => {
1439
1476
  expect(result.html).toContain('</article>');
1440
1477
  });
1441
1478
 
1442
- test("should render CMS list with styles as utility classes", async () => {
1479
+ test('should render CMS list as a pure repeater (no container) even when style is present', async () => {
1480
+ // Current behavior: list nodes are pure repeaters — they emit only their
1481
+ // per-item children with no wrapper element, so node-level style produces
1482
+ // no utility classes (style a parent node instead). Previously the list
1483
+ // rendered a styled <div data-cms-list> container.
1443
1484
  const pageData: JSONPage = {
1444
1485
  root: {
1445
- type: "cms-list",
1446
- collection: "blog-posts",
1486
+ type: 'cms-list',
1487
+ collection: 'blog-posts',
1447
1488
  style: {
1448
1489
  base: {
1449
1490
  display: 'flex',
@@ -1451,76 +1492,105 @@ describe("SSR Renderer - CMSList rendering", () => {
1451
1492
  gap: '24px',
1452
1493
  },
1453
1494
  },
1454
- children: [
1455
- { type: "node", tag: "div", children: ["{{item.title}}"] }
1456
- ]
1457
- } as unknown as ComponentNode
1495
+ children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
1496
+ } as unknown as ComponentNode,
1458
1497
  };
1459
1498
 
1460
1499
  const mockCmsService = new CMSService();
1461
1500
  mockCmsService.queryItems = async () => mockCmsItems;
1462
1501
 
1463
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1464
-
1465
- // Should wrap in div with utility classes (f=flex, fd-col=column, g-24px=gap)
1466
- expect(result.html).toContain('class="');
1467
- expect(result.html).toContain('f');
1468
- expect(result.html).toContain('fd-col');
1469
- expect(result.html).toContain('g-24px');
1470
- // Should have data attributes
1471
- expect(result.html).toContain('data-cms-list="true"');
1472
- expect(result.html).toContain('data-collection="blog-posts"');
1473
- });
1474
-
1475
- test("should render CMS list with attributes", async () => {
1502
+ const result = await renderPageSSR(
1503
+ pageData,
1504
+ {},
1505
+ '/',
1506
+ '',
1507
+ undefined,
1508
+ undefined,
1509
+ undefined,
1510
+ undefined,
1511
+ mockCmsService,
1512
+ );
1513
+
1514
+ // Only the repeated children are emitted once per item, in order
1515
+ expect(result.html).toContain('<div>First Post</div>');
1516
+ expect(result.html).toContain('<div>Second Post</div>');
1517
+ // No container element means no class attribute and no list data attributes
1518
+ expect(result.html).not.toContain('class="');
1519
+ expect(result.html).not.toContain('data-cms-list="true"');
1520
+ expect(result.html).not.toContain('data-collection="blog-posts"');
1521
+ });
1522
+
1523
+ test('should render CMS list as a pure repeater (no container) even when attributes are present', async () => {
1524
+ // Current behavior: list nodes are pure repeaters with no wrapper element,
1525
+ // so node-level attributes are not rendered (set them on a parent node
1526
+ // instead). Previously the list rendered a <div data-cms-list> container
1527
+ // carrying these attributes.
1476
1528
  const pageData: JSONPage = {
1477
1529
  root: {
1478
- type: "cms-list",
1479
- collection: "blog-posts",
1530
+ type: 'cms-list',
1531
+ collection: 'blog-posts',
1480
1532
  attributes: {
1481
1533
  id: 'blog-list',
1482
1534
  'data-testid': 'cms-blog',
1483
1535
  },
1484
- children: [
1485
- { type: "node", tag: "div", children: ["{{item.title}}"] }
1486
- ]
1487
- } as unknown as ComponentNode
1536
+ children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
1537
+ } as unknown as ComponentNode,
1488
1538
  };
1489
1539
 
1490
1540
  const mockCmsService = new CMSService();
1491
1541
  mockCmsService.queryItems = async () => mockCmsItems;
1492
1542
 
1493
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1494
-
1495
- // Should have custom attributes
1496
- expect(result.html).toContain('id="blog-list"');
1497
- expect(result.html).toContain('data-testid="cms-blog"');
1498
- // Should have data attributes
1499
- expect(result.html).toContain('data-cms-list="true"');
1500
- expect(result.html).toContain('data-collection="blog-posts"');
1501
- });
1502
-
1503
- test("should process item templates in link href", async () => {
1543
+ const result = await renderPageSSR(
1544
+ pageData,
1545
+ {},
1546
+ '/',
1547
+ '',
1548
+ undefined,
1549
+ undefined,
1550
+ undefined,
1551
+ undefined,
1552
+ mockCmsService,
1553
+ );
1554
+
1555
+ // Only the repeated children are emitted — once per item, in order
1556
+ expect(result.html).toContain('<div>First Post</div>');
1557
+ expect(result.html).toContain('<div>Second Post</div>');
1558
+ // No container element means no custom attributes and no list data attributes
1559
+ expect(result.html).not.toContain('id="blog-list"');
1560
+ expect(result.html).not.toContain('data-testid="cms-blog"');
1561
+ expect(result.html).not.toContain('data-cms-list="true"');
1562
+ expect(result.html).not.toContain('data-collection="blog-posts"');
1563
+ });
1564
+
1565
+ test('should process item templates in link href', async () => {
1504
1566
  const pageData: JSONPage = {
1505
1567
  root: {
1506
- type: "cms-list",
1507
- collection: "blog-posts",
1568
+ type: 'cms-list',
1569
+ collection: 'blog-posts',
1508
1570
  children: [
1509
1571
  {
1510
- type: "link",
1511
- href: "/blog/{{item.slug}}",
1512
- children: [
1513
- { type: "node", tag: "span", children: ["{{item.title}}"] }
1514
- ]
1515
- }
1516
- ]
1517
- } as unknown as ComponentNode
1572
+ type: 'link',
1573
+ href: '/blog/{{item.slug}}',
1574
+ children: [{ type: 'node', tag: 'span', children: ['{{item.title}}'] }],
1575
+ },
1576
+ ],
1577
+ } as unknown as ComponentNode,
1518
1578
  };
1519
1579
 
1520
1580
  const mockCmsService = new CMSService();
1521
1581
  mockCmsService.queryItems = async () => mockCmsItems;
1522
1582
 
1523
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1583
+ const result = await renderPageSSR(
1584
+ pageData,
1585
+ {},
1586
+ '/',
1587
+ '',
1588
+ undefined,
1589
+ undefined,
1590
+ undefined,
1591
+ undefined,
1592
+ mockCmsService,
1593
+ );
1524
1594
 
1525
1595
  // Should render link as <a> with processed href
1526
1596
  expect(result.html).toContain('href="/blog/post-1"');
@@ -1529,26 +1599,36 @@ describe("SSR Renderer - CMSList rendering", () => {
1529
1599
  expect(result.html).toContain('Second Post');
1530
1600
  });
1531
1601
 
1532
- test("should process item templates in anchor tag attributes", async () => {
1602
+ test('should process item templates in anchor tag attributes', async () => {
1533
1603
  const pageData: JSONPage = {
1534
1604
  root: {
1535
- type: "cms-list",
1536
- collection: "blog-posts",
1605
+ type: 'cms-list',
1606
+ collection: 'blog-posts',
1537
1607
  children: [
1538
1608
  {
1539
- type: "node",
1540
- tag: "a",
1541
- attributes: { href: "/blog/{{item.slug}}" },
1542
- children: ["{{item.title}}"]
1543
- }
1544
- ]
1545
- } as unknown as ComponentNode
1609
+ type: 'node',
1610
+ tag: 'a',
1611
+ attributes: { href: '/blog/{{item.slug}}' },
1612
+ children: ['{{item.title}}'],
1613
+ },
1614
+ ],
1615
+ } as unknown as ComponentNode,
1546
1616
  };
1547
1617
 
1548
1618
  const mockCmsService = new CMSService();
1549
1619
  mockCmsService.queryItems = async () => mockCmsItems;
1550
1620
 
1551
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, undefined, mockCmsService);
1621
+ const result = await renderPageSSR(
1622
+ pageData,
1623
+ {},
1624
+ '/',
1625
+ '',
1626
+ undefined,
1627
+ undefined,
1628
+ undefined,
1629
+ undefined,
1630
+ mockCmsService,
1631
+ );
1552
1632
 
1553
1633
  // Should render anchor with processed href from attributes
1554
1634
  expect(result.html).toContain('href="/blog/post-1"');
@@ -1557,7 +1637,7 @@ describe("SSR Renderer - CMSList rendering", () => {
1557
1637
  expect(result.html).toContain('Second Post');
1558
1638
  });
1559
1639
 
1560
- test("should resolve items from cms context using {{cms.field}} template", async () => {
1640
+ test('should resolve items from cms context using {{cms.field}} template', async () => {
1561
1641
  // This tests nested cms-list in CMS template pages where items
1562
1642
  // come from a reference field like {{cms.category}}
1563
1643
  const mockCategories: CMSItem[] = [
@@ -1567,104 +1647,122 @@ describe("SSR Renderer - CMSList rendering", () => {
1567
1647
 
1568
1648
  const pageData: JSONPage = {
1569
1649
  root: {
1570
- type: "cms-list",
1571
- collection: "categories",
1572
- items: "{{cms.categoryIds}}", // Reference field from CMS context
1573
- itemAs: "category",
1574
- children: [
1575
- { type: "node", tag: "span", children: ["{{category.title}}"] }
1576
- ]
1577
- } as unknown as ComponentNode
1650
+ type: 'cms-list',
1651
+ collection: 'categories',
1652
+ items: '{{cms.categoryIds}}', // Reference field from CMS context
1653
+ itemAs: 'category',
1654
+ children: [{ type: 'node', tag: 'span', children: ['{{category.title}}'] }],
1655
+ } as unknown as ComponentNode,
1578
1656
  };
1579
1657
 
1580
1658
  const mockCmsService = new CMSService();
1581
- mockCmsService.getItemsByIds = async (collection: string, ids: string[]) => {
1659
+ mockCmsService.getItemsByIds = async (_collection: string, ids: string[]) => {
1582
1660
  // Return matching categories
1583
- return mockCategories.filter(c => ids.includes(c._id));
1661
+ return mockCategories.filter((c) => ids.includes(c._id));
1584
1662
  };
1585
1663
 
1586
1664
  // CMS context has reference field with category IDs
1587
1665
  const cmsContext = { cms: { categoryIds: ['cat-1', 'cat-2'] } };
1588
1666
 
1589
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, cmsContext, mockCmsService);
1667
+ const result = await renderPageSSR(
1668
+ pageData,
1669
+ {},
1670
+ '/',
1671
+ '',
1672
+ undefined,
1673
+ undefined,
1674
+ undefined,
1675
+ cmsContext,
1676
+ mockCmsService,
1677
+ );
1590
1678
 
1591
1679
  // Should render categories from resolved reference field
1592
1680
  expect(result.html).toContain('Technology');
1593
1681
  expect(result.html).toContain('Design');
1594
1682
  });
1595
1683
 
1596
- test("should resolve single item from cms context using {{cms.field}} template", async () => {
1684
+ test('should resolve single item from cms context using {{cms.field}} template', async () => {
1597
1685
  // Test single reference (not array)
1598
- const mockCategories: CMSItem[] = [
1599
- { _id: 'cat-1', slug: 'tech', title: 'Technology' },
1600
- ];
1686
+ const mockCategories: CMSItem[] = [{ _id: 'cat-1', slug: 'tech', title: 'Technology' }];
1601
1687
 
1602
1688
  const pageData: JSONPage = {
1603
1689
  root: {
1604
- type: "cms-list",
1605
- collection: "categories",
1606
- items: "{{cms.category}}", // Single reference field
1607
- itemAs: "category",
1608
- children: [
1609
- { type: "node", tag: "span", children: ["{{category.title}}"] }
1610
- ]
1611
- } as unknown as ComponentNode
1690
+ type: 'cms-list',
1691
+ collection: 'categories',
1692
+ items: '{{cms.category}}', // Single reference field
1693
+ itemAs: 'category',
1694
+ children: [{ type: 'node', tag: 'span', children: ['{{category.title}}'] }],
1695
+ } as unknown as ComponentNode,
1612
1696
  };
1613
1697
 
1614
1698
  const mockCmsService = new CMSService();
1615
- mockCmsService.getItemsByIds = async (collection: string, ids: string[]) => {
1616
- return mockCategories.filter(c => ids.includes(c._id));
1699
+ mockCmsService.getItemsByIds = async (_collection: string, ids: string[]) => {
1700
+ return mockCategories.filter((c) => ids.includes(c._id));
1617
1701
  };
1618
1702
 
1619
1703
  // CMS context has single reference (string, not array)
1620
1704
  const cmsContext = { cms: { category: 'cat-1' } };
1621
1705
 
1622
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, cmsContext, mockCmsService);
1706
+ const result = await renderPageSSR(
1707
+ pageData,
1708
+ {},
1709
+ '/',
1710
+ '',
1711
+ undefined,
1712
+ undefined,
1713
+ undefined,
1714
+ cmsContext,
1715
+ mockCmsService,
1716
+ );
1623
1717
 
1624
1718
  expect(result.html).toContain('Technology');
1625
1719
  });
1626
1720
 
1627
- test("should resolve nested cms field path using {{cms.nested.field}} template", async () => {
1721
+ test('should resolve nested cms field path using {{cms.nested.field}} template', async () => {
1628
1722
  // Test nested path resolution like {{cms.author.categories}}
1629
- const mockCategories: CMSItem[] = [
1630
- { _id: 'cat-1', slug: 'tech', title: 'Technology' },
1631
- ];
1723
+ const mockCategories: CMSItem[] = [{ _id: 'cat-1', slug: 'tech', title: 'Technology' }];
1632
1724
 
1633
1725
  const pageData: JSONPage = {
1634
1726
  root: {
1635
- type: "cms-list",
1636
- collection: "categories",
1637
- items: "{{cms.author.categories}}",
1638
- itemAs: "category",
1639
- children: [
1640
- { type: "node", tag: "span", children: ["{{category.title}}"] }
1641
- ]
1642
- } as unknown as ComponentNode
1727
+ type: 'cms-list',
1728
+ collection: 'categories',
1729
+ items: '{{cms.author.categories}}',
1730
+ itemAs: 'category',
1731
+ children: [{ type: 'node', tag: 'span', children: ['{{category.title}}'] }],
1732
+ } as unknown as ComponentNode,
1643
1733
  };
1644
1734
 
1645
1735
  const mockCmsService = new CMSService();
1646
- mockCmsService.getItemsByIds = async (collection: string, ids: string[]) => {
1647
- return mockCategories.filter(c => ids.includes(c._id));
1736
+ mockCmsService.getItemsByIds = async (_collection: string, ids: string[]) => {
1737
+ return mockCategories.filter((c) => ids.includes(c._id));
1648
1738
  };
1649
1739
 
1650
1740
  // CMS context has nested structure
1651
1741
  const cmsContext = { cms: { author: { categories: ['cat-1'] } } };
1652
1742
 
1653
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, cmsContext, mockCmsService);
1743
+ const result = await renderPageSSR(
1744
+ pageData,
1745
+ {},
1746
+ '/',
1747
+ '',
1748
+ undefined,
1749
+ undefined,
1750
+ undefined,
1751
+ cmsContext,
1752
+ mockCmsService,
1753
+ );
1654
1754
 
1655
1755
  expect(result.html).toContain('Technology');
1656
1756
  });
1657
1757
 
1658
- test("should return empty when cms field is missing", async () => {
1758
+ test('should return empty when cms field is missing', async () => {
1659
1759
  const pageData: JSONPage = {
1660
1760
  root: {
1661
- type: "cms-list",
1662
- collection: "categories",
1663
- items: "{{cms.nonexistent}}",
1664
- children: [
1665
- { type: "node", tag: "span", children: ["{{item.title}}"] }
1666
- ]
1667
- } as unknown as ComponentNode
1761
+ type: 'cms-list',
1762
+ collection: 'categories',
1763
+ items: '{{cms.nonexistent}}',
1764
+ children: [{ type: 'node', tag: 'span', children: ['{{item.title}}'] }],
1765
+ } as unknown as ComponentNode,
1668
1766
  };
1669
1767
 
1670
1768
  const mockCmsService = new CMSService();
@@ -1673,80 +1771,84 @@ describe("SSR Renderer - CMSList rendering", () => {
1673
1771
  // CMS context without the referenced field
1674
1772
  const cmsContext = { cms: { title: 'Page Title' } };
1675
1773
 
1676
- const result = await renderPageSSR(pageData, {}, '/', '', undefined, undefined, undefined, cmsContext, mockCmsService);
1774
+ const result = await renderPageSSR(
1775
+ pageData,
1776
+ {},
1777
+ '/',
1778
+ '',
1779
+ undefined,
1780
+ undefined,
1781
+ undefined,
1782
+ cmsContext,
1783
+ mockCmsService,
1784
+ );
1677
1785
 
1678
1786
  // Should return empty (no items to render)
1679
1787
  expect(result.html).toBe('');
1680
1788
  });
1681
1789
  });
1682
1790
 
1683
- describe("SSR Renderer - Interactive Styles", () => {
1791
+ describe('SSR Renderer - Interactive Styles', () => {
1684
1792
  /**
1685
1793
  * Tests for interactive styles (hover, active, etc.) in SSR rendering.
1686
1794
  * Verifies that element classes are generated correctly for both
1687
1795
  * page elements and component elements.
1688
1796
  */
1689
1797
 
1690
- describe("Page element interactive styles", () => {
1691
- test("should add element class for interactive styles", async () => {
1798
+ describe('Page element interactive styles', () => {
1799
+ test('should add element class for interactive styles', async () => {
1692
1800
  const pageData: JSONPage = {
1693
1801
  root: {
1694
- type: "node",
1695
- tag: "button",
1696
- label: "submit-btn",
1697
- interactiveStyles: [
1698
- { postfix: ":hover", style: { backgroundColor: "primary" } },
1699
- ],
1700
- children: ["Submit"],
1802
+ type: 'node',
1803
+ tag: 'button',
1804
+ label: 'submit-btn',
1805
+ interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary' } }],
1806
+ children: ['Submit'],
1701
1807
  },
1702
1808
  };
1703
1809
 
1704
- const result = await renderPageSSR(pageData, {}, "/contact", "");
1810
+ const result = await renderPageSSR(pageData, {}, '/contact', '');
1705
1811
 
1706
1812
  // Should have element class in HTML
1707
- expect(result.html).toContain("class=");
1813
+ expect(result.html).toContain('class=');
1708
1814
  // Page elements should have p_ prefix
1709
- expect(result.html).toContain("p_contact_submit-btn");
1815
+ expect(result.html).toContain('p_contact_submit-btn');
1710
1816
  });
1711
1817
 
1712
- test("should generate interactive CSS for page elements", async () => {
1818
+ test('should generate interactive CSS for page elements', async () => {
1713
1819
  const pageData: JSONPage = {
1714
1820
  root: {
1715
- type: "node",
1716
- tag: "div",
1717
- label: "card",
1718
- interactiveStyles: [
1719
- { postfix: ":hover", style: { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" } },
1720
- ],
1721
- children: ["Card content"],
1821
+ type: 'node',
1822
+ tag: 'div',
1823
+ label: 'card',
1824
+ interactiveStyles: [{ postfix: ':hover', style: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' } }],
1825
+ children: ['Card content'],
1722
1826
  },
1723
1827
  };
1724
1828
 
1725
- const result = await generateSSRHTML(pageData, {}, "/about");
1829
+ const result = await generateSSRHTML(pageData, {}, '/about');
1726
1830
 
1727
1831
  // Generated CSS should include interactive styles
1728
1832
  // generateSSRHTML returns the full HTML string directly
1729
- expect(result).toContain("p_about_card");
1833
+ expect(result).toContain('p_about_card');
1730
1834
  });
1731
1835
 
1732
- test("should generate correct class for nested page elements", async () => {
1836
+ test('should generate correct class for nested page elements', async () => {
1733
1837
  const pageData: JSONPage = {
1734
1838
  root: {
1735
- type: "node",
1736
- tag: "div",
1839
+ type: 'node',
1840
+ tag: 'div',
1737
1841
  children: [
1738
1842
  {
1739
- type: "node",
1740
- tag: "div",
1843
+ type: 'node',
1844
+ tag: 'div',
1741
1845
  children: [
1742
1846
  {
1743
- type: "node",
1744
- tag: "button",
1745
- label: "nested-btn",
1746
- interactiveStyles: [
1747
- { postfix: ":hover", style: { opacity: "0.9" } },
1748
- ],
1749
- children: ["Click"],
1847
+ type: 'node',
1848
+ tag: 'button',
1849
+ label: 'nested-btn',
1850
+ interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
1851
+ children: ['Click'],
1750
1852
  },
1751
1853
  ],
1752
1854
  },
@@ -1754,295 +1856,291 @@ describe("SSR Renderer - Interactive Styles", () => {
1754
1856
  },
1755
1857
  };
1756
1858
 
1757
- const result = await renderPageSSR(pageData, {}, "/services", "");
1859
+ const result = await renderPageSSR(pageData, {}, '/services', '');
1758
1860
 
1759
1861
  // Nested element should have correct page class
1760
- expect(result.html).toContain("p_services_nested-btn");
1862
+ expect(result.html).toContain('p_services_nested-btn');
1761
1863
  });
1762
1864
 
1763
- test("should handle multiple interactive states", async () => {
1865
+ test('should handle multiple interactive states', async () => {
1764
1866
  const pageData: JSONPage = {
1765
1867
  root: {
1766
- type: "node",
1767
- tag: "a",
1768
- label: "nav-link",
1868
+ type: 'node',
1869
+ tag: 'a',
1870
+ label: 'nav-link',
1769
1871
  interactiveStyles: [
1770
- { postfix: ":hover", style: { color: "primary" } },
1771
- { postfix: ":active", style: { color: "primary-dark" } },
1772
- { postfix: ":focus", style: { outline: "2px solid var(--primary)" } },
1872
+ { postfix: ':hover', style: { color: 'primary' } },
1873
+ { postfix: ':active', style: { color: 'primary-dark' } },
1874
+ { postfix: ':focus', style: { outline: '2px solid var(--primary)' } },
1773
1875
  ],
1774
- children: ["Link"],
1876
+ children: ['Link'],
1775
1877
  },
1776
1878
  };
1777
1879
 
1778
- const result = await renderPageSSR(pageData, {}, "/", "");
1880
+ const result = await renderPageSSR(pageData, {}, '/', '');
1779
1881
 
1780
1882
  // Should have single class for all states
1781
- expect(result.html).toContain("p_index_nav-link");
1883
+ expect(result.html).toContain('p_index_nav-link');
1782
1884
  });
1783
1885
  });
1784
1886
 
1785
- describe("Responsive interactive styles", () => {
1786
- test("should handle responsive hover styles", async () => {
1887
+ describe('Responsive interactive styles', () => {
1888
+ test('should handle responsive hover styles', async () => {
1787
1889
  const pageData: JSONPage = {
1788
1890
  root: {
1789
- type: "node",
1790
- tag: "div",
1791
- label: "responsive-card",
1891
+ type: 'node',
1892
+ tag: 'div',
1893
+ label: 'responsive-card',
1792
1894
  interactiveStyles: [
1793
1895
  {
1794
- postfix: ":hover",
1896
+ postfix: ':hover',
1795
1897
  style: {
1796
- base: { padding: "20px" },
1797
- tablet: { padding: "16px" },
1798
- mobile: { padding: "12px" },
1898
+ base: { padding: '20px' },
1899
+ tablet: { padding: '16px' },
1900
+ mobile: { padding: '12px' },
1799
1901
  },
1800
1902
  },
1801
1903
  ],
1802
- children: ["Responsive content"],
1904
+ children: ['Responsive content'],
1803
1905
  },
1804
1906
  };
1805
1907
 
1806
- const result = await renderPageSSR(pageData, {}, "/test", "");
1908
+ const result = await renderPageSSR(pageData, {}, '/test', '');
1807
1909
 
1808
1910
  // Should have element class
1809
- expect(result.html).toContain("p_test_responsive-card");
1911
+ expect(result.html).toContain('p_test_responsive-card');
1810
1912
  });
1811
1913
  });
1812
1914
 
1813
- describe("Interactive styles with generateElementClass flag", () => {
1814
- test("should add element class when generateElementClass is true", async () => {
1915
+ describe('Interactive styles with generateElementClass flag', () => {
1916
+ test('should add element class when generateElementClass is true', async () => {
1815
1917
  const pageData: JSONPage = {
1816
1918
  root: {
1817
- type: "node",
1818
- tag: "div",
1819
- label: "target-element",
1919
+ type: 'node',
1920
+ tag: 'div',
1921
+ label: 'target-element',
1820
1922
  generateElementClass: true,
1821
- children: ["Has class flag"],
1923
+ children: ['Has class flag'],
1822
1924
  },
1823
1925
  };
1824
1926
 
1825
- const result = await renderPageSSR(pageData, {}, "/page", "");
1927
+ const result = await renderPageSSR(pageData, {}, '/page', '');
1826
1928
 
1827
1929
  // Should have element class even without interactive styles
1828
- expect(result.html).toContain("p_page_target-element");
1930
+ expect(result.html).toContain('p_page_target-element');
1829
1931
  });
1830
1932
  });
1831
1933
 
1832
- describe("Edge cases", () => {
1833
- test("should not add element class when no interactive styles or flag", async () => {
1934
+ describe('Edge cases', () => {
1935
+ test('should not add element class when no interactive styles or flag', async () => {
1834
1936
  const pageData: JSONPage = {
1835
1937
  root: {
1836
- type: "node",
1837
- tag: "div",
1838
- children: ["Plain element"],
1938
+ type: 'node',
1939
+ tag: 'div',
1940
+ children: ['Plain element'],
1839
1941
  },
1840
1942
  };
1841
1943
 
1842
- const result = await renderPageSSR(pageData, {}, "/plain", "");
1944
+ const result = await renderPageSSR(pageData, {}, '/plain', '');
1843
1945
 
1844
1946
  // Should not have p_ prefixed class
1845
- expect(result.html).not.toContain("p_plain_");
1947
+ expect(result.html).not.toContain('p_plain_');
1846
1948
  });
1847
1949
 
1848
- test("should handle empty interactive styles array", async () => {
1950
+ test('should handle empty interactive styles array', async () => {
1849
1951
  const pageData: JSONPage = {
1850
1952
  root: {
1851
- type: "node",
1852
- tag: "div",
1953
+ type: 'node',
1954
+ tag: 'div',
1853
1955
  interactiveStyles: [],
1854
- children: ["Empty styles"],
1956
+ children: ['Empty styles'],
1855
1957
  },
1856
1958
  };
1857
1959
 
1858
- const result = await renderPageSSR(pageData, {}, "/empty", "");
1960
+ const result = await renderPageSSR(pageData, {}, '/empty', '');
1859
1961
 
1860
1962
  // Empty array should not add class
1861
- expect(result.html).not.toContain("p_empty_");
1963
+ expect(result.html).not.toContain('p_empty_');
1862
1964
  });
1863
1965
 
1864
- test("should handle index page path", async () => {
1966
+ test('should handle index page path', async () => {
1865
1967
  const pageData: JSONPage = {
1866
1968
  root: {
1867
- type: "node",
1868
- tag: "button",
1869
- label: "hero-cta",
1870
- interactiveStyles: [
1871
- { postfix: ":hover", style: { backgroundColor: "primary" } },
1872
- ],
1873
- children: ["Get Started"],
1969
+ type: 'node',
1970
+ tag: 'button',
1971
+ label: 'hero-cta',
1972
+ interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary' } }],
1973
+ children: ['Get Started'],
1874
1974
  },
1875
1975
  };
1876
1976
 
1877
- const result = await renderPageSSR(pageData, {}, "/", "");
1977
+ const result = await renderPageSSR(pageData, {}, '/', '');
1878
1978
 
1879
1979
  // Root path "/" should use "index" as file name
1880
- expect(result.html).toContain("p_index_hero-cta");
1980
+ expect(result.html).toContain('p_index_hero-cta');
1881
1981
  });
1882
1982
 
1883
- test("should handle nested page paths", async () => {
1983
+ test('should handle nested page paths', async () => {
1884
1984
  const pageData: JSONPage = {
1885
1985
  root: {
1886
- type: "node",
1887
- tag: "div",
1888
- label: "blog-card",
1889
- interactiveStyles: [
1890
- { postfix: ":hover", style: { transform: "translateY(-2px)" } },
1891
- ],
1892
- children: ["Blog post"],
1986
+ type: 'node',
1987
+ tag: 'div',
1988
+ label: 'blog-card',
1989
+ interactiveStyles: [{ postfix: ':hover', style: { transform: 'translateY(-2px)' } }],
1990
+ children: ['Blog post'],
1893
1991
  },
1894
1992
  };
1895
1993
 
1896
- const result = await renderPageSSR(pageData, {}, "/blog/posts", "");
1994
+ const result = await renderPageSSR(pageData, {}, '/blog/posts', '');
1897
1995
 
1898
1996
  // Nested path should be sanitized (slashes become underscores)
1899
- expect(result.html).toContain("p_blog_posts_blog-card");
1997
+ expect(result.html).toContain('p_blog_posts_blog-card');
1900
1998
  });
1901
1999
  });
1902
2000
 
1903
- describe("SSR Renderer - Conditional Rendering (if property)", () => {
1904
- test("should render node when if is true", async () => {
2001
+ describe('SSR Renderer - Conditional Rendering (if property)', () => {
2002
+ test('should render node when if is true', async () => {
1905
2003
  const pageData: JSONPage = {
1906
2004
  root: {
1907
- type: "node",
1908
- tag: "div",
2005
+ type: 'node',
2006
+ tag: 'div',
1909
2007
  if: true,
1910
- children: ["Visible content"],
2008
+ children: ['Visible content'],
1911
2009
  } as any,
1912
2010
  };
1913
2011
 
1914
- const result = await renderPageSSR(pageData, {}, "/", "");
2012
+ const result = await renderPageSSR(pageData, {}, '/', '');
1915
2013
 
1916
- expect(result.html).toContain("Visible content");
2014
+ expect(result.html).toContain('Visible content');
1917
2015
  });
1918
2016
 
1919
- test("should skip node when if is false", async () => {
2017
+ test('should skip node when if is false', async () => {
1920
2018
  const pageData: JSONPage = {
1921
2019
  root: {
1922
- type: "node",
1923
- tag: "div",
2020
+ type: 'node',
2021
+ tag: 'div',
1924
2022
  if: false,
1925
- children: ["Hidden content"],
2023
+ children: ['Hidden content'],
1926
2024
  } as any,
1927
2025
  };
1928
2026
 
1929
- const result = await renderPageSSR(pageData, {}, "/", "");
2027
+ const result = await renderPageSSR(pageData, {}, '/', '');
1930
2028
 
1931
- expect(result.html).not.toContain("Hidden content");
1932
- expect(result.html).toBe("");
2029
+ expect(result.html).not.toContain('Hidden content');
2030
+ expect(result.html).toBe('');
1933
2031
  });
1934
2032
 
1935
- test("should render node when if property is absent", async () => {
2033
+ test('should render node when if property is absent', async () => {
1936
2034
  const pageData: JSONPage = {
1937
2035
  root: {
1938
- type: "node",
1939
- tag: "div",
1940
- children: ["Default visible"],
2036
+ type: 'node',
2037
+ tag: 'div',
2038
+ children: ['Default visible'],
1941
2039
  },
1942
2040
  };
1943
2041
 
1944
- const result = await renderPageSSR(pageData, {}, "/", "");
2042
+ const result = await renderPageSSR(pageData, {}, '/', '');
1945
2043
 
1946
- expect(result.html).toContain("Default visible");
2044
+ expect(result.html).toContain('Default visible');
1947
2045
  });
1948
2046
 
1949
- test("should skip children when parent if is false", async () => {
2047
+ test('should skip children when parent if is false', async () => {
1950
2048
  const pageData: JSONPage = {
1951
2049
  root: {
1952
- type: "node",
1953
- tag: "div",
2050
+ type: 'node',
2051
+ tag: 'div',
1954
2052
  if: false,
1955
2053
  children: [
1956
- { type: "node", tag: "span", children: ["Child 1"] },
1957
- { type: "node", tag: "span", children: ["Child 2"] },
2054
+ { type: 'node', tag: 'span', children: ['Child 1'] },
2055
+ { type: 'node', tag: 'span', children: ['Child 2'] },
1958
2056
  ],
1959
2057
  } as any,
1960
2058
  };
1961
2059
 
1962
- const result = await renderPageSSR(pageData, {}, "/", "");
2060
+ const result = await renderPageSSR(pageData, {}, '/', '');
1963
2061
 
1964
- expect(result.html).not.toContain("Child 1");
1965
- expect(result.html).not.toContain("Child 2");
2062
+ expect(result.html).not.toContain('Child 1');
2063
+ expect(result.html).not.toContain('Child 2');
1966
2064
  });
1967
2065
 
1968
- test("should handle if with embed nodes", async () => {
2066
+ test('should handle if with embed nodes', async () => {
1969
2067
  const pageData: JSONPage = {
1970
2068
  root: {
1971
- type: "embed",
1972
- html: "<svg></svg>",
2069
+ type: 'embed',
2070
+ html: '<svg></svg>',
1973
2071
  if: false,
1974
2072
  } as any,
1975
2073
  };
1976
2074
 
1977
- const result = await renderPageSSR(pageData, {}, "/", "");
2075
+ const result = await renderPageSSR(pageData, {}, '/', '');
1978
2076
 
1979
- expect(result.html).not.toContain("<svg>");
2077
+ expect(result.html).not.toContain('<svg>');
1980
2078
  });
1981
2079
 
1982
- test("should handle if with link nodes", async () => {
2080
+ test('should handle if with link nodes', async () => {
1983
2081
  const pageData: JSONPage = {
1984
2082
  root: {
1985
- type: "link",
1986
- href: "/page",
2083
+ type: 'link',
2084
+ href: '/page',
1987
2085
  if: false,
1988
- children: ["Hidden link"],
2086
+ children: ['Hidden link'],
1989
2087
  } as any,
1990
2088
  };
1991
2089
 
1992
- const result = await renderPageSSR(pageData, {}, "/", "");
2090
+ const result = await renderPageSSR(pageData, {}, '/', '');
1993
2091
 
1994
- expect(result.html).not.toContain("Hidden link");
2092
+ expect(result.html).not.toContain('Hidden link');
1995
2093
  });
1996
2094
 
1997
- test("should handle if with locale-list nodes", async () => {
2095
+ test('should handle if with locale-list nodes', async () => {
1998
2096
  const pageData: JSONPage = {
1999
2097
  root: {
2000
- type: "locale-list",
2098
+ type: 'locale-list',
2001
2099
  if: false,
2002
2100
  } as any,
2003
2101
  };
2004
2102
 
2005
- const result = await renderPageSSR(pageData, {}, "/", "");
2103
+ const result = await renderPageSSR(pageData, {}, '/', '');
2006
2104
 
2007
- expect(result.html).not.toContain("locale-list");
2105
+ expect(result.html).not.toContain('locale-list');
2008
2106
  });
2009
2107
 
2010
- test("should render conditional child nodes independently", async () => {
2108
+ test('should render conditional child nodes independently', async () => {
2011
2109
  const pageData: JSONPage = {
2012
2110
  root: {
2013
- type: "node",
2014
- tag: "div",
2111
+ type: 'node',
2112
+ tag: 'div',
2015
2113
  children: [
2016
- { type: "node", tag: "span", if: true, children: ["Visible"] } as any,
2017
- { type: "node", tag: "span", if: false, children: ["Hidden"] } as any,
2018
- { type: "node", tag: "span", children: ["Always visible"] },
2114
+ { type: 'node', tag: 'span', if: true, children: ['Visible'] } as any,
2115
+ { type: 'node', tag: 'span', if: false, children: ['Hidden'] } as any,
2116
+ { type: 'node', tag: 'span', children: ['Always visible'] },
2019
2117
  ],
2020
2118
  },
2021
2119
  };
2022
2120
 
2023
- const result = await renderPageSSR(pageData, {}, "/", "");
2121
+ const result = await renderPageSSR(pageData, {}, '/', '');
2024
2122
 
2025
- expect(result.html).toContain("Visible");
2026
- expect(result.html).not.toContain("Hidden");
2027
- expect(result.html).toContain("Always visible");
2123
+ expect(result.html).toContain('Visible');
2124
+ expect(result.html).not.toContain('Hidden');
2125
+ expect(result.html).toContain('Always visible');
2028
2126
  });
2029
2127
 
2030
- test("should handle if mapping with component props in SSR", async () => {
2128
+ test('should handle if mapping with component props in SSR', async () => {
2031
2129
  const components: Record<string, ComponentDefinition> = {
2032
2130
  ConditionalBanner: {
2033
2131
  component: {
2034
2132
  interface: {
2035
- showBanner: { type: "boolean", default: false },
2133
+ showBanner: { type: 'boolean', default: false },
2036
2134
  },
2037
2135
  structure: {
2038
- type: "node",
2039
- tag: "div",
2136
+ type: 'node',
2137
+ tag: 'div',
2040
2138
  if: {
2041
2139
  _mapping: true,
2042
- prop: "showBanner",
2043
- values: { "true": true, "false": false },
2140
+ prop: 'showBanner',
2141
+ values: { true: true, false: false },
2044
2142
  },
2045
- children: ["Banner content"],
2143
+ children: ['Banner content'],
2046
2144
  } as any,
2047
2145
  },
2048
2146
  },
@@ -2051,32 +2149,32 @@ describe("SSR Renderer - Interactive Styles", () => {
2051
2149
  // Test with showBanner: true
2052
2150
  const pageDataVisible: JSONPage = {
2053
2151
  root: {
2054
- type: "component",
2055
- component: "ConditionalBanner",
2152
+ type: 'component',
2153
+ component: 'ConditionalBanner',
2056
2154
  props: { showBanner: true },
2057
2155
  } as any,
2058
2156
  };
2059
2157
 
2060
- const resultVisible = await renderPageSSR(pageDataVisible, components, "/", "");
2061
- expect(resultVisible.html).toContain("Banner content");
2158
+ const resultVisible = await renderPageSSR(pageDataVisible, components, '/', '');
2159
+ expect(resultVisible.html).toContain('Banner content');
2062
2160
 
2063
2161
  // Test with showBanner: false
2064
2162
  const pageDataHidden: JSONPage = {
2065
2163
  root: {
2066
- type: "component",
2067
- component: "ConditionalBanner",
2164
+ type: 'component',
2165
+ component: 'ConditionalBanner',
2068
2166
  props: { showBanner: false },
2069
2167
  } as any,
2070
2168
  };
2071
2169
 
2072
- const resultHidden = await renderPageSSR(pageDataHidden, components, "/", "");
2073
- expect(resultHidden.html).not.toContain("Banner content");
2170
+ const resultHidden = await renderPageSSR(pageDataHidden, components, '/', '');
2171
+ expect(resultHidden.html).not.toContain('Banner content');
2074
2172
  });
2075
2173
  });
2076
2174
  });
2077
2175
 
2078
- describe("SSR Renderer - List node rendering", () => {
2079
- test("should render children for each item in list prop", async () => {
2176
+ describe('SSR Renderer - List node rendering', () => {
2177
+ test('should render children for each item in list prop', async () => {
2080
2178
  // Component with list prop definition
2081
2179
  const components: Record<string, ComponentDefinition> = {
2082
2180
  ItemsList: {
@@ -2085,34 +2183,28 @@ describe("SSR Renderer - List node rendering", () => {
2085
2183
  items: {
2086
2184
  type: 'list',
2087
2185
  itemSchema: {
2088
- title: { type: 'string' }
2186
+ title: { type: 'string' },
2089
2187
  },
2090
- default: []
2091
- }
2188
+ default: [],
2189
+ },
2092
2190
  },
2093
2191
  structure: {
2094
- type: "list",
2095
- source: "items",
2096
- children: [
2097
- { type: "node", tag: "div", children: ["{{item.title}}"] }
2098
- ]
2099
- } as any
2100
- }
2101
- }
2192
+ type: 'list',
2193
+ source: 'items',
2194
+ children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
2195
+ } as any,
2196
+ },
2197
+ },
2102
2198
  };
2103
2199
 
2104
2200
  const pageData: JSONPage = {
2105
2201
  root: {
2106
- type: "component",
2107
- component: "ItemsList",
2202
+ type: 'component',
2203
+ component: 'ItemsList',
2108
2204
  props: {
2109
- items: [
2110
- { title: "First Item" },
2111
- { title: "Second Item" },
2112
- { title: "Third Item" }
2113
- ]
2114
- }
2115
- } as any
2205
+ items: [{ title: 'First Item' }, { title: 'Second Item' }, { title: 'Third Item' }],
2206
+ },
2207
+ } as any,
2116
2208
  };
2117
2209
 
2118
2210
  const result = await renderPageSSR(pageData, components, '/', '');
@@ -2122,7 +2214,7 @@ describe("SSR Renderer - List node rendering", () => {
2122
2214
  expect(result.html).toContain('Third Item');
2123
2215
  });
2124
2216
 
2125
- test("should provide item context variables (itemIndex, itemFirst, itemLast)", async () => {
2217
+ test('should provide item context variables (itemIndex, itemFirst, itemLast)', async () => {
2126
2218
  const components: Record<string, ComponentDefinition> = {
2127
2219
  TagsList: {
2128
2220
  component: {
@@ -2130,34 +2222,36 @@ describe("SSR Renderer - List node rendering", () => {
2130
2222
  tags: {
2131
2223
  type: 'list',
2132
2224
  itemSchema: {
2133
- name: { type: 'string' }
2225
+ name: { type: 'string' },
2134
2226
  },
2135
- default: []
2136
- }
2227
+ default: [],
2228
+ },
2137
2229
  },
2138
2230
  structure: {
2139
- type: "list",
2140
- source: "tags",
2141
- itemAs: "tag",
2231
+ type: 'list',
2232
+ source: 'tags',
2233
+ itemAs: 'tag',
2142
2234
  children: [
2143
- { type: "node", tag: "span", attributes: { 'data-index': '{{tagIndex}}', 'data-first': '{{tagFirst}}', 'data-last': '{{tagLast}}' }, children: ["{{tag.name}}"] }
2144
- ]
2145
- } as any
2146
- }
2147
- }
2235
+ {
2236
+ type: 'node',
2237
+ tag: 'span',
2238
+ attributes: { 'data-index': '{{tagIndex}}', 'data-first': '{{tagFirst}}', 'data-last': '{{tagLast}}' },
2239
+ children: ['{{tag.name}}'],
2240
+ },
2241
+ ],
2242
+ } as any,
2243
+ },
2244
+ },
2148
2245
  };
2149
2246
 
2150
2247
  const pageData: JSONPage = {
2151
2248
  root: {
2152
- type: "component",
2153
- component: "TagsList",
2249
+ type: 'component',
2250
+ component: 'TagsList',
2154
2251
  props: {
2155
- tags: [
2156
- { name: "Tag1" },
2157
- { name: "Tag2" }
2158
- ]
2159
- }
2160
- } as any
2252
+ tags: [{ name: 'Tag1' }, { name: 'Tag2' }],
2253
+ },
2254
+ } as any,
2161
2255
  };
2162
2256
 
2163
2257
  const result = await renderPageSSR(pageData, components, '/', '');
@@ -2170,7 +2264,7 @@ describe("SSR Renderer - List node rendering", () => {
2170
2264
  expect(result.html).toContain('Tag2');
2171
2265
  });
2172
2266
 
2173
- test("should render empty when list prop is empty", async () => {
2267
+ test('should render empty when list prop is empty', async () => {
2174
2268
  const components: Record<string, ComponentDefinition> = {
2175
2269
  EmptyList: {
2176
2270
  component: {
@@ -2178,30 +2272,28 @@ describe("SSR Renderer - List node rendering", () => {
2178
2272
  items: {
2179
2273
  type: 'list',
2180
2274
  itemSchema: {
2181
- title: { type: 'string' }
2275
+ title: { type: 'string' },
2182
2276
  },
2183
- default: []
2184
- }
2277
+ default: [],
2278
+ },
2185
2279
  },
2186
2280
  structure: {
2187
- type: "list",
2188
- source: "items",
2189
- children: [
2190
- { type: "node", tag: "div", children: ["{{item.title}}"] }
2191
- ]
2192
- } as any
2193
- }
2194
- }
2281
+ type: 'list',
2282
+ source: 'items',
2283
+ children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
2284
+ } as any,
2285
+ },
2286
+ },
2195
2287
  };
2196
2288
 
2197
2289
  const pageData: JSONPage = {
2198
2290
  root: {
2199
- type: "component",
2200
- component: "EmptyList",
2291
+ type: 'component',
2292
+ component: 'EmptyList',
2201
2293
  props: {
2202
- items: []
2203
- }
2204
- } as any
2294
+ items: [],
2295
+ },
2296
+ } as any,
2205
2297
  };
2206
2298
 
2207
2299
  const result = await renderPageSSR(pageData, components, '/', '');
@@ -2210,7 +2302,7 @@ describe("SSR Renderer - List node rendering", () => {
2210
2302
  expect(result.html).not.toContain('item.');
2211
2303
  });
2212
2304
 
2213
- test("should support nested lists", async () => {
2305
+ test('should support nested lists', async () => {
2214
2306
  const components: Record<string, ComponentDefinition> = {
2215
2307
  CategoryList: {
2216
2308
  component: {
@@ -2218,44 +2310,46 @@ describe("SSR Renderer - List node rendering", () => {
2218
2310
  categories: {
2219
2311
  type: 'list',
2220
2312
  itemSchema: {
2221
- name: { type: 'string' }
2313
+ name: { type: 'string' },
2222
2314
  },
2223
- default: []
2224
- }
2315
+ default: [],
2316
+ },
2225
2317
  },
2226
2318
  structure: {
2227
- type: "list",
2228
- source: "categories",
2229
- itemAs: "category",
2319
+ type: 'list',
2320
+ source: 'categories',
2321
+ itemAs: 'category',
2230
2322
  children: [
2231
- { type: "node", tag: "div", children: [
2232
- "{{category.name}}",
2233
- {
2234
- type: "list",
2235
- source: "{{category.items}}",
2236
- itemAs: "subItem",
2237
- children: [
2238
- { type: "node", tag: "span", children: ["{{subItem.label}}"] }
2239
- ]
2240
- }
2241
- ] }
2242
- ]
2243
- } as any
2244
- }
2245
- }
2323
+ {
2324
+ type: 'node',
2325
+ tag: 'div',
2326
+ children: [
2327
+ '{{category.name}}',
2328
+ {
2329
+ type: 'list',
2330
+ source: '{{category.items}}',
2331
+ itemAs: 'subItem',
2332
+ children: [{ type: 'node', tag: 'span', children: ['{{subItem.label}}'] }],
2333
+ },
2334
+ ],
2335
+ },
2336
+ ],
2337
+ } as any,
2338
+ },
2339
+ },
2246
2340
  };
2247
2341
 
2248
2342
  const pageData: JSONPage = {
2249
2343
  root: {
2250
- type: "component",
2251
- component: "CategoryList",
2344
+ type: 'component',
2345
+ component: 'CategoryList',
2252
2346
  props: {
2253
2347
  categories: [
2254
- { name: "Category A", items: [{ label: "Item A1" }, { label: "Item A2" }] },
2255
- { name: "Category B", items: [{ label: "Item B1" }] }
2256
- ]
2257
- }
2258
- } as any
2348
+ { name: 'Category A', items: [{ label: 'Item A1' }, { label: 'Item A2' }] },
2349
+ { name: 'Category B', items: [{ label: 'Item B1' }] },
2350
+ ],
2351
+ },
2352
+ } as any,
2259
2353
  };
2260
2354
 
2261
2355
  const result = await renderPageSSR(pageData, components, '/', '');