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,14 +1,15 @@
1
- import { test, expect, describe, beforeEach, mock } from "bun:test";
2
- import { ComponentBuilder } from "./ComponentBuilder";
3
- import { ComponentRegistry } from "../componentRegistry";
4
- import { ElementRegistry } from "../elementRegistry";
5
- import type { ComponentNode, ListNode, CMSItem } from "../../shared/types";
6
- import { NODE_TYPE } from "../../shared/constants";
7
- import { createMockElementRegistry } from "../../test-utils/mocks";
1
+ import { test, expect, describe, beforeEach, mock } from 'bun:test';
2
+ import { ComponentBuilder } from './ComponentBuilder';
3
+ import { ComponentRegistry } from '../componentRegistry';
4
+ import type { ElementRegistry } from '../elementRegistry';
5
+ import type { ComponentNode, ListNode, CMSItem } from '../../shared/types';
6
+ import { NODE_TYPE } from '../../shared/constants';
7
+ import { createMockElementRegistry } from '../../test-utils/mocks';
8
+ import { UtilityClassCollector } from '../styles/UtilityClassCollector';
8
9
 
9
10
  // Note: Using typed mocks from test-utils/mocks instead of inline 'as any' casts
10
11
 
11
- describe("ComponentBuilder", () => {
12
+ describe('ComponentBuilder', () => {
12
13
  let componentRegistry: ComponentRegistry;
13
14
  let elementRegistry: ElementRegistry;
14
15
  let builder: ComponentBuilder;
@@ -32,52 +33,52 @@ describe("ComponentBuilder", () => {
32
33
  });
33
34
  });
34
35
 
35
- describe("buildComponent - Basic Cases", () => {
36
- test("should return null for null node", () => {
36
+ describe('buildComponent - Basic Cases', () => {
37
+ test('should return null for null node', () => {
37
38
  const result = builder.buildComponent({ node: null });
38
39
  expect(result).toBeNull();
39
40
  });
40
41
 
41
- test("should return null for undefined node", () => {
42
+ test('should return null for undefined node', () => {
42
43
  const result = builder.buildComponent({ node: undefined });
43
44
  expect(result).toBeNull();
44
45
  });
45
46
 
46
- test("should return string for string node", () => {
47
- const result = builder.buildComponent({ node: "Hello" });
48
- expect(result).toBe("Hello");
47
+ test('should return string for string node', () => {
48
+ const result = builder.buildComponent({ node: 'Hello' });
49
+ expect(result).toBe('Hello');
49
50
  });
50
51
 
51
- test("should return number for number node", () => {
52
+ test('should return number for number node', () => {
52
53
  const result = builder.buildComponent({ node: 42 });
53
54
  expect(result).toBe(42);
54
55
  });
55
56
 
56
- test("should return array for array of nodes", () => {
57
+ test('should return array for array of nodes', () => {
57
58
  const nodes: ComponentNode[] = [
58
- { type: "node", tag: "div", children: ["Hello"] },
59
- { type: "node", tag: "span", children: ["World"] },
59
+ { type: 'node', tag: 'div', children: ['Hello'] },
60
+ { type: 'node', tag: 'span', children: ['World'] },
60
61
  ];
61
62
  const result = builder.buildComponent({ node: nodes });
62
63
  expect(Array.isArray(result)).toBe(true);
63
64
  expect((result as any[]).length).toBe(2);
64
65
  });
65
66
 
66
- test("should return ReactElement for HTML node", () => {
67
+ test('should return ReactElement for HTML node', () => {
67
68
  const node: ComponentNode = {
68
- type: "node",
69
- tag: "div",
70
- children: ["Test"],
69
+ type: 'node',
70
+ tag: 'div',
71
+ children: ['Test'],
71
72
  };
72
73
  const result = builder.buildComponent({ node });
73
74
  expect(result).not.toBeNull();
74
- expect(typeof result).toBe("object");
75
+ expect(typeof result).toBe('object');
75
76
  });
76
77
 
77
- test("should handle HTML node with empty tag gracefully", () => {
78
+ test('should handle HTML node with empty tag gracefully', () => {
78
79
  const node: ComponentNode = {
79
- type: "node",
80
- tag: "",
80
+ type: 'node',
81
+ tag: '',
81
82
  children: [],
82
83
  };
83
84
 
@@ -85,124 +86,124 @@ describe("ComponentBuilder", () => {
85
86
 
86
87
  // Implementation returns a div element for empty tag (graceful fallback)
87
88
  expect(result).not.toBeNull();
88
- expect(typeof result).toBe("object");
89
+ expect(typeof result).toBe('object');
89
90
  });
90
91
 
91
- test("resolves an _i18n value object as the node to the default locale string", () => {
92
+ test('resolves an _i18n value object as the node to the default locale string', () => {
92
93
  // Mirrors the SSR behavior: authors can pass localized strings anywhere
93
94
  // `children` is accepted. The client resolves _i18n at the buildComponent
94
95
  // entry point so all downstream code sees a plain string.
95
96
  const i18nConfig = {
96
- defaultLocale: "en",
97
+ defaultLocale: 'en',
97
98
  locales: [
98
- { code: "en", name: "EN", nativeName: "English", langTag: "en-US" },
99
- { code: "pl", name: "PL", nativeName: "Polski", langTag: "pl-PL" },
99
+ { code: 'en', name: 'EN', nativeName: 'English', langTag: 'en-US' },
100
+ { code: 'pl', name: 'PL', nativeName: 'Polski', langTag: 'pl-PL' },
100
101
  ],
101
102
  };
102
103
  const result = builder.buildComponent({
103
- node: { _i18n: true, en: "Hello", pl: "Cześć" } as any,
104
+ node: { _i18n: true, en: 'Hello', pl: 'Cześć' } as any,
104
105
  i18nConfig,
105
106
  });
106
- expect(result).toBe("Hello");
107
+ expect(result).toBe('Hello');
107
108
  });
108
109
 
109
- test("resolves an _i18n value object to the active locale", () => {
110
+ test('resolves an _i18n value object to the active locale', () => {
110
111
  const i18nConfig = {
111
- defaultLocale: "en",
112
+ defaultLocale: 'en',
112
113
  locales: [
113
- { code: "en", name: "EN", nativeName: "English", langTag: "en-US" },
114
- { code: "pl", name: "PL", nativeName: "Polski", langTag: "pl-PL" },
114
+ { code: 'en', name: 'EN', nativeName: 'English', langTag: 'en-US' },
115
+ { code: 'pl', name: 'PL', nativeName: 'Polski', langTag: 'pl-PL' },
115
116
  ],
116
117
  };
117
118
  const result = builder.buildComponent({
118
- node: { _i18n: true, en: "Hello", pl: "Cześć" } as any,
119
+ node: { _i18n: true, en: 'Hello', pl: 'Cześć' } as any,
119
120
  i18nConfig,
120
- locale: "pl",
121
+ locale: 'pl',
121
122
  });
122
- expect(result).toBe("Cześć");
123
+ expect(result).toBe('Cześć');
123
124
  });
124
125
  });
125
126
 
126
- describe("buildComponent - Component Instances", () => {
127
- test("should handle non-existent component gracefully", () => {
127
+ describe('buildComponent - Component Instances', () => {
128
+ test('should handle non-existent component gracefully', () => {
128
129
  const node: ComponentNode = {
129
- type: "component",
130
- component: "NonExistent",
130
+ type: 'component',
131
+ component: 'NonExistent',
131
132
  };
132
133
  const result = builder.buildComponent({ node });
133
134
  // Returns error boundary or placeholder element for missing component
134
135
  expect(result).not.toBeNull();
135
- expect(typeof result).toBe("object");
136
+ expect(typeof result).toBe('object');
136
137
  });
137
138
 
138
- test("should build component instance with structure", () => {
139
+ test('should build component instance with structure', () => {
139
140
  const componentDef = {
140
141
  component: {
141
142
  interface: {},
142
143
  structure: {
143
- type: "node",
144
- tag: "div",
145
- children: ["Component Content"],
144
+ type: 'node',
145
+ tag: 'div',
146
+ children: ['Component Content'],
146
147
  },
147
148
  },
148
149
  };
149
- componentRegistry.register("TestComponent", componentDef as any);
150
+ componentRegistry.register('TestComponent', componentDef as any);
150
151
 
151
152
  const node: ComponentNode = {
152
- type: "component",
153
- component: "TestComponent",
153
+ type: 'component',
154
+ component: 'TestComponent',
154
155
  };
155
156
  const result = builder.buildComponent({ node });
156
157
  expect(result).not.toBeNull();
157
- expect(typeof result).toBe("object");
158
+ expect(typeof result).toBe('object');
158
159
  });
159
160
 
160
- test("should handle component missing structure gracefully", () => {
161
+ test('should handle component missing structure gracefully', () => {
161
162
  // Test with component that has no component property at all
162
163
  const componentDef = {
163
164
  // Missing component property entirely
164
165
  };
165
- componentRegistry.register("BrokenComponent", componentDef as any);
166
+ componentRegistry.register('BrokenComponent', componentDef as any);
166
167
 
167
168
  const node: ComponentNode = {
168
- type: "component",
169
- component: "BrokenComponent",
169
+ type: 'component',
170
+ component: 'BrokenComponent',
170
171
  };
171
172
 
172
173
  const result = builder.buildComponent({ node });
173
174
 
174
175
  // When componentDef.component is missing, it should return ErrorBoundary
175
176
  expect(result).not.toBeNull();
176
- expect(typeof result).toBe("object");
177
+ expect(typeof result).toBe('object');
177
178
  });
178
179
 
179
- test("should resolve props with defaults from interface", () => {
180
+ test('should resolve props with defaults from interface', () => {
180
181
  const componentDef = {
181
182
  component: {
182
183
  interface: {
183
184
  title: {
184
- type: "string",
185
- default: "Default Title",
185
+ type: 'string',
186
+ default: 'Default Title',
186
187
  },
187
188
  },
188
189
  structure: {
189
- type: "node",
190
- tag: "div",
190
+ type: 'node',
191
+ tag: 'div',
191
192
  children: [],
192
193
  },
193
194
  },
194
195
  };
195
- componentRegistry.register("ComponentWithDefaults", componentDef as any);
196
+ componentRegistry.register('ComponentWithDefaults', componentDef as any);
196
197
 
197
198
  const node: ComponentNode = {
198
- type: "component",
199
- component: "ComponentWithDefaults",
199
+ type: 'component',
200
+ component: 'ComponentWithDefaults',
200
201
  };
201
202
  const result = builder.buildComponent({ node });
202
203
  expect(result).not.toBeNull();
203
204
  });
204
205
 
205
- test("should handle component building errors gracefully", () => {
206
+ test('should handle component building errors gracefully', () => {
206
207
  // This test verifies error handling is in place
207
208
  // We can't easily mock processStructure in Bun, so we test with a valid component
208
209
  // Error handling is verified by the try-catch in buildComponent
@@ -210,65 +211,65 @@ describe("ComponentBuilder", () => {
210
211
  component: {
211
212
  interface: {},
212
213
  structure: {
213
- type: "node",
214
- tag: "div",
214
+ type: 'node',
215
+ tag: 'div',
215
216
  children: [],
216
217
  },
217
218
  },
218
219
  };
219
- componentRegistry.register("ValidComponent", componentDef as any);
220
+ componentRegistry.register('ValidComponent', componentDef as any);
220
221
 
221
222
  const node: ComponentNode = {
222
- type: "component",
223
- component: "ValidComponent",
223
+ type: 'component',
224
+ component: 'ValidComponent',
224
225
  };
225
-
226
+
226
227
  const result = builder.buildComponent({ node });
227
-
228
+
228
229
  // Component should build successfully
229
230
  expect(result).not.toBeNull();
230
- expect(typeof result).toBe("object");
231
+ expect(typeof result).toBe('object');
231
232
  });
232
233
  });
233
234
 
234
- describe("buildComponent - Link Component", () => {
235
- test("should render Link component as anchor tag", () => {
235
+ describe('buildComponent - Link Component', () => {
236
+ test('should render Link component as anchor tag', () => {
236
237
  const node: ComponentNode = {
237
- type: "node",
238
- tag: "Link",
238
+ type: 'node',
239
+ tag: 'Link',
239
240
  props: {
240
- to: "/about",
241
+ to: '/about',
241
242
  },
242
- children: ["Click me"],
243
+ children: ['Click me'],
243
244
  };
244
245
  const result = builder.buildComponent({ node });
245
246
  expect(result).not.toBeNull();
246
- expect(typeof result).toBe("object");
247
+ expect(typeof result).toBe('object');
247
248
  });
248
249
 
249
- test("should handle Link navigation when selecting mode is disabled", () => {
250
+ test('should handle Link navigation when selecting mode is disabled', () => {
250
251
  const node: ComponentNode = {
251
- type: "node",
252
- tag: "Link",
252
+ type: 'node',
253
+ tag: 'Link',
253
254
  props: {
254
- to: "/test",
255
+ to: '/test',
255
256
  },
256
257
  children: [],
257
258
  };
258
-
259
+
259
260
  // Link component should render regardless of selecting mode
260
261
  // The actual behavior is tested through integration tests
261
262
  const result = builder.buildComponent({ node });
262
263
  expect(result).not.toBeNull();
263
- expect(typeof result).toBe("object");
264
+ expect(typeof result).toBe('object');
264
265
  });
265
266
  });
266
267
 
267
- describe("buildComponent - HTML Elements", () => {
268
- test("should register element via ref callback", () => {
268
+ describe('buildComponent - HTML Elements', () => {
269
+ test('should register element via ref callback', () => {
269
270
  const node: ComponentNode = {
270
- type: "node",
271
- tag: "div",
271
+ type: 'node',
272
+ tag: 'div',
272
273
  children: [],
273
274
  };
274
275
  const result = builder.buildComponent({ node });
@@ -277,14 +278,14 @@ describe("ComponentBuilder", () => {
277
278
  // We can't easily test this without rendering, but we verify the ref is set
278
279
  });
279
280
 
280
- test("should extract and merge styles", () => {
281
+ test('should extract and merge styles', () => {
281
282
  const node: ComponentNode = {
282
- type: "node",
283
- tag: "div",
283
+ type: 'node',
284
+ tag: 'div',
284
285
  style: {
285
286
  base: {
286
- color: "red",
287
- fontSize: "16px",
287
+ color: 'red',
288
+ fontSize: '16px',
288
289
  },
289
290
  },
290
291
  children: [],
@@ -293,13 +294,13 @@ describe("ComponentBuilder", () => {
293
294
  expect(result).not.toBeNull();
294
295
  });
295
296
 
296
- test("should extract and merge attributes", () => {
297
+ test('should extract and merge attributes', () => {
297
298
  const node: ComponentNode = {
298
- type: "node",
299
- tag: "div",
299
+ type: 'node',
300
+ tag: 'div',
300
301
  attributes: {
301
- id: "test-id",
302
- className: "test-class",
302
+ id: 'test-id',
303
+ className: 'test-class',
303
304
  },
304
305
  children: [],
305
306
  };
@@ -307,16 +308,38 @@ describe("ComponentBuilder", () => {
307
308
  expect(result).not.toBeNull();
308
309
  });
309
310
 
310
- test("should filter out internal props", () => {
311
+ test('class-string styling: utility tokens in attributes.class inject canvas CSS', () => {
312
+ // The design canvas builds React elements via ComponentBuilder. A node styled via
313
+ // attributes.class (not node.style) must still get its utility CSS injected into the
314
+ // live <style id="utility-css">, exactly like the node.style path — else the class
315
+ // lands on the element but renders unstyled.
316
+ UtilityClassCollector.destroy();
311
317
  const node: ComponentNode = {
312
- type: "node",
313
- tag: "div",
318
+ type: 'node',
319
+ tag: 'p',
320
+ attributes: { class: 'p-[24px] swiper' },
321
+ children: ['styled'],
322
+ };
323
+ const result = builder.buildComponent({ node });
324
+ expect(result).not.toBeNull();
325
+ const css = document.getElementById('utility-css')?.textContent || '';
326
+ // Utility token p-[24px] generated a rule...
327
+ expect(css).toContain('padding: 24px');
328
+ // ...and the foreign token (swiper) produced none (collect() skips no-rule tokens).
329
+ expect(css).not.toContain('swiper');
330
+ UtilityClassCollector.destroy();
331
+ });
332
+
333
+ test('should filter out internal props', () => {
334
+ const node: ComponentNode = {
335
+ type: 'node',
336
+ tag: 'div',
314
337
  props: {
315
- type: "should-be-removed",
316
- tag: "should-be-removed",
317
- component: "should-be-removed",
318
- children: "should-be-removed",
319
- validProp: "should-remain",
338
+ type: 'should-be-removed',
339
+ tag: 'should-be-removed',
340
+ component: 'should-be-removed',
341
+ children: 'should-be-removed',
342
+ validProp: 'should-remain',
320
343
  },
321
344
  children: [],
322
345
  };
@@ -324,44 +347,44 @@ describe("ComponentBuilder", () => {
324
347
  expect(result).not.toBeNull();
325
348
  });
326
349
 
327
- test("should detect component root via __componentProps", () => {
350
+ test('should detect component root via __componentProps', () => {
328
351
  const node: ComponentNode = {
329
- type: "node",
330
- tag: "div",
352
+ type: 'node',
353
+ tag: 'div',
331
354
  children: [],
332
355
  };
333
356
  const result = builder.buildComponent({
334
357
  node,
335
358
  customProps: {
336
- __componentProps: { title: "Test" },
359
+ __componentProps: { title: 'Test' },
337
360
  },
338
361
  });
339
362
  expect(result).not.toBeNull();
340
363
  });
341
364
 
342
- test("should remap body tag to div to keep SSR and client DOMs identical", () => {
365
+ test('should remap body tag to div to keep SSR and client DOMs identical', () => {
343
366
  const node: ComponentNode = {
344
- type: "node",
345
- tag: "body",
346
- attributes: { className: "page-root" },
347
- children: ["Content"],
367
+ type: 'node',
368
+ tag: 'body',
369
+ attributes: { className: 'page-root' },
370
+ children: ['Content'],
348
371
  };
349
372
  const result = builder.buildComponent({ node }) as any;
350
373
  expect(result).not.toBeNull();
351
- expect(result.type).toBe("div");
352
- expect(result.props.className).toBe("page-root");
374
+ expect(result.type).toBe('div');
375
+ expect(result.props.className).toBe('page-root');
353
376
  });
354
377
 
355
- test("should remap html and head tags to div", () => {
356
- for (const tag of ["html", "head", "BODY"] as const) {
357
- const node: ComponentNode = { type: "node", tag, children: [] };
378
+ test('should remap html and head tags to div', () => {
379
+ for (const tag of ['html', 'head', 'BODY'] as const) {
380
+ const node: ComponentNode = { type: 'node', tag, children: [] };
358
381
  const result = builder.buildComponent({ node }) as any;
359
- expect(result.type).toBe("div");
382
+ expect(result.type).toBe('div');
360
383
  }
361
384
  });
362
385
  });
363
386
 
364
- describe("buildChildren", () => {
387
+ describe('buildChildren', () => {
365
388
  const defaultCtx = {
366
389
  elementPath: [0] as number[],
367
390
  parentComponentName: null,
@@ -369,42 +392,42 @@ describe("ComponentBuilder", () => {
369
392
  componentContext: null,
370
393
  };
371
394
 
372
- test("should return array for array of children", () => {
395
+ test('should return array for array of children', () => {
373
396
  const children: ComponentNode[] = [
374
- { type: "node", tag: "div", children: ["Child 1"] },
375
- { type: "node", tag: "span", children: ["Child 2"] },
397
+ { type: 'node', tag: 'div', children: ['Child 1'] },
398
+ { type: 'node', tag: 'span', children: ['Child 2'] },
376
399
  ];
377
400
  const result = builder.buildChildren(children, defaultCtx);
378
401
  expect(Array.isArray(result)).toBe(true);
379
402
  expect((result as any[]).length).toBe(2);
380
403
  });
381
404
 
382
- test("should return single element for single child", () => {
405
+ test('should return single element for single child', () => {
383
406
  const child: ComponentNode = {
384
- type: "node",
385
- tag: "div",
386
- children: ["Single child"],
407
+ type: 'node',
408
+ tag: 'div',
409
+ children: ['Single child'],
387
410
  };
388
411
  const result = builder.buildChildren(child, defaultCtx);
389
412
  expect(result).not.toBeNull();
390
413
  expect(Array.isArray(result)).toBe(false);
391
414
  });
392
415
 
393
- test("should return null for undefined children", () => {
416
+ test('should return null for undefined children', () => {
394
417
  const result = builder.buildChildren(undefined, defaultCtx);
395
418
  expect(result).toBeNull();
396
419
  });
397
420
 
398
- test("should return empty array for empty array", () => {
421
+ test('should return empty array for empty array', () => {
399
422
  const result = builder.buildChildren([], defaultCtx);
400
423
  expect(Array.isArray(result)).toBe(true);
401
424
  expect((result as any[]).length).toBe(0);
402
425
  });
403
426
 
404
- test("should generate correct paths for children", () => {
427
+ test('should generate correct paths for children', () => {
405
428
  const children: ComponentNode[] = [
406
- { type: "node", tag: "div", children: [] },
407
- { type: "node", tag: "span", children: [] },
429
+ { type: 'node', tag: 'div', children: [] },
430
+ { type: 'node', tag: 'span', children: [] },
408
431
  ];
409
432
  const result = builder.buildChildren(children, { ...defaultCtx, elementPath: [0, 1] });
410
433
  expect(Array.isArray(result)).toBe(true);
@@ -412,51 +435,51 @@ describe("ComponentBuilder", () => {
412
435
  });
413
436
  });
414
437
 
415
- describe("Helper Functions", () => {
416
- test("getParentComponentNameForNestedComponent should prioritize componentContext", () => {
438
+ describe('Helper Functions', () => {
439
+ test('getParentComponentNameForNestedComponent should prioritize componentContext', () => {
417
440
  // This is tested indirectly through component building
418
441
  // We can't directly test private methods, but we can verify behavior
419
442
  const componentDef = {
420
443
  component: {
421
444
  interface: {},
422
445
  structure: {
423
- type: "node",
424
- tag: "div",
446
+ type: 'node',
447
+ tag: 'div',
425
448
  children: [],
426
449
  },
427
450
  },
428
451
  };
429
- componentRegistry.register("ParentComponent", componentDef as any);
452
+ componentRegistry.register('ParentComponent', componentDef as any);
430
453
 
431
454
  const node: ComponentNode = {
432
- type: "component",
433
- component: "ParentComponent",
455
+ type: 'component',
456
+ component: 'ParentComponent',
434
457
  };
435
458
  const result = builder.buildComponent({
436
459
  node,
437
- componentContext: "ContextComponent",
438
- parentComponentName: "ParentName",
460
+ componentContext: 'ContextComponent',
461
+ parentComponentName: 'ParentName',
439
462
  });
440
463
  expect(result).not.toBeNull();
441
464
  });
442
465
 
443
- test("getEffectiveParentComponentName should use componentContext when available", () => {
466
+ test('getEffectiveParentComponentName should use componentContext when available', () => {
444
467
  // Tested indirectly through component building
445
468
  const node: ComponentNode = {
446
- type: "node",
447
- tag: "div",
469
+ type: 'node',
470
+ tag: 'div',
448
471
  children: [],
449
472
  };
450
473
  const result = builder.buildComponent({
451
474
  node,
452
- componentContext: "ContextComponent",
453
- parentComponentName: "ParentName",
475
+ componentContext: 'ContextComponent',
476
+ parentComponentName: 'ParentName',
454
477
  });
455
478
  expect(result).not.toBeNull();
456
479
  });
457
480
  });
458
481
 
459
- describe("Path Assignment for Slot Markers", () => {
482
+ describe('Path Assignment for Slot Markers', () => {
460
483
  /**
461
484
  * KNOWN ISSUE: Slot markers consume path indices incorrectly
462
485
  *
@@ -474,21 +497,18 @@ describe("ComponentBuilder", () => {
474
497
  * TODO: Fix path assignment to skip slot markers when assigning indices,
475
498
  * or adjust indices after slot expansion.
476
499
  */
477
- test.skip("slot markers should not consume path indices", () => {
500
+ test.skip('slot markers should not consume path indices', () => {
478
501
  const sectionDef = {
479
502
  component: {
480
503
  interface: {},
481
504
  structure: {
482
- type: "node",
483
- tag: "div",
505
+ type: 'node',
506
+ tag: 'div',
484
507
  children: [
485
508
  {
486
- type: "node",
487
- tag: "div",
488
- children: [
489
- { type: "slot" },
490
- { type: "component", component: "Card" },
491
- ],
509
+ type: 'node',
510
+ tag: 'div',
511
+ children: [{ type: 'slot' }, { type: 'component', component: 'Card' }],
492
512
  },
493
513
  ],
494
514
  },
@@ -499,20 +519,20 @@ describe("ComponentBuilder", () => {
499
519
  component: {
500
520
  interface: {},
501
521
  structure: {
502
- type: "node",
503
- tag: "div",
504
- children: ["Card Content"],
522
+ type: 'node',
523
+ tag: 'div',
524
+ children: ['Card Content'],
505
525
  },
506
526
  },
507
527
  };
508
528
 
509
- componentRegistry.register("Section", sectionDef as any);
510
- componentRegistry.register("Card", cardDef as any);
529
+ componentRegistry.register('Section', sectionDef as any);
530
+ componentRegistry.register('Card', cardDef as any);
511
531
 
512
532
  const pageNode: ComponentNode = {
513
- type: "component",
514
- component: "Section",
515
- children: [{ type: "node", tag: "p", children: ["Instance Child"] }],
533
+ type: 'component',
534
+ component: 'Section',
535
+ children: [{ type: 'node', tag: 'p', children: ['Instance Child'] }],
516
536
  };
517
537
 
518
538
  const result = builder.buildComponent({ node: pageNode });
@@ -524,51 +544,51 @@ describe("ComponentBuilder", () => {
524
544
  // OR Card gets a path that properly reflects its position relative to expanded slot content
525
545
  });
526
546
 
527
- test("should build component with slot marker correctly", () => {
547
+ test('should build component with slot marker correctly', () => {
528
548
  // Button.json: button > children
529
549
  const buttonDef = {
530
550
  component: {
531
551
  interface: {
532
- children: { type: "string", default: "Click me" },
552
+ children: { type: 'string', default: 'Click me' },
533
553
  },
534
554
  structure: {
535
- type: "node",
536
- tag: "button",
537
- children: [{ type: "slot" }],
555
+ type: 'node',
556
+ tag: 'button',
557
+ children: [{ type: 'slot' }],
538
558
  },
539
559
  },
540
560
  };
541
561
 
542
- componentRegistry.register("Button", buttonDef as any);
562
+ componentRegistry.register('Button', buttonDef as any);
543
563
 
544
564
  const pageNode: ComponentNode = {
545
- type: "component",
546
- component: "Button",
547
- children: [{ type: "node", tag: "span", children: ["Submit"] }],
565
+ type: 'component',
566
+ component: 'Button',
567
+ children: [{ type: 'node', tag: 'span', children: ['Submit'] }],
548
568
  };
549
569
 
550
570
  const result = builder.buildComponent({ node: pageNode });
551
571
  expect(result).not.toBeNull();
552
- expect(typeof result).toBe("object");
572
+ expect(typeof result).toBe('object');
553
573
  });
554
574
 
555
- test("should build nested children with multiple siblings after slot", () => {
575
+ test('should build nested children with multiple siblings after slot', () => {
556
576
  // Structure: div > div > [slot, Card, Button]
557
577
 
558
578
  const sectionDef = {
559
579
  component: {
560
580
  interface: {},
561
581
  structure: {
562
- type: "node",
563
- tag: "div",
582
+ type: 'node',
583
+ tag: 'div',
564
584
  children: [
565
585
  {
566
- type: "node",
567
- tag: "div",
586
+ type: 'node',
587
+ tag: 'div',
568
588
  children: [
569
- { type: "slot" },
570
- { type: "component", component: "Card" },
571
- { type: "component", component: "Button" },
589
+ { type: 'slot' },
590
+ { type: 'component', component: 'Card' },
591
+ { type: 'component', component: 'Button' },
572
592
  ],
573
593
  },
574
594
  ],
@@ -580,9 +600,9 @@ describe("ComponentBuilder", () => {
580
600
  component: {
581
601
  interface: {},
582
602
  structure: {
583
- type: "node",
584
- tag: "div",
585
- children: ["Card"],
603
+ type: 'node',
604
+ tag: 'div',
605
+ children: ['Card'],
586
606
  },
587
607
  },
588
608
  };
@@ -591,57 +611,54 @@ describe("ComponentBuilder", () => {
591
611
  component: {
592
612
  interface: {},
593
613
  structure: {
594
- type: "node",
595
- tag: "button",
596
- children: ["Button"],
614
+ type: 'node',
615
+ tag: 'button',
616
+ children: ['Button'],
597
617
  },
598
618
  },
599
619
  };
600
620
 
601
- componentRegistry.register("Section", sectionDef as any);
602
- componentRegistry.register("Card", cardDef as any);
603
- componentRegistry.register("Button", buttonDef as any);
621
+ componentRegistry.register('Section', sectionDef as any);
622
+ componentRegistry.register('Card', cardDef as any);
623
+ componentRegistry.register('Button', buttonDef as any);
604
624
 
605
625
  const pageNode: ComponentNode = {
606
- type: "component",
607
- component: "Section",
608
- children: [{ type: "node", tag: "p", children: ["Content"] }],
626
+ type: 'component',
627
+ component: 'Section',
628
+ children: [{ type: 'node', tag: 'p', children: ['Content'] }],
609
629
  };
610
630
 
611
631
  const result = builder.buildComponent({ node: pageNode });
612
632
  expect(result).not.toBeNull();
613
- expect(typeof result).toBe("object");
633
+ expect(typeof result).toBe('object');
614
634
  });
615
635
  });
616
636
 
617
- describe("Integration Tests", () => {
618
- test("should build complex component tree", () => {
637
+ describe('Integration Tests', () => {
638
+ test('should build complex component tree', () => {
619
639
  const cardDef = {
620
640
  component: {
621
641
  interface: {
622
- title: { type: "string", default: "Card Title" },
642
+ title: { type: 'string', default: 'Card Title' },
623
643
  },
624
644
  structure: {
625
- type: "node",
626
- tag: "div",
627
- children: [
628
- { type: "node", tag: "h2", children: ["{{title}}"] },
629
- { type: "slot" },
630
- ],
645
+ type: 'node',
646
+ tag: 'div',
647
+ children: [{ type: 'node', tag: 'h2', children: ['{{title}}'] }, { type: 'slot' }],
631
648
  },
632
649
  },
633
650
  };
634
- componentRegistry.register("Card", cardDef as any);
651
+ componentRegistry.register('Card', cardDef as any);
635
652
 
636
653
  const pageNode: ComponentNode = {
637
- type: "node",
638
- tag: "div",
654
+ type: 'node',
655
+ tag: 'div',
639
656
  children: [
640
657
  {
641
- type: "component",
642
- component: "Card",
643
- props: { title: "My Card" },
644
- children: [{ type: "node", tag: "p", children: ["Card content"] }],
658
+ type: 'component',
659
+ component: 'Card',
660
+ props: { title: 'My Card' },
661
+ children: [{ type: 'node', tag: 'p', children: ['Card content'] }],
645
662
  },
646
663
  ],
647
664
  };
@@ -649,90 +666,86 @@ describe("ComponentBuilder", () => {
649
666
  expect(result).not.toBeNull();
650
667
  });
651
668
 
652
- test("should handle nested component instances", () => {
669
+ test('should handle nested component instances', () => {
653
670
  const innerDef = {
654
671
  component: {
655
672
  interface: {},
656
673
  structure: {
657
- type: "node",
658
- tag: "span",
659
- children: ["Inner"],
674
+ type: 'node',
675
+ tag: 'span',
676
+ children: ['Inner'],
660
677
  },
661
678
  },
662
679
  };
663
- componentRegistry.register("Inner", innerDef as any);
680
+ componentRegistry.register('Inner', innerDef as any);
664
681
 
665
682
  const outerDef = {
666
683
  component: {
667
684
  interface: {},
668
685
  structure: {
669
- type: "node",
670
- tag: "div",
671
- children: [
672
- { type: "component", component: "Inner" },
673
- ],
686
+ type: 'node',
687
+ tag: 'div',
688
+ children: [{ type: 'component', component: 'Inner' }],
674
689
  },
675
690
  },
676
691
  };
677
- componentRegistry.register("Outer", outerDef as any);
692
+ componentRegistry.register('Outer', outerDef as any);
678
693
 
679
694
  const node: ComponentNode = {
680
- type: "component",
681
- component: "Outer",
695
+ type: 'component',
696
+ component: 'Outer',
682
697
  };
683
698
  const result = builder.buildComponent({ node });
684
699
  expect(result).not.toBeNull();
685
700
  });
686
701
 
687
- test("should propagate component context correctly", () => {
702
+ test('should propagate component context correctly', () => {
688
703
  const componentDef = {
689
704
  component: {
690
705
  interface: {},
691
706
  structure: {
692
- type: "node",
693
- tag: "div",
694
- children: [
695
- { type: "node", tag: "span", children: ["Child"] },
696
- ],
707
+ type: 'node',
708
+ tag: 'div',
709
+ children: [{ type: 'node', tag: 'span', children: ['Child'] }],
697
710
  },
698
711
  },
699
712
  };
700
- componentRegistry.register("TestComponent", componentDef as any);
713
+ componentRegistry.register('TestComponent', componentDef as any);
701
714
 
702
715
  const node: ComponentNode = {
703
- type: "component",
704
- component: "TestComponent",
716
+ type: 'component',
717
+ component: 'TestComponent',
705
718
  };
706
719
  const result = builder.buildComponent({
707
720
  node,
708
- componentContext: "TestComponent",
721
+ componentContext: 'TestComponent',
709
722
  });
710
723
  expect(result).not.toBeNull();
711
724
  });
712
725
 
713
- test("should respect viewportWidth for responsive styles", () => {
726
+ test('should respect viewportWidth for responsive styles', () => {
714
727
  const node: ComponentNode = {
715
- type: "node",
716
- tag: "div",
728
+ type: 'node',
729
+ tag: 'div',
717
730
  style: {
718
- base: { fontSize: "24px" },
719
- tablet: { fontSize: "18px" },
720
- mobile: { fontSize: "16px" },
731
+ base: { fontSize: '24px' },
732
+ tablet: { fontSize: '18px' },
733
+ mobile: { fontSize: '16px' },
721
734
  },
722
735
  children: [],
723
736
  };
724
-
737
+
725
738
  const resultDesktop = builder.buildComponent({ node, viewportWidth: 1920 });
726
739
  const resultTablet = builder.buildComponent({ node, viewportWidth: 768 });
727
740
  const resultMobile = builder.buildComponent({ node, viewportWidth: 375 });
728
-
741
+
729
742
  expect(resultDesktop).not.toBeNull();
730
743
  expect(resultTablet).not.toBeNull();
731
744
  expect(resultMobile).not.toBeNull();
732
745
  });
733
746
  });
734
747
 
735
- describe("buildComponent - List (Collection Mode)", () => {
748
+ describe('buildComponent - List (Collection Mode)', () => {
736
749
  // Helper to create mock CMS items
737
750
  const createMockCMSItems = (count: number): CMSItem[] => {
738
751
  return Array.from({ length: count }, (_, i) => ({
@@ -747,7 +760,7 @@ describe("ComponentBuilder", () => {
747
760
  const createCollectionListNode = (
748
761
  source: string,
749
762
  children: ComponentNode[],
750
- options: { limit?: number; offset?: number } = {}
763
+ options: { limit?: number; offset?: number } = {},
751
764
  ): ListNode => ({
752
765
  type: NODE_TYPE.LIST,
753
766
  sourceType: 'collection',
@@ -756,11 +769,9 @@ describe("ComponentBuilder", () => {
756
769
  ...options,
757
770
  });
758
771
 
759
- test("should render children for each CMS item", () => {
772
+ test('should render children for each CMS item', () => {
760
773
  const items = createMockCMSItems(3);
761
- const node = createCollectionListNode("posts", [
762
- { type: "node", tag: "div", children: ["Item content"] },
763
- ]);
774
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item content'] }]);
764
775
 
765
776
  const result = builder.buildComponent({
766
777
  node: node as unknown as ComponentNode,
@@ -768,18 +779,16 @@ describe("ComponentBuilder", () => {
768
779
  });
769
780
 
770
781
  expect(result).not.toBeNull();
771
- expect(typeof result).toBe("object");
782
+ expect(typeof result).toBe('object');
772
783
  // Result should have props.children with 3 items
773
784
  const element = result as { props?: { children?: unknown[] } };
774
785
  expect(Array.isArray(element?.props?.children)).toBe(true);
775
786
  expect(element?.props?.children?.length).toBe(3);
776
787
  });
777
788
 
778
- test("should add data-cms-item-index attribute to each item wrapper", () => {
789
+ test('should add data-cms-item-index attribute to each item wrapper', () => {
779
790
  const items = createMockCMSItems(3);
780
- const node = createCollectionListNode("posts", [
781
- { type: "node", tag: "div", children: ["Item content"] },
782
- ]);
791
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item content'] }]);
783
792
 
784
793
  const result = builder.buildComponent({
785
794
  node: node as unknown as ComponentNode,
@@ -801,13 +810,9 @@ describe("ComponentBuilder", () => {
801
810
  });
802
811
  });
803
812
 
804
- test("should interpolate {{item.field}} in text", () => {
805
- const items: CMSItem[] = [
806
- { _id: "1", _slug: "test", title: "Hello World" },
807
- ];
808
- const node = createCollectionListNode("posts", [
809
- { type: "node", tag: "h1", children: ["{{item.title}}"] },
810
- ]);
813
+ test('should interpolate {{item.field}} in text', () => {
814
+ const items: CMSItem[] = [{ _id: '1', _slug: 'test', title: 'Hello World' }];
815
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'h1', children: ['{{item.title}}'] }]);
811
816
 
812
817
  const result = builder.buildComponent({
813
818
  node: node as unknown as ComponentNode,
@@ -816,13 +821,13 @@ describe("ComponentBuilder", () => {
816
821
 
817
822
  expect(result).not.toBeNull();
818
823
  // The text should be interpolated - we verify by checking result structure
819
- expect(typeof result).toBe("object");
824
+ expect(typeof result).toBe('object');
820
825
  });
821
826
 
822
- test("should interpolate {{itemIndex}}, {{itemFirst}}, {{itemLast}}", () => {
827
+ test('should interpolate {{itemIndex}}, {{itemFirst}}, {{itemLast}}', () => {
823
828
  const items = createMockCMSItems(3);
824
- const node = createCollectionListNode("posts", [
825
- { type: "node", tag: "span", children: ["Index: {{itemIndex}}, First: {{itemFirst}}, Last: {{itemLast}}"] },
829
+ const node = createCollectionListNode('posts', [
830
+ { type: 'node', tag: 'span', children: ['Index: {{itemIndex}}, First: {{itemFirst}}, Last: {{itemLast}}'] },
826
831
  ]);
827
832
 
828
833
  const result = builder.buildComponent({
@@ -831,16 +836,14 @@ describe("ComponentBuilder", () => {
831
836
  });
832
837
 
833
838
  expect(result).not.toBeNull();
834
- expect(typeof result).toBe("object");
839
+ expect(typeof result).toBe('object');
835
840
  // Should have 3 item containers
836
841
  const element = result as { props?: { children?: unknown[] } };
837
842
  expect(element?.props?.children?.length).toBe(3);
838
843
  });
839
844
 
840
- test("should show empty state when no items", () => {
841
- const node = createCollectionListNode("posts", [
842
- { type: "node", tag: "div", children: ["Item"] },
843
- ]);
845
+ test('should show empty state when no items', () => {
846
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }]);
844
847
 
845
848
  const result = builder.buildComponent({
846
849
  node: node as unknown as ComponentNode,
@@ -848,16 +851,14 @@ describe("ComponentBuilder", () => {
848
851
  });
849
852
 
850
853
  expect(result).not.toBeNull();
851
- expect(typeof result).toBe("object");
854
+ expect(typeof result).toBe('object');
852
855
  // Empty state should be a single div with message
853
856
  const element = result as { props?: { children?: unknown } };
854
857
  expect(element?.props?.children).not.toBeNull();
855
858
  });
856
859
 
857
- test("should show empty state when collection not in map", () => {
858
- const node = createCollectionListNode("posts", [
859
- { type: "node", tag: "div", children: ["Item"] },
860
- ]);
860
+ test('should show empty state when collection not in map', () => {
861
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }]);
861
862
 
862
863
  const result = builder.buildComponent({
863
864
  node: node as unknown as ComponentNode,
@@ -865,14 +866,12 @@ describe("ComponentBuilder", () => {
865
866
  });
866
867
 
867
868
  expect(result).not.toBeNull();
868
- expect(typeof result).toBe("object");
869
+ expect(typeof result).toBe('object');
869
870
  });
870
871
 
871
- test("should apply limit to items", () => {
872
+ test('should apply limit to items', () => {
872
873
  const items = createMockCMSItems(5);
873
- const node = createCollectionListNode("posts", [
874
- { type: "node", tag: "div", children: ["Item"] },
875
- ], { limit: 2 });
874
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], { limit: 2 });
876
875
 
877
876
  const result = builder.buildComponent({
878
877
  node: node as unknown as ComponentNode,
@@ -885,11 +884,9 @@ describe("ComponentBuilder", () => {
885
884
  expect(element?.props?.children?.length).toBe(2);
886
885
  });
887
886
 
888
- test("should apply offset to items", () => {
887
+ test('should apply offset to items', () => {
889
888
  const items = createMockCMSItems(5);
890
- const node = createCollectionListNode("posts", [
891
- { type: "node", tag: "div", children: ["Item"] },
892
- ], { offset: 2 });
889
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], { offset: 2 });
893
890
 
894
891
  const result = builder.buildComponent({
895
892
  node: node as unknown as ComponentNode,
@@ -902,11 +899,12 @@ describe("ComponentBuilder", () => {
902
899
  expect(element?.props?.children?.length).toBe(3); // 5 - 2 = 3
903
900
  });
904
901
 
905
- test("should apply both limit and offset", () => {
902
+ test('should apply both limit and offset', () => {
906
903
  const items = createMockCMSItems(5);
907
- const node = createCollectionListNode("posts", [
908
- { type: "node", tag: "div", children: ["Item"] },
909
- ], { offset: 1, limit: 2 });
904
+ const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], {
905
+ offset: 1,
906
+ limit: 2,
907
+ });
910
908
 
911
909
  const result = builder.buildComponent({
912
910
  node: node as unknown as ComponentNode,
@@ -919,19 +917,19 @@ describe("ComponentBuilder", () => {
919
917
  expect(element?.props?.children?.length).toBe(2);
920
918
  });
921
919
 
922
- test("should filter items with simple object filter", () => {
920
+ test('should filter items with simple object filter', () => {
923
921
  const items: CMSItem[] = [
924
- { _id: "1", _slug: "post1", featured: true, title: "Featured Post" },
925
- { _id: "2", _slug: "post2", featured: false, title: "Regular Post 1" },
926
- { _id: "3", _slug: "post3", featured: true, title: "Another Featured" },
927
- { _id: "4", _slug: "post4", featured: false, title: "Regular Post 2" },
922
+ { _id: '1', _slug: 'post1', featured: true, title: 'Featured Post' },
923
+ { _id: '2', _slug: 'post2', featured: false, title: 'Regular Post 1' },
924
+ { _id: '3', _slug: 'post3', featured: true, title: 'Another Featured' },
925
+ { _id: '4', _slug: 'post4', featured: false, title: 'Regular Post 2' },
928
926
  ];
929
927
  const node: ListNode = {
930
928
  type: NODE_TYPE.LIST,
931
929
  sourceType: 'collection',
932
- source: "posts",
930
+ source: 'posts',
933
931
  filter: { featured: true },
934
- children: [{ type: "node", tag: "div", children: ["Item"] }],
932
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
935
933
  };
936
934
 
937
935
  const result = builder.buildComponent({
@@ -945,19 +943,19 @@ describe("ComponentBuilder", () => {
945
943
  expect(element?.props?.children?.length).toBe(2); // Only featured items
946
944
  });
947
945
 
948
- test("should filter items with operator condition", () => {
946
+ test('should filter items with operator condition', () => {
949
947
  const items: CMSItem[] = [
950
- { _id: "1", _slug: "post1", price: 50, title: "Cheap" },
951
- { _id: "2", _slug: "post2", price: 150, title: "Expensive 1" },
952
- { _id: "3", _slug: "post3", price: 200, title: "Expensive 2" },
953
- { _id: "4", _slug: "post4", price: 75, title: "Mid" },
948
+ { _id: '1', _slug: 'post1', price: 50, title: 'Cheap' },
949
+ { _id: '2', _slug: 'post2', price: 150, title: 'Expensive 1' },
950
+ { _id: '3', _slug: 'post3', price: 200, title: 'Expensive 2' },
951
+ { _id: '4', _slug: 'post4', price: 75, title: 'Mid' },
954
952
  ];
955
953
  const node: ListNode = {
956
954
  type: NODE_TYPE.LIST,
957
955
  sourceType: 'collection',
958
- source: "posts",
959
- filter: { field: "price", operator: "gt", value: 100 },
960
- children: [{ type: "node", tag: "div", children: ["Item"] }],
956
+ source: 'posts',
957
+ filter: { field: 'price', operator: 'gt', value: 100 },
958
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
961
959
  };
962
960
 
963
961
  const result = builder.buildComponent({
@@ -971,24 +969,24 @@ describe("ComponentBuilder", () => {
971
969
  expect(element?.props?.children?.length).toBe(2); // Only items with price > 100
972
970
  });
973
971
 
974
- test("should filter items with template value from cmsContext", () => {
972
+ test('should filter items with template value from cmsContext', () => {
975
973
  const items: CMSItem[] = [
976
- { _id: "1", _slug: "post1", category: "tech", title: "Tech Post 1" },
977
- { _id: "2", _slug: "post2", category: "news", title: "News Post" },
978
- { _id: "3", _slug: "post3", category: "tech", title: "Tech Post 2" },
974
+ { _id: '1', _slug: 'post1', category: 'tech', title: 'Tech Post 1' },
975
+ { _id: '2', _slug: 'post2', category: 'news', title: 'News Post' },
976
+ { _id: '3', _slug: 'post3', category: 'tech', title: 'Tech Post 2' },
979
977
  ];
980
978
  const node: ListNode = {
981
979
  type: NODE_TYPE.LIST,
982
980
  sourceType: 'collection',
983
- source: "posts",
984
- filter: { category: "{{cms.category}}" },
985
- children: [{ type: "node", tag: "div", children: ["Item"] }],
981
+ source: 'posts',
982
+ filter: { category: '{{cms.category}}' },
983
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
986
984
  };
987
985
 
988
986
  const result = builder.buildComponent({
989
987
  node: node as unknown as ComponentNode,
990
988
  collectionItemsMap: { posts: items },
991
- cmsContext: { category: "tech" },
989
+ cmsContext: { category: 'tech' },
992
990
  });
993
991
 
994
992
  expect(result).not.toBeNull();
@@ -997,18 +995,18 @@ describe("ComponentBuilder", () => {
997
995
  expect(element?.props?.children?.length).toBe(2); // Only tech posts
998
996
  });
999
997
 
1000
- test("should sort items ascending", () => {
998
+ test('should sort items ascending', () => {
1001
999
  const items: CMSItem[] = [
1002
- { _id: "1", _slug: "c", title: "C" },
1003
- { _id: "2", _slug: "a", title: "A" },
1004
- { _id: "3", _slug: "b", title: "B" },
1000
+ { _id: '1', _slug: 'c', title: 'C' },
1001
+ { _id: '2', _slug: 'a', title: 'A' },
1002
+ { _id: '3', _slug: 'b', title: 'B' },
1005
1003
  ];
1006
1004
  const node: ListNode = {
1007
1005
  type: NODE_TYPE.LIST,
1008
1006
  sourceType: 'collection',
1009
- source: "posts",
1010
- sort: { field: "title", order: "asc" },
1011
- children: [{ type: "node", tag: "div", children: ["{{item.title}}"] }],
1007
+ source: 'posts',
1008
+ sort: { field: 'title', order: 'asc' },
1009
+ children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
1012
1010
  };
1013
1011
 
1014
1012
  const result = builder.buildComponent({
@@ -1023,18 +1021,18 @@ describe("ComponentBuilder", () => {
1023
1021
  // We can't easily check the order of rendered children without more complex inspection
1024
1022
  });
1025
1023
 
1026
- test("should sort items descending", () => {
1024
+ test('should sort items descending', () => {
1027
1025
  const items: CMSItem[] = [
1028
- { _id: "1", _slug: "p1", date: 100 },
1029
- { _id: "2", _slug: "p2", date: 300 },
1030
- { _id: "3", _slug: "p3", date: 200 },
1026
+ { _id: '1', _slug: 'p1', date: 100 },
1027
+ { _id: '2', _slug: 'p2', date: 300 },
1028
+ { _id: '3', _slug: 'p3', date: 200 },
1031
1029
  ];
1032
1030
  const node: ListNode = {
1033
1031
  type: NODE_TYPE.LIST,
1034
1032
  sourceType: 'collection',
1035
- source: "posts",
1036
- sort: { field: "date", order: "desc" },
1037
- children: [{ type: "node", tag: "div", children: ["Item"] }],
1033
+ source: 'posts',
1034
+ sort: { field: 'date', order: 'desc' },
1035
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
1038
1036
  };
1039
1037
 
1040
1038
  const result = builder.buildComponent({
@@ -1048,22 +1046,22 @@ describe("ComponentBuilder", () => {
1048
1046
  expect(element?.props?.children?.length).toBe(3);
1049
1047
  });
1050
1048
 
1051
- test("should apply filter BEFORE limit", () => {
1049
+ test('should apply filter BEFORE limit', () => {
1052
1050
  // 5 items: 2 featured, 3 not featured
1053
1051
  const items: CMSItem[] = [
1054
- { _id: "1", _slug: "p1", featured: true, title: "Featured 1" },
1055
- { _id: "2", _slug: "p2", featured: false, title: "Regular 1" },
1056
- { _id: "3", _slug: "p3", featured: false, title: "Regular 2" },
1057
- { _id: "4", _slug: "p4", featured: true, title: "Featured 2" },
1058
- { _id: "5", _slug: "p5", featured: false, title: "Regular 3" },
1052
+ { _id: '1', _slug: 'p1', featured: true, title: 'Featured 1' },
1053
+ { _id: '2', _slug: 'p2', featured: false, title: 'Regular 1' },
1054
+ { _id: '3', _slug: 'p3', featured: false, title: 'Regular 2' },
1055
+ { _id: '4', _slug: 'p4', featured: true, title: 'Featured 2' },
1056
+ { _id: '5', _slug: 'p5', featured: false, title: 'Regular 3' },
1059
1057
  ];
1060
1058
  const node: ListNode = {
1061
1059
  type: NODE_TYPE.LIST,
1062
1060
  sourceType: 'collection',
1063
- source: "posts",
1061
+ source: 'posts',
1064
1062
  filter: { featured: true },
1065
1063
  limit: 3, // Without proper order, this would limit before filtering
1066
- children: [{ type: "node", tag: "div", children: ["Item"] }],
1064
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
1067
1065
  };
1068
1066
 
1069
1067
  const result = builder.buildComponent({
@@ -1078,21 +1076,21 @@ describe("ComponentBuilder", () => {
1078
1076
  expect(element?.props?.children?.length).toBe(2);
1079
1077
  });
1080
1078
 
1081
- test("should apply sort BEFORE limit", () => {
1079
+ test('should apply sort BEFORE limit', () => {
1082
1080
  const items: CMSItem[] = [
1083
- { _id: "1", _slug: "p1", date: 100 },
1084
- { _id: "2", _slug: "p2", date: 300 },
1085
- { _id: "3", _slug: "p3", date: 200 },
1086
- { _id: "4", _slug: "p4", date: 400 },
1087
- { _id: "5", _slug: "p5", date: 50 },
1081
+ { _id: '1', _slug: 'p1', date: 100 },
1082
+ { _id: '2', _slug: 'p2', date: 300 },
1083
+ { _id: '3', _slug: 'p3', date: 200 },
1084
+ { _id: '4', _slug: 'p4', date: 400 },
1085
+ { _id: '5', _slug: 'p5', date: 50 },
1088
1086
  ];
1089
1087
  const node: ListNode = {
1090
1088
  type: NODE_TYPE.LIST,
1091
1089
  sourceType: 'collection',
1092
- source: "posts",
1093
- sort: { field: "date", order: "desc" },
1090
+ source: 'posts',
1091
+ sort: { field: 'date', order: 'desc' },
1094
1092
  limit: 2, // Should get 2 most recent (highest date)
1095
- children: [{ type: "node", tag: "div", children: ["Item"] }],
1093
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
1096
1094
  };
1097
1095
 
1098
1096
  const result = builder.buildComponent({
@@ -1106,22 +1104,22 @@ describe("ComponentBuilder", () => {
1106
1104
  expect(element?.props?.children?.length).toBe(2);
1107
1105
  });
1108
1106
 
1109
- test("should apply multiple filter conditions with AND logic", () => {
1107
+ test('should apply multiple filter conditions with AND logic', () => {
1110
1108
  const items: CMSItem[] = [
1111
- { _id: "1", _slug: "p1", featured: true, price: 150 },
1112
- { _id: "2", _slug: "p2", featured: false, price: 200 },
1113
- { _id: "3", _slug: "p3", featured: true, price: 50 },
1114
- { _id: "4", _slug: "p4", featured: true, price: 200 },
1109
+ { _id: '1', _slug: 'p1', featured: true, price: 150 },
1110
+ { _id: '2', _slug: 'p2', featured: false, price: 200 },
1111
+ { _id: '3', _slug: 'p3', featured: true, price: 50 },
1112
+ { _id: '4', _slug: 'p4', featured: true, price: 200 },
1115
1113
  ];
1116
1114
  const node: ListNode = {
1117
1115
  type: NODE_TYPE.LIST,
1118
1116
  sourceType: 'collection',
1119
- source: "posts",
1117
+ source: 'posts',
1120
1118
  filter: [
1121
- { field: "featured", value: true },
1122
- { field: "price", operator: "gte", value: 100 }
1119
+ { field: 'featured', value: true },
1120
+ { field: 'price', operator: 'gte', value: 100 },
1123
1121
  ],
1124
- children: [{ type: "node", tag: "div", children: ["Item"] }],
1122
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
1125
1123
  };
1126
1124
 
1127
1125
  const result = builder.buildComponent({
@@ -1137,7 +1135,69 @@ describe("ComponentBuilder", () => {
1137
1135
  });
1138
1136
  });
1139
1137
 
1140
- describe("Interactive Styles - Element Class Generation", () => {
1138
+ describe('buildComponent - List (Remote Mode)', () => {
1139
+ // Remote lists (sourceType: 'remote') have no Astro runtime in the editor; the studio
1140
+ // pre-fetches the endpoint and injects items into collectionItemsMap keyed by the URL.
1141
+ const URL = 'https://api.example.com/markets';
1142
+
1143
+ const createRemoteItems = (count: number): CMSItem[] =>
1144
+ Array.from({ length: count }, (_, i) => ({ _id: String(i), name: `Coin ${i}`, rank: i }));
1145
+
1146
+ const createRemoteListNode = (
1147
+ children: ComponentNode[],
1148
+ options: { url?: string; limit?: number; offset?: number; sort?: unknown } = {},
1149
+ ): ListNode =>
1150
+ ({
1151
+ type: NODE_TYPE.LIST,
1152
+ sourceType: 'remote',
1153
+ url: options.url ?? URL,
1154
+ children,
1155
+ ...options,
1156
+ }) as unknown as ListNode;
1157
+
1158
+ test('renders an item for each injected remote item (keyed by URL)', () => {
1159
+ const items = createRemoteItems(3);
1160
+ const node = createRemoteListNode([{ type: 'node', tag: 'div', children: ['{{item.name}}'] }]);
1161
+
1162
+ const result = builder.buildComponent({
1163
+ node: node as unknown as ComponentNode,
1164
+ collectionItemsMap: { [URL]: items },
1165
+ });
1166
+
1167
+ const element = result as { props?: { children?: unknown[] } };
1168
+ expect(Array.isArray(element?.props?.children)).toBe(true);
1169
+ expect(element?.props?.children?.length).toBe(3);
1170
+ });
1171
+
1172
+ test('applies limit to the injected remote items', () => {
1173
+ const items = createRemoteItems(10);
1174
+ const node = createRemoteListNode([{ type: 'node', tag: 'div', children: ['{{item.name}}'] }], { limit: 4 });
1175
+
1176
+ const result = builder.buildComponent({
1177
+ node: node as unknown as ComponentNode,
1178
+ collectionItemsMap: { [URL]: items },
1179
+ });
1180
+
1181
+ const element = result as { props?: { children?: unknown[] } };
1182
+ // The regression: changing Limit must NOT empty the list — it slices the cached items.
1183
+ expect(element?.props?.children?.length).toBe(4);
1184
+ });
1185
+
1186
+ test('shows empty state (not a crash) when the URL has no injected items yet', () => {
1187
+ const node = createRemoteListNode([{ type: 'node', tag: 'div', children: ['{{item.name}}'] }]);
1188
+
1189
+ const result = builder.buildComponent({
1190
+ node: node as unknown as ComponentNode,
1191
+ collectionItemsMap: {}, // not fetched yet
1192
+ });
1193
+
1194
+ expect(result).not.toBeNull();
1195
+ const element = result as { props?: { children?: unknown } };
1196
+ expect(element?.props?.children).not.toBeNull();
1197
+ });
1198
+ });
1199
+
1200
+ describe('Interactive Styles - Element Class Generation', () => {
1141
1201
  /**
1142
1202
  * These tests verify that element classes are generated correctly
1143
1203
  * for interactive styles in both page and component contexts.
@@ -1149,11 +1209,11 @@ describe("ComponentBuilder", () => {
1149
1209
  * 4. The same component element always gets the same class
1150
1210
  */
1151
1211
 
1152
- test("should not add element class when no interactive styles", () => {
1212
+ test('should not add element class when no interactive styles', () => {
1153
1213
  const node: ComponentNode = {
1154
- type: "node",
1155
- tag: "div",
1156
- children: ["No interactive styles"],
1214
+ type: 'node',
1215
+ tag: 'div',
1216
+ children: ['No interactive styles'],
1157
1217
  };
1158
1218
 
1159
1219
  const result = builder.buildComponent({ node });
@@ -1161,12 +1221,12 @@ describe("ComponentBuilder", () => {
1161
1221
  // No element class should be added when there are no interactive styles
1162
1222
  });
1163
1223
 
1164
- test("should add element class when node has generateElementClass flag", () => {
1224
+ test('should add element class when node has generateElementClass flag', () => {
1165
1225
  const node: ComponentNode = {
1166
- type: "node",
1167
- tag: "div",
1226
+ type: 'node',
1227
+ tag: 'div',
1168
1228
  generateElementClass: true,
1169
- children: ["Has element class flag"],
1229
+ children: ['Has element class flag'],
1170
1230
  };
1171
1231
 
1172
1232
  const result = builder.buildComponent({ node });
@@ -1174,14 +1234,12 @@ describe("ComponentBuilder", () => {
1174
1234
  // Element class should be generated when flag is set
1175
1235
  });
1176
1236
 
1177
- test("should add element class when node has interactive styles", () => {
1237
+ test('should add element class when node has interactive styles', () => {
1178
1238
  const node: ComponentNode = {
1179
- type: "node",
1180
- tag: "div",
1181
- interactiveStyles: [
1182
- { postfix: ":hover", style: { backgroundColor: "primary" } },
1183
- ],
1184
- children: ["Has interactive styles"],
1239
+ type: 'node',
1240
+ tag: 'div',
1241
+ interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary' } }],
1242
+ children: ['Has interactive styles'],
1185
1243
  };
1186
1244
 
1187
1245
  const result = builder.buildComponent({ node });
@@ -1189,23 +1247,21 @@ describe("ComponentBuilder", () => {
1189
1247
  // Element class should be generated for interactive styles
1190
1248
  });
1191
1249
 
1192
- test("should use page-based class for page elements", () => {
1250
+ test('should use page-based class for page elements', () => {
1193
1251
  // Create builder with page name getter
1194
1252
  const pageBuilder = new ComponentBuilder({
1195
1253
  componentRegistry,
1196
1254
  elementRegistry,
1197
- getCurrentPageName: () => "about",
1198
- getCurrentFileType: () => "page",
1255
+ getCurrentPageName: () => 'about',
1256
+ getCurrentFileType: () => 'page',
1199
1257
  });
1200
1258
 
1201
1259
  const node: ComponentNode = {
1202
- type: "node",
1203
- tag: "button",
1204
- label: "submit-button",
1205
- interactiveStyles: [
1206
- { postfix: ":hover", style: { opacity: "0.9" } },
1207
- ],
1208
- children: ["Submit"],
1260
+ type: 'node',
1261
+ tag: 'button',
1262
+ label: 'submit-button',
1263
+ interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
1264
+ children: ['Submit'],
1209
1265
  };
1210
1266
 
1211
1267
  const result = pageBuilder.buildComponent({ node });
@@ -1213,28 +1269,26 @@ describe("ComponentBuilder", () => {
1213
1269
  // Element class should be p_about_submit-button for page elements
1214
1270
  });
1215
1271
 
1216
- test("should use component-based class when inside component context", () => {
1272
+ test('should use component-based class when inside component context', () => {
1217
1273
  const node: ComponentNode = {
1218
- type: "node",
1219
- tag: "button",
1220
- label: "action-button",
1221
- interactiveStyles: [
1222
- { postfix: ":hover", style: { backgroundColor: "primary-dark" } },
1223
- ],
1224
- children: ["Click"],
1274
+ type: 'node',
1275
+ tag: 'button',
1276
+ label: 'action-button',
1277
+ interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary-dark' } }],
1278
+ children: ['Click'],
1225
1279
  };
1226
1280
 
1227
1281
  // Build with componentContext set (inside a component)
1228
1282
  const result = builder.buildComponent({
1229
1283
  node,
1230
- componentContext: "Button",
1284
+ componentContext: 'Button',
1231
1285
  });
1232
1286
 
1233
1287
  expect(result).not.toBeNull();
1234
1288
  // Element class should be c_button_action-button for component elements
1235
1289
  });
1236
1290
 
1237
- test("should use relative path for component elements", () => {
1291
+ test('should use relative path for component elements', () => {
1238
1292
  // When a component is placed at different positions on a page,
1239
1293
  // its internal elements should always get the same class
1240
1294
  // because they use paths relative to the component root.
@@ -1243,32 +1297,28 @@ describe("ComponentBuilder", () => {
1243
1297
  component: {
1244
1298
  interface: {},
1245
1299
  structure: {
1246
- type: "node",
1247
- tag: "div",
1248
- label: "root",
1249
- interactiveStyles: [
1250
- { postfix: ":hover", style: { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" } },
1251
- ],
1300
+ type: 'node',
1301
+ tag: 'div',
1302
+ label: 'root',
1303
+ interactiveStyles: [{ postfix: ':hover', style: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' } }],
1252
1304
  children: [
1253
1305
  {
1254
- type: "node",
1255
- tag: "span",
1256
- label: "label",
1257
- interactiveStyles: [
1258
- { postfix: ":hover", style: { color: "primary" } },
1259
- ],
1260
- children: ["Label"],
1306
+ type: 'node',
1307
+ tag: 'span',
1308
+ label: 'label',
1309
+ interactiveStyles: [{ postfix: ':hover', style: { color: 'primary' } }],
1310
+ children: ['Label'],
1261
1311
  },
1262
1312
  ],
1263
1313
  },
1264
1314
  },
1265
1315
  };
1266
- componentRegistry.register("HoverCard", componentDef as any);
1316
+ componentRegistry.register('HoverCard', componentDef as any);
1267
1317
 
1268
1318
  // First instance at page path [0]
1269
1319
  const node1: ComponentNode = {
1270
- type: "component",
1271
- component: "HoverCard",
1320
+ type: 'component',
1321
+ component: 'HoverCard',
1272
1322
  };
1273
1323
 
1274
1324
  const result1 = builder.buildComponent({
@@ -1291,7 +1341,7 @@ describe("ComponentBuilder", () => {
1291
1341
  // component-relative paths, not page-absolute paths
1292
1342
  });
1293
1343
 
1294
- test("should generate different classes for page vs component with same name", () => {
1344
+ test('should generate different classes for page vs component with same name', () => {
1295
1345
  // A page named "button" and a component named "Button" should
1296
1346
  // produce different element classes (p_button_... vs c_button_...)
1297
1347
 
@@ -1299,22 +1349,20 @@ describe("ComponentBuilder", () => {
1299
1349
  component: {
1300
1350
  interface: {},
1301
1351
  structure: {
1302
- type: "node",
1303
- tag: "button",
1304
- label: "root",
1305
- interactiveStyles: [
1306
- { postfix: ":hover", style: { opacity: "0.9" } },
1307
- ],
1308
- children: ["Component Button"],
1352
+ type: 'node',
1353
+ tag: 'button',
1354
+ label: 'root',
1355
+ interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
1356
+ children: ['Component Button'],
1309
1357
  },
1310
1358
  },
1311
1359
  };
1312
- componentRegistry.register("Button", componentDef as any);
1360
+ componentRegistry.register('Button', componentDef as any);
1313
1361
 
1314
1362
  // Component instance
1315
1363
  const componentNode: ComponentNode = {
1316
- type: "component",
1317
- component: "Button",
1364
+ type: 'component',
1365
+ component: 'Button',
1318
1366
  };
1319
1367
 
1320
1368
  const componentResult = builder.buildComponent({ node: componentNode });
@@ -1324,18 +1372,16 @@ describe("ComponentBuilder", () => {
1324
1372
  const pageBuilder = new ComponentBuilder({
1325
1373
  componentRegistry,
1326
1374
  elementRegistry,
1327
- getCurrentPageName: () => "button",
1328
- getCurrentFileType: () => "page",
1375
+ getCurrentPageName: () => 'button',
1376
+ getCurrentFileType: () => 'page',
1329
1377
  });
1330
1378
 
1331
1379
  const pageNode: ComponentNode = {
1332
- type: "node",
1333
- tag: "button",
1334
- label: "root",
1335
- interactiveStyles: [
1336
- { postfix: ":hover", style: { opacity: "0.9" } },
1337
- ],
1338
- children: ["Page Button"],
1380
+ type: 'node',
1381
+ tag: 'button',
1382
+ label: 'root',
1383
+ interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
1384
+ children: ['Page Button'],
1339
1385
  };
1340
1386
 
1341
1387
  const pageResult = pageBuilder.buildComponent({ node: pageNode });
@@ -1344,26 +1390,24 @@ describe("ComponentBuilder", () => {
1344
1390
  // Component uses c_button_root, page uses p_button_root
1345
1391
  });
1346
1392
 
1347
- test("should propagate componentContext through nested children", () => {
1393
+ test('should propagate componentContext through nested children', () => {
1348
1394
  const componentDef = {
1349
1395
  component: {
1350
1396
  interface: {},
1351
1397
  structure: {
1352
- type: "node",
1353
- tag: "div",
1398
+ type: 'node',
1399
+ tag: 'div',
1354
1400
  children: [
1355
1401
  {
1356
- type: "node",
1357
- tag: "div",
1402
+ type: 'node',
1403
+ tag: 'div',
1358
1404
  children: [
1359
1405
  {
1360
- type: "node",
1361
- tag: "button",
1362
- label: "deeply-nested",
1363
- interactiveStyles: [
1364
- { postfix: ":hover", style: { color: "primary" } },
1365
- ],
1366
- children: ["Nested Button"],
1406
+ type: 'node',
1407
+ tag: 'button',
1408
+ label: 'deeply-nested',
1409
+ interactiveStyles: [{ postfix: ':hover', style: { color: 'primary' } }],
1410
+ children: ['Nested Button'],
1367
1411
  },
1368
1412
  ],
1369
1413
  },
@@ -1371,11 +1415,11 @@ describe("ComponentBuilder", () => {
1371
1415
  },
1372
1416
  },
1373
1417
  };
1374
- componentRegistry.register("DeepComponent", componentDef as any);
1418
+ componentRegistry.register('DeepComponent', componentDef as any);
1375
1419
 
1376
1420
  const node: ComponentNode = {
1377
- type: "component",
1378
- component: "DeepComponent",
1421
+ type: 'component',
1422
+ component: 'DeepComponent',
1379
1423
  };
1380
1424
 
1381
1425
  const result = builder.buildComponent({ node });
@@ -1385,50 +1429,50 @@ describe("ComponentBuilder", () => {
1385
1429
  // because componentContext is propagated through the tree
1386
1430
  });
1387
1431
 
1388
- test("should handle multiple interactive states on same element", () => {
1432
+ test('should handle multiple interactive states on same element', () => {
1389
1433
  const node: ComponentNode = {
1390
- type: "node",
1391
- tag: "button",
1392
- label: "multi-state",
1434
+ type: 'node',
1435
+ tag: 'button',
1436
+ label: 'multi-state',
1393
1437
  interactiveStyles: [
1394
- { postfix: ":hover", style: { backgroundColor: "primary-light" } },
1395
- { postfix: ":active", style: { backgroundColor: "primary-dark" } },
1396
- { postfix: ":focus", style: { outline: "2px solid var(--primary)" } },
1397
- { postfix: ":disabled", style: { opacity: "0.5", cursor: "not-allowed" } },
1438
+ { postfix: ':hover', style: { backgroundColor: 'primary-light' } },
1439
+ { postfix: ':active', style: { backgroundColor: 'primary-dark' } },
1440
+ { postfix: ':focus', style: { outline: '2px solid var(--primary)' } },
1441
+ { postfix: ':disabled', style: { opacity: '0.5', cursor: 'not-allowed' } },
1398
1442
  ],
1399
- children: ["Multi-state Button"],
1443
+ children: ['Multi-state Button'],
1400
1444
  };
1401
1445
 
1402
1446
  const result = builder.buildComponent({
1403
1447
  node,
1404
- componentContext: "MultiButton",
1448
+ componentContext: 'MultiButton',
1405
1449
  });
1406
1450
 
1407
1451
  expect(result).not.toBeNull();
1408
1452
  // All states should use the same element class: c_multibutton_multi-state
1409
1453
  });
1410
1454
 
1411
- test("should handle responsive interactive styles", () => {
1455
+ test('should handle responsive interactive styles', () => {
1412
1456
  const node: ComponentNode = {
1413
- type: "node",
1414
- tag: "div",
1415
- label: "responsive-hover",
1457
+ type: 'node',
1458
+ tag: 'div',
1459
+ label: 'responsive-hover',
1416
1460
  interactiveStyles: [
1417
1461
  {
1418
- postfix: ":hover",
1462
+ postfix: ':hover',
1419
1463
  style: {
1420
- base: { padding: "20px" },
1421
- tablet: { padding: "16px" },
1422
- mobile: { padding: "12px" },
1464
+ base: { padding: '20px' },
1465
+ tablet: { padding: '16px' },
1466
+ mobile: { padding: '12px' },
1423
1467
  },
1424
1468
  },
1425
1469
  ],
1426
- children: ["Responsive hover"],
1470
+ children: ['Responsive hover'],
1427
1471
  };
1428
1472
 
1429
1473
  const result = builder.buildComponent({
1430
1474
  node,
1431
- componentContext: "ResponsiveCard",
1475
+ componentContext: 'ResponsiveCard',
1432
1476
  });
1433
1477
 
1434
1478
  expect(result).not.toBeNull();
@@ -1436,27 +1480,27 @@ describe("ComponentBuilder", () => {
1436
1480
  });
1437
1481
  });
1438
1482
 
1439
- describe("buildComponent - Conditional Rendering (if property)", () => {
1440
- test("should render node when if is true", () => {
1483
+ describe('buildComponent - Conditional Rendering (if property)', () => {
1484
+ test('should render node when if is true', () => {
1441
1485
  const node: ComponentNode = {
1442
- type: "node",
1443
- tag: "div",
1486
+ type: 'node',
1487
+ tag: 'div',
1444
1488
  if: true,
1445
- children: ["Visible content"],
1489
+ children: ['Visible content'],
1446
1490
  } as any;
1447
1491
 
1448
1492
  const result = builder.buildComponent({ node });
1449
1493
 
1450
1494
  expect(result).not.toBeNull();
1451
- expect(typeof result).toBe("object");
1495
+ expect(typeof result).toBe('object');
1452
1496
  });
1453
1497
 
1454
- test("should skip node when if is false", () => {
1498
+ test('should skip node when if is false', () => {
1455
1499
  const node: ComponentNode = {
1456
- type: "node",
1457
- tag: "div",
1500
+ type: 'node',
1501
+ tag: 'div',
1458
1502
  if: false,
1459
- children: ["Hidden content"],
1503
+ children: ['Hidden content'],
1460
1504
  } as any;
1461
1505
 
1462
1506
  const result = builder.buildComponent({ node });
@@ -1464,11 +1508,11 @@ describe("ComponentBuilder", () => {
1464
1508
  expect(result).toBeNull();
1465
1509
  });
1466
1510
 
1467
- test("should render node when if property is absent", () => {
1511
+ test('should render node when if property is absent', () => {
1468
1512
  const node: ComponentNode = {
1469
- type: "node",
1470
- tag: "div",
1471
- children: ["Default visible"],
1513
+ type: 'node',
1514
+ tag: 'div',
1515
+ children: ['Default visible'],
1472
1516
  };
1473
1517
 
1474
1518
  const result = builder.buildComponent({ node });
@@ -1476,41 +1520,41 @@ describe("ComponentBuilder", () => {
1476
1520
  expect(result).not.toBeNull();
1477
1521
  });
1478
1522
 
1479
- test("should resolve if mapping from component props", () => {
1523
+ test('should resolve if mapping from component props', () => {
1480
1524
  const node: ComponentNode = {
1481
- type: "node",
1482
- tag: "div",
1525
+ type: 'node',
1526
+ tag: 'div',
1483
1527
  if: {
1484
1528
  _mapping: true,
1485
- prop: "showBanner",
1486
- values: { "true": true, "false": false },
1529
+ prop: 'showBanner',
1530
+ values: { true: true, false: false },
1487
1531
  },
1488
- children: ["Conditional banner"],
1532
+ children: ['Conditional banner'],
1489
1533
  } as any;
1490
1534
 
1491
1535
  // When prop is 'true', should render
1492
1536
  const resultVisible = builder.buildComponent({
1493
1537
  node,
1494
- componentResolvedProps: { showBanner: "true" },
1538
+ componentResolvedProps: { showBanner: 'true' },
1495
1539
  });
1496
1540
  expect(resultVisible).not.toBeNull();
1497
1541
 
1498
1542
  // When prop is 'false', should skip
1499
1543
  const resultHidden = builder.buildComponent({
1500
1544
  node,
1501
- componentResolvedProps: { showBanner: "false" },
1545
+ componentResolvedProps: { showBanner: 'false' },
1502
1546
  });
1503
1547
  expect(resultHidden).toBeNull();
1504
1548
  });
1505
1549
 
1506
- test("should skip children when parent if is false", () => {
1550
+ test('should skip children when parent if is false', () => {
1507
1551
  const node: ComponentNode = {
1508
- type: "node",
1509
- tag: "div",
1552
+ type: 'node',
1553
+ tag: 'div',
1510
1554
  if: false,
1511
1555
  children: [
1512
- { type: "node", tag: "span", children: ["Child 1"] },
1513
- { type: "node", tag: "span", children: ["Child 2"] },
1556
+ { type: 'node', tag: 'span', children: ['Child 1'] },
1557
+ { type: 'node', tag: 'span', children: ['Child 2'] },
1514
1558
  ],
1515
1559
  } as any;
1516
1560
 
@@ -1519,16 +1563,16 @@ describe("ComponentBuilder", () => {
1519
1563
  expect(result).toBeNull();
1520
1564
  });
1521
1565
 
1522
- test("should handle if with component nodes", () => {
1523
- componentRegistry.register("TestComponent", {
1566
+ test('should handle if with component nodes', () => {
1567
+ componentRegistry.register('TestComponent', {
1524
1568
  component: {
1525
- structure: { type: "node", tag: "div", children: ["Component content"] },
1569
+ structure: { type: 'node', tag: 'div', children: ['Component content'] },
1526
1570
  },
1527
1571
  } as any);
1528
1572
 
1529
1573
  const node: ComponentNode = {
1530
- type: "component",
1531
- component: "TestComponent",
1574
+ type: 'component',
1575
+ component: 'TestComponent',
1532
1576
  if: false,
1533
1577
  } as any;
1534
1578
 
@@ -1537,10 +1581,10 @@ describe("ComponentBuilder", () => {
1537
1581
  expect(result).toBeNull();
1538
1582
  });
1539
1583
 
1540
- test("should handle if with embed nodes", () => {
1584
+ test('should handle if with embed nodes', () => {
1541
1585
  const node: ComponentNode = {
1542
- type: "embed",
1543
- html: "<svg></svg>",
1586
+ type: 'embed',
1587
+ html: '<svg></svg>',
1544
1588
  if: false,
1545
1589
  } as any;
1546
1590
 
@@ -1549,12 +1593,12 @@ describe("ComponentBuilder", () => {
1549
1593
  expect(result).toBeNull();
1550
1594
  });
1551
1595
 
1552
- test("should handle if with link nodes", () => {
1596
+ test('should handle if with link nodes', () => {
1553
1597
  const node: ComponentNode = {
1554
- type: "link",
1555
- href: "/page",
1598
+ type: 'link',
1599
+ href: '/page',
1556
1600
  if: false,
1557
- children: ["Hidden link"],
1601
+ children: ['Hidden link'],
1558
1602
  } as any;
1559
1603
 
1560
1604
  const result = builder.buildComponent({ node });
@@ -1562,16 +1606,16 @@ describe("ComponentBuilder", () => {
1562
1606
  expect(result).toBeNull();
1563
1607
  });
1564
1608
 
1565
- test("treats unresolved string-template if as falsy", () => {
1609
+ test('treats unresolved string-template if as falsy', () => {
1566
1610
  // When `if: "{{buttonText}}"` references an undefined prop,
1567
1611
  // evaluateTemplate preserves the original template string (backward-compat).
1568
1612
  // Without component/CMS/item context to resolve it, treat the node as hidden
1569
1613
  // — matches user expectation when binding visibility to an unset prop.
1570
1614
  const node: ComponentNode = {
1571
- type: "link",
1572
- href: "/page",
1573
- if: "{{buttonText}}",
1574
- children: ["Hidden link"],
1615
+ type: 'link',
1616
+ href: '/page',
1617
+ if: '{{buttonText}}',
1618
+ children: ['Hidden link'],
1575
1619
  } as any;
1576
1620
 
1577
1621
  const result = builder.buildComponent({ node });
@@ -1579,9 +1623,9 @@ describe("ComponentBuilder", () => {
1579
1623
  expect(result).toBeNull();
1580
1624
  });
1581
1625
 
1582
- test("should handle if with locale-list nodes", () => {
1626
+ test('should handle if with locale-list nodes', () => {
1583
1627
  const node: ComponentNode = {
1584
- type: "locale-list",
1628
+ type: 'locale-list',
1585
1629
  if: false,
1586
1630
  } as any;
1587
1631
 
@@ -1590,29 +1634,29 @@ describe("ComponentBuilder", () => {
1590
1634
  expect(result).toBeNull();
1591
1635
  });
1592
1636
 
1593
- test("should render when mapping prop value not found (default true)", () => {
1637
+ test('should render when mapping prop value not found (default true)', () => {
1594
1638
  const node: ComponentNode = {
1595
- type: "node",
1596
- tag: "div",
1639
+ type: 'node',
1640
+ tag: 'div',
1597
1641
  if: {
1598
1642
  _mapping: true,
1599
- prop: "status",
1600
- values: { "active": true, "inactive": false },
1643
+ prop: 'status',
1644
+ values: { active: true, inactive: false },
1601
1645
  },
1602
- children: ["Content"],
1646
+ children: ['Content'],
1603
1647
  } as any;
1604
1648
 
1605
1649
  // 'unknown' is not in the values map, should default to true
1606
1650
  const result = builder.buildComponent({
1607
1651
  node,
1608
- componentResolvedProps: { status: "unknown" },
1652
+ componentResolvedProps: { status: 'unknown' },
1609
1653
  });
1610
1654
 
1611
1655
  expect(result).not.toBeNull();
1612
1656
  });
1613
1657
  });
1614
1658
 
1615
- describe("Edge Case 7: List Paths Tracking (Collection Mode)", () => {
1659
+ describe('Edge Case 7: List Paths Tracking (Collection Mode)', () => {
1616
1660
  const createMockCMSItems = (count: number): CMSItem[] => {
1617
1661
  return Array.from({ length: count }, (_, i) => ({
1618
1662
  _id: `item-${i}`,
@@ -1621,13 +1665,13 @@ describe("ComponentBuilder", () => {
1621
1665
  }));
1622
1666
  };
1623
1667
 
1624
- test("should pass listPaths through buildChildren", () => {
1668
+ test('should pass listPaths through buildChildren', () => {
1625
1669
  // Create a simple list with items
1626
1670
  const node: ListNode = {
1627
1671
  type: NODE_TYPE.LIST,
1628
1672
  sourceType: 'collection',
1629
- source: "posts",
1630
- children: [{ type: "node", tag: "div", children: ["Item"] }],
1673
+ source: 'posts',
1674
+ children: [{ type: 'node', tag: 'div', children: ['Item'] }],
1631
1675
  };
1632
1676
 
1633
1677
  const items = createMockCMSItems(2);
@@ -1643,18 +1687,18 @@ describe("ComponentBuilder", () => {
1643
1687
  expect(element?.props?.children?.length).toBe(2);
1644
1688
  });
1645
1689
 
1646
- test("should track listPaths in nested lists", () => {
1690
+ test('should track listPaths in nested lists', () => {
1647
1691
  // Outer list with inner list
1648
1692
  const outerNode: ListNode = {
1649
1693
  type: NODE_TYPE.LIST,
1650
1694
  sourceType: 'collection',
1651
- source: "categories",
1695
+ source: 'categories',
1652
1696
  children: [
1653
1697
  {
1654
1698
  type: NODE_TYPE.LIST,
1655
1699
  sourceType: 'collection',
1656
- source: "posts",
1657
- children: [{ type: "node", tag: "span", children: ["Post"] }],
1700
+ source: 'posts',
1701
+ children: [{ type: 'node', tag: 'span', children: ['Post'] }],
1658
1702
  } as any,
1659
1703
  ],
1660
1704
  };
@@ -1673,28 +1717,26 @@ describe("ComponentBuilder", () => {
1673
1717
  });
1674
1718
  });
1675
1719
 
1676
- describe("Edge Case 5: Slot Content Context Handling", () => {
1677
- test("should not pass componentContext to slot content elements", () => {
1720
+ describe('Edge Case 5: Slot Content Context Handling', () => {
1721
+ test('should not pass componentContext to slot content elements', () => {
1678
1722
  // Register a component with a slot
1679
- componentRegistry.register("Container", {
1680
- type: "Container",
1723
+ componentRegistry.register('Container', {
1724
+ type: 'Container',
1681
1725
  component: {
1682
1726
  interface: {},
1683
1727
  structure: {
1684
- type: "node",
1685
- tag: "div",
1686
- children: [{ type: "slot" }],
1728
+ type: 'node',
1729
+ tag: 'div',
1730
+ children: [{ type: 'slot' }],
1687
1731
  },
1688
1732
  },
1689
1733
  });
1690
1734
 
1691
1735
  // Component instance with children (slot content)
1692
1736
  const node: ComponentNode = {
1693
- type: "component",
1694
- component: "Container",
1695
- children: [
1696
- { type: "node", tag: "span", children: ["Slot content"] },
1697
- ],
1737
+ type: 'component',
1738
+ component: 'Container',
1739
+ children: [{ type: 'node', tag: 'span', children: ['Slot content'] }],
1698
1740
  };
1699
1741
 
1700
1742
  const result = builder.buildComponent({
@@ -1704,29 +1746,29 @@ describe("ComponentBuilder", () => {
1704
1746
 
1705
1747
  // Should render successfully
1706
1748
  expect(result).not.toBeNull();
1707
- expect(typeof result).toBe("object");
1749
+ expect(typeof result).toBe('object');
1708
1750
  });
1709
1751
 
1710
- test("should mark slot content with _isSlotContent flag", () => {
1752
+ test('should mark slot content with _isSlotContent flag', () => {
1711
1753
  // This tests that slot content is marked, which affects context handling
1712
- componentRegistry.register("Wrapper", {
1713
- type: "Wrapper",
1754
+ componentRegistry.register('Wrapper', {
1755
+ type: 'Wrapper',
1714
1756
  component: {
1715
1757
  interface: {},
1716
1758
  structure: {
1717
- type: "node",
1718
- tag: "section",
1719
- children: [{ type: "slot" }],
1759
+ type: 'node',
1760
+ tag: 'section',
1761
+ children: [{ type: 'slot' }],
1720
1762
  },
1721
1763
  },
1722
1764
  });
1723
1765
 
1724
1766
  const node: ComponentNode = {
1725
- type: "component",
1726
- component: "Wrapper",
1767
+ type: 'component',
1768
+ component: 'Wrapper',
1727
1769
  children: [
1728
- { type: "node", tag: "div", children: ["Child 1"] },
1729
- { type: "node", tag: "div", children: ["Child 2"] },
1770
+ { type: 'node', tag: 'div', children: ['Child 1'] },
1771
+ { type: 'node', tag: 'div', children: ['Child 2'] },
1730
1772
  ],
1731
1773
  };
1732
1774
 
@@ -1738,27 +1780,24 @@ describe("ComponentBuilder", () => {
1738
1780
  expect(element?.props?.children).toBeDefined();
1739
1781
  });
1740
1782
 
1741
- test("should preserve componentContext for component structure elements", () => {
1783
+ test('should preserve componentContext for component structure elements', () => {
1742
1784
  // Register a component
1743
- componentRegistry.register("Card", {
1744
- type: "Card",
1785
+ componentRegistry.register('Card', {
1786
+ type: 'Card',
1745
1787
  component: {
1746
1788
  interface: {},
1747
1789
  structure: {
1748
- type: "node",
1749
- tag: "article",
1750
- children: [
1751
- { type: "node", tag: "h2", children: ["Header"] },
1752
- { type: "slot" },
1753
- ],
1790
+ type: 'node',
1791
+ tag: 'article',
1792
+ children: [{ type: 'node', tag: 'h2', children: ['Header'] }, { type: 'slot' }],
1754
1793
  },
1755
1794
  },
1756
1795
  });
1757
1796
 
1758
1797
  const node: ComponentNode = {
1759
- type: "component",
1760
- component: "Card",
1761
- children: [{ type: "node", tag: "p", children: ["Content"] }],
1798
+ type: 'component',
1799
+ component: 'Card',
1800
+ children: [{ type: 'node', tag: 'p', children: ['Content'] }],
1762
1801
  };
1763
1802
 
1764
1803
  const result = builder.buildComponent({ node });
@@ -1767,5 +1806,69 @@ describe("ComponentBuilder", () => {
1767
1806
  expect(result).not.toBeNull();
1768
1807
  });
1769
1808
  });
1770
- });
1771
1809
 
1810
+ // Islands and Custom nodes are black boxes meno-core can't run — the canvas
1811
+ // shows a placeholder. The wrapper still wires data-element-path via a ref so a
1812
+ // click on the placeholder selects the node like any regular element.
1813
+ describe('buildComponent - Island / Custom node selection', () => {
1814
+ // Drive the ref the same way React would on mount and capture what it stamps.
1815
+ const captureRefAttrs = (ref: unknown): Record<string, string> => {
1816
+ const attrs: Record<string, string> = {};
1817
+ const fakeEl = {
1818
+ setAttribute: (name: string, value: string) => {
1819
+ attrs[name] = value;
1820
+ },
1821
+ style: { setProperty: () => {} },
1822
+ } as unknown as HTMLElement;
1823
+ (ref as (el: HTMLElement | null) => void)(fakeEl);
1824
+ return attrs;
1825
+ };
1826
+
1827
+ test('island wrapper carries data-island, display:contents, and a selection ref', () => {
1828
+ const node: ComponentNode = { type: 'island', src: 'Counter.tsx', client: { directive: 'load' } } as any;
1829
+ const result = builder.buildComponent({ node, elementPath: [0, 2] }) as any;
1830
+
1831
+ expect(result.type).toBe('div');
1832
+ expect(result.props['data-island']).toBe('Counter.tsx');
1833
+ expect(result.props.style.display).toBe('contents');
1834
+ expect(typeof (result.ref ?? result.props.ref)).toBe('function');
1835
+
1836
+ const attrs = captureRefAttrs(result.ref ?? result.props.ref);
1837
+ expect(attrs['data-element-path']).toBe('0,2');
1838
+ });
1839
+
1840
+ test('empty island renders its basename as the placeholder', () => {
1841
+ const node: ComponentNode = { type: 'island', src: 'widgets/Chart.vue' } as any;
1842
+ const result = builder.buildComponent({ node }) as any;
1843
+ // inner is the placeholder span carrying the basename.
1844
+ expect(result.props.children.props.children).toBe('Chart');
1845
+ });
1846
+
1847
+ test('custom wrapper carries data-custom and a selection ref', () => {
1848
+ const node: ComponentNode = { type: 'custom', src: 'Fancy.astro' } as any;
1849
+ const result = builder.buildComponent({ node, elementPath: [0, 1] }) as any;
1850
+
1851
+ expect(result.type).toBe('div');
1852
+ expect(result.props['data-custom']).toBe('Fancy.astro');
1853
+ expect(result.props.style.display).toBe('contents');
1854
+
1855
+ const attrs = captureRefAttrs(result.ref ?? result.props.ref);
1856
+ expect(attrs['data-element-path']).toBe('0,1');
1857
+ });
1858
+
1859
+ test('island ref stamps CMS-list context when present', () => {
1860
+ const node: ComponentNode = { type: 'island', src: 'Counter.tsx' } as any;
1861
+ const result = builder.buildComponent({
1862
+ node,
1863
+ elementPath: [0, 0],
1864
+ cmsItemIndexPath: [1],
1865
+ cmsListPaths: [[0]],
1866
+ }) as any;
1867
+
1868
+ const attrs = captureRefAttrs(result.ref ?? result.props.ref);
1869
+ expect(attrs['data-element-path']).toBe('0,0');
1870
+ expect(attrs['data-cms-item-index']).toBe('1');
1871
+ expect(attrs['data-cms-context']).toContain('"itemIndexPath":[1]');
1872
+ });
1873
+ });
1874
+ });