meno-core 1.0.53 → 1.0.54

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 (387) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +48 -57
  3. package/build-astro.ts +131 -113
  4. package/build-next.ts +109 -96
  5. package/build-static.test.ts +39 -10
  6. package/build-static.ts +120 -122
  7. package/dist/bin/cli.js +34 -38
  8. package/dist/bin/cli.js.map +2 -2
  9. package/dist/build-static.js +12 -11
  10. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  11. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  12. package/dist/chunks/{chunk-IGYR22T6.js → chunk-2FN4UOVO.js} +512 -239
  13. package/dist/chunks/chunk-2FN4UOVO.js.map +7 -0
  14. package/dist/chunks/chunk-3XER4E5W.js +168 -0
  15. package/dist/chunks/chunk-3XER4E5W.js.map +7 -0
  16. package/dist/chunks/{chunk-X754AHS5.js → chunk-5ETZFREW.js} +9 -12
  17. package/dist/chunks/chunk-5ETZFREW.js.map +7 -0
  18. package/dist/chunks/{chunk-2MHDV5BF.js → chunk-7E4IF5L7.js} +15 -21
  19. package/dist/chunks/chunk-7E4IF5L7.js.map +7 -0
  20. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-7HWQUVTU.js} +1578 -1349
  21. package/dist/chunks/chunk-7HWQUVTU.js.map +7 -0
  22. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-AE3QK5QW.js} +110 -21
  23. package/dist/chunks/chunk-AE3QK5QW.js.map +7 -0
  24. package/dist/chunks/{chunk-GZHGVVW3.js → chunk-F6KTJYGV.js} +7 -7
  25. package/dist/chunks/chunk-F6KTJYGV.js.map +7 -0
  26. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-FZITJSSS.js} +2 -6
  27. package/dist/chunks/chunk-FZITJSSS.js.map +7 -0
  28. package/dist/chunks/{chunk-QB2LNO4W.js → chunk-GSYYA5GX.js} +2 -2
  29. package/dist/chunks/chunk-GSYYA5GX.js.map +7 -0
  30. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-HIZMY3EP.js} +1 -1
  31. package/dist/chunks/chunk-HIZMY3EP.js.map +7 -0
  32. package/dist/chunks/{chunk-AZQYF6KE.js → chunk-I2WEGYA7.js} +41 -176
  33. package/dist/chunks/chunk-I2WEGYA7.js.map +7 -0
  34. package/dist/chunks/{chunk-I7YIGZXT.js → chunk-JNO3CNLJ.js} +6 -9
  35. package/dist/chunks/chunk-JNO3CNLJ.js.map +7 -0
  36. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-NVRBTSQG.js} +2 -4
  37. package/dist/chunks/chunk-NVRBTSQG.js.map +7 -0
  38. package/dist/chunks/{chunk-JGP5A3Y5.js → chunk-Q4OBWKXG.js} +40 -33
  39. package/dist/chunks/chunk-Q4OBWKXG.js.map +7 -0
  40. package/dist/chunks/{chunk-R6XHAFBF.js → chunk-QTE32Y53.js} +250 -242
  41. package/dist/chunks/chunk-QTE32Y53.js.map +7 -0
  42. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-STDY3OVM.js} +214 -85
  43. package/dist/chunks/chunk-STDY3OVM.js.map +7 -0
  44. package/dist/chunks/configService-PRJZF7Y6.js +14 -0
  45. package/dist/chunks/{constants-STK2YBIW.js → constants-KIQEYMAM.js} +2 -2
  46. package/dist/chunks/{fs-JGINUXGL.js → fs-ZI5JEU7V.js} +2 -2
  47. package/dist/entries/server-router.js +14 -19
  48. package/dist/entries/server-router.js.map +2 -2
  49. package/dist/lib/client/index.js +638 -332
  50. package/dist/lib/client/index.js.map +2 -2
  51. package/dist/lib/server/index.js +177 -235
  52. package/dist/lib/server/index.js.map +2 -2
  53. package/dist/lib/shared/index.js +80 -44
  54. package/dist/lib/shared/index.js.map +2 -2
  55. package/dist/lib/shared/richtext/index.js +1 -1
  56. package/dist/lib/test-utils/index.js +38 -60
  57. package/dist/lib/test-utils/index.js.map +2 -2
  58. package/entries/client-router.tsx +10 -8
  59. package/entries/server-router.tsx +1 -7
  60. package/lib/client/ClientInitializer.ts +8 -8
  61. package/lib/client/ErrorBoundary.test.tsx +146 -143
  62. package/lib/client/ErrorBoundary.tsx +175 -127
  63. package/lib/client/componentRegistry.test.ts +96 -108
  64. package/lib/client/componentRegistry.ts +1 -2
  65. package/lib/client/contexts/ThemeContext.tsx +3 -2
  66. package/lib/client/core/ComponentBuilder.test.ts +513 -560
  67. package/lib/client/core/ComponentBuilder.ts +318 -146
  68. package/lib/client/core/ComponentRenderer.test.tsx +1 -2
  69. package/lib/client/core/ComponentRenderer.tsx +46 -33
  70. package/lib/client/core/builders/embedBuilder.ts +241 -53
  71. package/lib/client/core/builders/linkBuilder.ts +71 -44
  72. package/lib/client/core/builders/linkNodeBuilder.ts +78 -53
  73. package/lib/client/core/builders/listBuilder.ts +115 -86
  74. package/lib/client/core/builders/localeListBuilder.ts +95 -60
  75. package/lib/client/core/builders/types.ts +5 -5
  76. package/lib/client/core/cmsTemplateProcessor.ts +7 -7
  77. package/lib/client/elementRegistry.ts +3 -3
  78. package/lib/client/fontFamiliesService.test.ts +2 -10
  79. package/lib/client/fontFamiliesService.ts +3 -3
  80. package/lib/client/hmr/HMRManager.tsx +8 -0
  81. package/lib/client/hmrCssReload.ts +14 -8
  82. package/lib/client/hmrWebSocket.ts +9 -14
  83. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  84. package/lib/client/hooks/useColorVariables.ts +12 -10
  85. package/lib/client/hooks/usePropertyAutocomplete.ts +3 -5
  86. package/lib/client/hooks/useVariables.ts +4 -4
  87. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  88. package/lib/client/hydration/HydrationUtils.ts +3 -4
  89. package/lib/client/i18nConfigService.test.ts +2 -7
  90. package/lib/client/i18nConfigService.ts +2 -2
  91. package/lib/client/meno-filter/MenoFilter.test.ts +19 -21
  92. package/lib/client/meno-filter/MenoFilter.ts +5 -9
  93. package/lib/client/meno-filter/bindings.ts +15 -40
  94. package/lib/client/meno-filter/init.ts +1 -1
  95. package/lib/client/meno-filter/renderer.ts +23 -29
  96. package/lib/client/meno-filter/script.generated.ts +1 -3
  97. package/lib/client/meno-filter/ui.ts +3 -5
  98. package/lib/client/meno-filter/updates.ts +15 -21
  99. package/lib/client/navigation.test.ts +159 -159
  100. package/lib/client/navigation.ts +0 -1
  101. package/lib/client/responsiveStyleResolver.test.ts +230 -228
  102. package/lib/client/responsiveStyleResolver.ts +13 -16
  103. package/lib/client/routing/RouteLoader.test.ts +23 -24
  104. package/lib/client/routing/RouteLoader.ts +22 -35
  105. package/lib/client/routing/Router.tsx +31 -3
  106. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  107. package/lib/client/scripts/ScriptExecutor.ts +15 -33
  108. package/lib/client/services/PrefetchService.test.ts +2 -2
  109. package/lib/client/services/PrefetchService.ts +10 -24
  110. package/lib/client/styleProcessor.test.ts +9 -9
  111. package/lib/client/styleProcessor.ts +18 -15
  112. package/lib/client/styles/StyleInjector.test.ts +122 -115
  113. package/lib/client/styles/StyleInjector.ts +9 -6
  114. package/lib/client/styles/UtilityClassCollector.ts +19 -26
  115. package/lib/client/styles/cspNonce.test.ts +2 -5
  116. package/lib/client/templateEngine.test.ts +554 -482
  117. package/lib/client/templateEngine.ts +200 -64
  118. package/lib/client/theme.ts +0 -1
  119. package/lib/client/utils/toast.ts +0 -1
  120. package/lib/server/__integration__/api-routes.test.ts +8 -4
  121. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  122. package/lib/server/__integration__/server-lifecycle.test.ts +2 -5
  123. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  124. package/lib/server/__integration__/static-assets.test.ts +1 -1
  125. package/lib/server/__integration__/test-helpers.ts +84 -70
  126. package/lib/server/ab/generateFunctions.ts +12 -10
  127. package/lib/server/astro/cmsPageEmitter.ts +23 -38
  128. package/lib/server/astro/componentEmitter.ts +25 -36
  129. package/lib/server/astro/cssCollector.ts +10 -26
  130. package/lib/server/astro/nodeToAstro.test.ts +1750 -30
  131. package/lib/server/astro/nodeToAstro.ts +198 -187
  132. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +105 -109
  133. package/lib/server/astro/normalizeOrphanTemplateProps.ts +4 -12
  134. package/lib/server/astro/pageEmitter.ts +9 -13
  135. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  136. package/lib/server/astro/tailwindMapper.ts +33 -40
  137. package/lib/server/astro/templateTransformer.ts +14 -17
  138. package/lib/server/createServer.ts +5 -17
  139. package/lib/server/cssGenerator.test.ts +35 -44
  140. package/lib/server/cssGenerator.ts +6 -17
  141. package/lib/server/fileWatcher.test.ts +124 -10
  142. package/lib/server/fileWatcher.ts +124 -118
  143. package/lib/server/index.ts +7 -1
  144. package/lib/server/jsonLoader.test.ts +39 -2
  145. package/lib/server/jsonLoader.ts +33 -31
  146. package/lib/server/middleware/cors.test.ts +20 -20
  147. package/lib/server/middleware/cors.ts +28 -4
  148. package/lib/server/middleware/errorHandler.test.ts +5 -3
  149. package/lib/server/middleware/errorHandler.ts +3 -8
  150. package/lib/server/middleware/index.ts +0 -1
  151. package/lib/server/middleware/logger.test.ts +7 -5
  152. package/lib/server/middleware/logger.ts +10 -22
  153. package/lib/server/pageCache.test.ts +76 -77
  154. package/lib/server/pageCache.ts +0 -1
  155. package/lib/server/projectContext.ts +4 -3
  156. package/lib/server/providers/fileSystemCMSProvider.test.ts +124 -95
  157. package/lib/server/providers/fileSystemCMSProvider.ts +35 -20
  158. package/lib/server/providers/fileSystemPageProvider.test.ts +84 -0
  159. package/lib/server/providers/fileSystemPageProvider.ts +31 -12
  160. package/lib/server/routes/api/cms.test.ts +26 -14
  161. package/lib/server/routes/api/cms.ts +9 -14
  162. package/lib/server/routes/api/components.ts +30 -34
  163. package/lib/server/routes/api/config.ts +0 -1
  164. package/lib/server/routes/api/core-routes.ts +47 -74
  165. package/lib/server/routes/api/functions.ts +8 -16
  166. package/lib/server/routes/api/index.ts +3 -6
  167. package/lib/server/routes/api/pages.ts +8 -11
  168. package/lib/server/routes/api/shared.test.ts +1 -1
  169. package/lib/server/routes/api/shared.ts +12 -9
  170. package/lib/server/routes/api/variables.test.ts +1 -3
  171. package/lib/server/routes/api/variables.ts +1 -1
  172. package/lib/server/routes/index.ts +23 -26
  173. package/lib/server/routes/pages.ts +34 -29
  174. package/lib/server/routes/static.ts +16 -4
  175. package/lib/server/runtime/bundler.ts +47 -32
  176. package/lib/server/runtime/fs.ts +3 -13
  177. package/lib/server/runtime/httpServer.ts +5 -9
  178. package/lib/server/services/ColorService.ts +32 -27
  179. package/lib/server/services/EnumService.test.ts +2 -6
  180. package/lib/server/services/EnumService.ts +7 -2
  181. package/lib/server/services/VariableService.test.ts +1 -5
  182. package/lib/server/services/VariableService.ts +6 -1
  183. package/lib/server/services/cmsService.test.ts +116 -78
  184. package/lib/server/services/cmsService.ts +24 -54
  185. package/lib/server/services/componentService.test.ts +122 -36
  186. package/lib/server/services/componentService.ts +54 -38
  187. package/lib/server/services/configService.test.ts +9 -31
  188. package/lib/server/services/configService.ts +20 -27
  189. package/lib/server/services/fileWatcherService.ts +40 -6
  190. package/lib/server/services/index.ts +0 -1
  191. package/lib/server/services/pageService.test.ts +1 -3
  192. package/lib/server/services/pageService.ts +14 -13
  193. package/lib/server/ssr/attributeBuilder.ts +16 -6
  194. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  195. package/lib/server/ssr/clientDataInjector.ts +7 -21
  196. package/lib/server/ssr/cmsSSRProcessor.ts +3 -6
  197. package/lib/server/ssr/cssCollector.ts +1 -1
  198. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  199. package/lib/server/ssr/errorOverlay.ts +3 -9
  200. package/lib/server/ssr/htmlGenerator.nonce.test.ts +3 -9
  201. package/lib/server/ssr/htmlGenerator.test.ts +120 -43
  202. package/lib/server/ssr/htmlGenerator.ts +120 -85
  203. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  204. package/lib/server/ssr/imageMetadata.ts +25 -19
  205. package/lib/server/ssr/jsCollector.test.ts +3 -13
  206. package/lib/server/ssr/jsCollector.ts +3 -8
  207. package/lib/server/ssr/liveReloadIntegration.test.ts +69 -28
  208. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  209. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  210. package/lib/server/ssr/ssrRenderer.test.ts +196 -246
  211. package/lib/server/ssr/ssrRenderer.ts +609 -225
  212. package/lib/server/ssrRenderer.test.ts +1044 -950
  213. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  214. package/lib/server/utils/jsonLineMapper.ts +1 -1
  215. package/lib/server/validateStyleCoverage.ts +18 -20
  216. package/lib/server/webflow/buildWebflow.ts +41 -53
  217. package/lib/server/webflow/nodeToWebflow.test.ts +150 -218
  218. package/lib/server/webflow/nodeToWebflow.ts +195 -258
  219. package/lib/server/webflow/styleMapper.test.ts +15 -56
  220. package/lib/server/webflow/styleMapper.ts +33 -41
  221. package/lib/server/webflow/types.ts +1 -8
  222. package/lib/server/websocketManager.ts +16 -19
  223. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  224. package/lib/shared/attributeNodeUtils.ts +5 -12
  225. package/lib/shared/breakpoints.ts +4 -11
  226. package/lib/shared/cmsQueryParser.test.ts +50 -42
  227. package/lib/shared/cmsQueryParser.ts +49 -31
  228. package/lib/shared/colorVariableUtils.test.ts +5 -5
  229. package/lib/shared/colorVariableUtils.ts +3 -8
  230. package/lib/shared/componentRefs.ts +1 -5
  231. package/lib/shared/constants.test.ts +3 -3
  232. package/lib/shared/constants.ts +4 -8
  233. package/lib/shared/cssGeneration.test.ts +262 -144
  234. package/lib/shared/cssGeneration.ts +189 -516
  235. package/lib/shared/cssNamedColors.ts +152 -30
  236. package/lib/shared/cssProperties.test.ts +5 -6
  237. package/lib/shared/cssProperties.ts +296 -112
  238. package/lib/shared/elementClassName.test.ts +109 -109
  239. package/lib/shared/elementClassName.ts +1 -1
  240. package/lib/shared/elementUtils.ts +12 -16
  241. package/lib/shared/errorLogger.ts +2 -10
  242. package/lib/shared/errors.test.ts +2 -13
  243. package/lib/shared/errors.ts +2 -8
  244. package/lib/shared/expressionEvaluator.test.ts +119 -0
  245. package/lib/shared/expressionEvaluator.ts +43 -24
  246. package/lib/shared/fontLoader.test.ts +19 -5
  247. package/lib/shared/friendlyError.ts +2 -3
  248. package/lib/shared/gradientUtils.test.ts +1 -5
  249. package/lib/shared/gradientUtils.ts +2 -6
  250. package/lib/shared/hrefRefs.ts +2 -10
  251. package/lib/shared/i18n.test.ts +1 -1
  252. package/lib/shared/i18n.ts +13 -34
  253. package/lib/shared/index.ts +4 -0
  254. package/lib/shared/inlineSvgStyleRules.ts +2 -4
  255. package/lib/shared/interactiveStyleMappings.test.ts +11 -33
  256. package/lib/shared/interactiveStyleMappings.ts +9 -16
  257. package/lib/shared/interactiveStyles.test.ts +165 -188
  258. package/lib/shared/interfaces/contentProvider.ts +1 -1
  259. package/lib/shared/itemTemplateUtils.test.ts +6 -12
  260. package/lib/shared/itemTemplateUtils.ts +19 -35
  261. package/lib/shared/jsonRepair.ts +8 -2
  262. package/lib/shared/libraryLoader.test.ts +15 -49
  263. package/lib/shared/libraryLoader.ts +7 -22
  264. package/lib/shared/netlifyLocale404.test.ts +179 -0
  265. package/lib/shared/netlifyLocale404.ts +110 -0
  266. package/lib/shared/nodeUtils.test.ts +24 -16
  267. package/lib/shared/nodeUtils.ts +49 -19
  268. package/lib/shared/pathArrayUtils.test.ts +1 -2
  269. package/lib/shared/pathArrayUtils.ts +1 -1
  270. package/lib/shared/pathSecurity.ts +1 -1
  271. package/lib/shared/pathUtils.test.ts +4 -6
  272. package/lib/shared/pathUtils.ts +42 -48
  273. package/lib/shared/paths/Path.test.ts +2 -2
  274. package/lib/shared/paths/Path.ts +0 -1
  275. package/lib/shared/paths/PathConverter.test.ts +1 -1
  276. package/lib/shared/paths/PathConverter.ts +14 -17
  277. package/lib/shared/paths/PathUtils.ts +9 -10
  278. package/lib/shared/paths/PathValidator.test.ts +2 -15
  279. package/lib/shared/paths/PathValidator.ts +11 -9
  280. package/lib/shared/paths/index.ts +1 -2
  281. package/lib/shared/propResolver.test.ts +240 -244
  282. package/lib/shared/propResolver.ts +14 -25
  283. package/lib/shared/pxToRem.test.ts +7 -6
  284. package/lib/shared/pxToRem.ts +2 -5
  285. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +9 -5
  286. package/lib/shared/registry/ClientRegistry.ts +0 -1
  287. package/lib/shared/registry/ComponentRegistry.test.ts +43 -29
  288. package/lib/shared/registry/ComponentRegistry.ts +9 -11
  289. package/lib/shared/registry/NodeTypeDefinition.ts +15 -8
  290. package/lib/shared/registry/RegistryManager.ts +1 -2
  291. package/lib/shared/registry/SSRRegistry.ts +0 -1
  292. package/lib/shared/registry/createNodeType.ts +7 -9
  293. package/lib/shared/registry/defineNodeType.ts +2 -6
  294. package/lib/shared/registry/index.ts +0 -1
  295. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  296. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  297. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +47 -18
  298. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  299. package/lib/shared/registry/nodeTypes/ListNodeType.ts +78 -74
  300. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  301. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -7
  302. package/lib/shared/registry/nodeTypes/index.ts +10 -2
  303. package/lib/shared/responsiveScaling.test.ts +15 -31
  304. package/lib/shared/responsiveScaling.ts +55 -37
  305. package/lib/shared/responsiveStyleUtils.ts +11 -13
  306. package/lib/shared/richtext/htmlToTiptap.test.ts +23 -14
  307. package/lib/shared/richtext/htmlToTiptap.ts +1 -3
  308. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  309. package/lib/shared/richtext/types.ts +1 -8
  310. package/lib/shared/slugTranslator.test.ts +13 -13
  311. package/lib/shared/slugTranslator.ts +12 -16
  312. package/lib/shared/slugify.ts +9 -15
  313. package/lib/shared/styleNodeUtils.test.ts +8 -8
  314. package/lib/shared/styleNodeUtils.ts +5 -10
  315. package/lib/shared/styleUtils.test.ts +87 -61
  316. package/lib/shared/styleUtils.ts +5 -6
  317. package/lib/shared/themeDefaults.test.ts +11 -11
  318. package/lib/shared/themeDefaults.ts +3 -4
  319. package/lib/shared/tree/PathBuilder.test.ts +62 -138
  320. package/lib/shared/tree/PathBuilder.ts +49 -39
  321. package/lib/shared/treePathUtils.test.ts +2 -10
  322. package/lib/shared/treePathUtils.ts +54 -59
  323. package/lib/shared/types/api.ts +1 -2
  324. package/lib/shared/types/cms.ts +25 -21
  325. package/lib/shared/types/comment.ts +49 -16
  326. package/lib/shared/types/components.ts +27 -25
  327. package/lib/shared/types/errors.test.ts +1 -6
  328. package/lib/shared/types/errors.ts +3 -7
  329. package/lib/shared/types/experiments.ts +28 -28
  330. package/lib/shared/types/index.ts +2 -2
  331. package/lib/shared/types/styles.ts +0 -1
  332. package/lib/shared/types/variables.test.ts +4 -13
  333. package/lib/shared/types/variables.ts +48 -27
  334. package/lib/shared/types.ts +1 -2
  335. package/lib/shared/utilityClassConfig.ts +648 -321
  336. package/lib/shared/utilityClassMapper.test.ts +203 -92
  337. package/lib/shared/utilityClassMapper.ts +188 -246
  338. package/lib/shared/utilityClassNames.ts +326 -0
  339. package/lib/shared/utils.test.ts +2 -10
  340. package/lib/shared/utils.ts +19 -10
  341. package/lib/shared/validation/cmsValidators.ts +2 -1
  342. package/lib/shared/validation/commentValidators.test.ts +53 -0
  343. package/lib/shared/validation/commentValidators.ts +12 -1
  344. package/lib/shared/validation/propValidator.test.ts +18 -20
  345. package/lib/shared/validation/propValidator.ts +12 -17
  346. package/lib/shared/validation/schemas.test.ts +24 -33
  347. package/lib/shared/validation/schemas.ts +469 -344
  348. package/lib/shared/validation/validators.test.ts +1 -6
  349. package/lib/shared/validation/validators.ts +89 -68
  350. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  351. package/lib/shared/viewportUnits.test.ts +11 -23
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +3 -7
  354. package/lib/test-utils/factories/DomMockFactory.ts +7 -19
  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 +5 -9
  358. package/lib/test-utils/factories/StoreMockFactory.ts +14 -25
  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 +12 -18
  365. package/lib/test-utils/mocks.ts +4 -2
  366. package/package.json +1 -1
  367. package/scripts/build-meno-filter.ts +1 -4
  368. package/vite.config.ts +4 -4
  369. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  370. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  371. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  372. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  373. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  374. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  375. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  376. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  377. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  378. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  379. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  380. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  381. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  382. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  383. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  384. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  385. /package/dist/chunks/{configService-R3OGU2UD.js.map → configService-PRJZF7Y6.js.map} +0 -0
  386. /package/dist/chunks/{constants-STK2YBIW.js.map → constants-KIQEYMAM.js.map} +0 -0
  387. /package/dist/chunks/{fs-JGINUXGL.js.map → fs-ZI5JEU7V.js.map} +0 -0
@@ -1,361 +1,382 @@
1
- import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";
2
- import { setGlobalTemplateContext, resetGlobalTemplateContext } from "../shared/globalTemplateContext";
3
- import {
4
- evaluateTemplate,
5
- resolveStyleMapping,
6
- processStructure,
7
- normalizeStyle
8
- } from "./templateEngine";
9
- import type { ComponentNode, ComponentDefinition, StructuredComponentDefinition, StyleMapping, TemplateContext } from "../shared/types";
10
-
11
- describe("Template Engine - evaluateTemplate", () => {
12
- describe("Complex template expressions", () => {
13
- test("should evaluate nested property access", () => {
14
- const template = "{{variants[variant].background}}";
1
+ import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test';
2
+ import { setGlobalTemplateContext, resetGlobalTemplateContext } from '../shared/globalTemplateContext';
3
+ import { evaluateTemplate, resolveStyleMapping, processStructure, normalizeStyle } from './templateEngine';
4
+ import type {
5
+ ComponentNode,
6
+ ComponentDefinition,
7
+ StructuredComponentDefinition,
8
+ StyleMapping,
9
+ TemplateContext,
10
+ } from '../shared/types';
11
+
12
+ describe('Template Engine - evaluateTemplate', () => {
13
+ describe('Complex template expressions', () => {
14
+ test('should evaluate nested property access', () => {
15
+ const template = '{{variants[variant].background}}';
15
16
  const context = {
16
17
  variants: {
17
- primary: { background: "#0070f3" },
18
- secondary: { background: "#f3f4f6" }
18
+ primary: { background: '#0070f3' },
19
+ secondary: { background: '#f3f4f6' },
19
20
  },
20
- variant: "primary"
21
+ variant: 'primary',
21
22
  };
22
-
23
+
23
24
  const result = evaluateTemplate(template, context);
24
- expect(result).toBe("#0070f3");
25
+ expect(result).toBe('#0070f3');
25
26
  });
26
27
 
27
- test("should evaluate ternary operators", () => {
28
+ test('should evaluate ternary operators', () => {
28
29
  const template = "{{disabled ? '0.5' : '1'}}";
29
30
  const context = { disabled: true };
30
-
31
+
31
32
  const result = evaluateTemplate(template, context);
32
- expect(result).toBe("0.5");
33
+ expect(result).toBe('0.5');
33
34
  });
34
35
 
35
- test("should evaluate ternary operators with false condition", () => {
36
+ test('should evaluate ternary operators with false condition', () => {
36
37
  const template = "{{disabled ? '0.5' : '1'}}";
37
38
  const context = { disabled: false };
38
-
39
+
39
40
  const result = evaluateTemplate(template, context);
40
- expect(result).toBe("1");
41
+ expect(result).toBe('1');
41
42
  });
42
43
 
43
- test("should evaluate mathematical operations - addition", () => {
44
- const template = "{{count + 1}}";
44
+ test('should evaluate mathematical operations - addition', () => {
45
+ const template = '{{count + 1}}';
45
46
  const context = { count: 5 };
46
-
47
+
47
48
  const result = evaluateTemplate(template, context);
48
49
  expect(result).toBe(6);
49
50
  });
50
51
 
51
- test("should evaluate mathematical operations - multiplication", () => {
52
- const template = "{{price * 1.1}}";
52
+ test('should evaluate mathematical operations - multiplication', () => {
53
+ const template = '{{price * 1.1}}';
53
54
  const context = { price: 100 };
54
-
55
+
55
56
  const result = evaluateTemplate(template, context);
56
57
  expect(result).toBeCloseTo(110, 5);
57
58
  });
58
59
 
59
- test("should evaluate string concatenation", () => {
60
+ test('should evaluate string concatenation', () => {
60
61
  const template = "{{'Hello ' + name}}";
61
- const context = { name: "World" };
62
-
62
+ const context = { name: 'World' };
63
+
63
64
  const result = evaluateTemplate(template, context);
64
- expect(result).toBe("Hello World");
65
+ expect(result).toBe('Hello World');
65
66
  });
66
67
 
67
- test("should evaluate logical AND operator", () => {
68
+ test('should evaluate logical AND operator', () => {
68
69
  const template = "{{isActive && 'active'}}";
69
70
  const context = { isActive: true };
70
-
71
+
71
72
  const result = evaluateTemplate(template, context);
72
- expect(result).toBe("active");
73
+ expect(result).toBe('active');
73
74
  });
74
75
 
75
- test("should evaluate logical AND operator with false", () => {
76
+ test('should evaluate logical AND operator with false', () => {
76
77
  const template = "{{isActive && 'active'}}";
77
78
  const context = { isActive: false };
78
-
79
+
79
80
  const result = evaluateTemplate(template, context);
80
81
  expect(result).toBe(false);
81
82
  });
82
83
 
83
- test("should evaluate complex nested expressions", () => {
84
+ test('should evaluate complex nested expressions', () => {
84
85
  const template = "{{user.profile.name || 'Anonymous'}}";
85
- const context = { user: { profile: { name: "John" } } };
86
-
86
+ const context = { user: { profile: { name: 'John' } } };
87
+
87
88
  const result = evaluateTemplate(template, context);
88
- expect(result).toBe("John");
89
+ expect(result).toBe('John');
89
90
  });
90
91
 
91
- test("should evaluate expressions with fallback", () => {
92
+ test('should evaluate expressions with fallback', () => {
92
93
  const template = "{{user.profile.name || 'Anonymous'}}";
93
94
  const context = { user: { profile: {} } };
94
-
95
+
95
96
  const result = evaluateTemplate(template, context);
96
- expect(result).toBe("Anonymous");
97
+ expect(result).toBe('Anonymous');
97
98
  });
98
99
  });
99
100
 
100
- describe("Edge cases", () => {
101
- test("should return original string if not a template", () => {
102
- const template = "plain text";
103
- const context = { name: "John" };
104
-
101
+ describe('Edge cases', () => {
102
+ test('should return original string if not a template', () => {
103
+ const template = 'plain text';
104
+ const context = { name: 'John' };
105
+
105
106
  const result = evaluateTemplate(template, context);
106
- expect(result).toBe("plain text");
107
+ expect(result).toBe('plain text');
107
108
  });
108
109
 
109
- test("should return template string on invalid expression", () => {
110
- const template = "{{invalid.expression}}";
110
+ test('should return template string on invalid expression', () => {
111
+ const template = '{{invalid.expression}}';
111
112
  const context = {};
112
-
113
+
113
114
  const result = evaluateTemplate(template, context);
114
115
  expect(result).toBe(template); // Should return original template on error
115
116
  });
116
117
 
117
- test("should return template string on syntax error", () => {
118
- const template = "{{count +}}"; // Missing operand
118
+ test('should return template string on syntax error', () => {
119
+ const template = '{{count +}}'; // Missing operand
119
120
  const context = { count: 5 };
120
-
121
+
121
122
  const result = evaluateTemplate(template, context);
122
123
  expect(result).toBe(template); // Should return original template on error
123
124
  });
124
125
 
125
- test("should handle missing context variables", () => {
126
- const template = "{{undefinedVar}}";
126
+ test('should handle missing context variables', () => {
127
+ const template = '{{undefinedVar}}';
127
128
  const context = {};
128
-
129
+
129
130
  const result = evaluateTemplate(template, context);
130
131
  expect(result).toBe(template); // Should return original template when var is undefined
131
132
  });
132
133
 
133
- test("should handle non-string templates", () => {
134
+ test('should handle non-string templates', () => {
134
135
  const template = 123 as unknown as string;
135
136
  const context = {};
136
-
137
+
137
138
  const result = evaluateTemplate(template, context);
138
139
  expect(result).toBe(123); // Should pass through non-strings
139
140
  });
140
141
 
141
- test("should handle empty template expression", () => {
142
- const template = "{{}}";
142
+ test('should handle empty template expression', () => {
143
+ const template = '{{}}';
143
144
  const context = {};
144
-
145
+
145
146
  const result = evaluateTemplate(template, context);
146
147
  expect(result).toBe(template); // Invalid expression
147
148
  });
148
149
 
149
- test("should handle template with whitespace", () => {
150
- const template = "{{ name }}";
151
- const context = { name: "John" };
152
-
150
+ test('should handle template with whitespace', () => {
151
+ const template = '{{ name }}';
152
+ const context = { name: 'John' };
153
+
153
154
  const result = evaluateTemplate(template, context);
154
- expect(result).toBe("John"); // Should trim whitespace
155
+ expect(result).toBe('John'); // Should trim whitespace
156
+ });
157
+
158
+ test('NaN result (arithmetic over a missing identifier) preserves the template', () => {
159
+ // A loop-var stagger evaluated by the props pass BEFORE the per-item pass:
160
+ // itemIndex isn't in context yet → 0.4 + undefined*0.2 = NaN. Returning NaN
161
+ // poisoned the attribute (React: "Received NaN for the `fade` attribute") and
162
+ // destroyed the template before the item pass could resolve it.
163
+ const template = '{{0.4+itemIndex*0.2}}';
164
+
165
+ expect(evaluateTemplate(template, {})).toBe(template);
166
+ expect(evaluateTemplate(template, { fade: '0.2' })).toBe(template);
167
+ });
168
+
169
+ test('the same arithmetic template resolves once the identifier is in context', () => {
170
+ expect(evaluateTemplate('{{0.4+itemIndex*0.2}}', { itemIndex: 2 })).toBeCloseTo(0.8);
171
+ });
172
+
173
+ test('a legitimately-numeric result still passes through (only NaN is preserved)', () => {
174
+ expect(evaluateTemplate('{{count / 0}}', { count: 5 })).toBe(Infinity);
175
+ expect(evaluateTemplate('{{count - 5}}', { count: 5 })).toBe(0);
155
176
  });
156
177
  });
157
178
  });
158
179
 
159
- describe("Template Engine - resolveStyleMapping", () => {
160
- describe("Valid style mappings", () => {
161
- test("should resolve prop-based style mapping", () => {
180
+ describe('Template Engine - resolveStyleMapping', () => {
181
+ describe('Valid style mappings', () => {
182
+ test('should resolve prop-based style mapping', () => {
162
183
  const mapping: StyleMapping = {
163
184
  _mapping: true,
164
- prop: "variant",
185
+ prop: 'variant',
165
186
  values: {
166
- primary: "#0070f3",
167
- secondary: "#f3f4f6"
168
- }
187
+ primary: '#0070f3',
188
+ secondary: '#f3f4f6',
189
+ },
169
190
  };
170
- const props = { variant: "primary" };
171
-
191
+ const props = { variant: 'primary' };
192
+
172
193
  const result = resolveStyleMapping(mapping, props);
173
- expect(result).toBe("#0070f3");
194
+ expect(result).toBe('#0070f3');
174
195
  });
175
196
 
176
- test("should resolve style mapping for secondary variant", () => {
197
+ test('should resolve style mapping for secondary variant', () => {
177
198
  const mapping: StyleMapping = {
178
199
  _mapping: true,
179
- prop: "variant",
200
+ prop: 'variant',
180
201
  values: {
181
- primary: "#0070f3",
182
- secondary: "#f3f4f6"
183
- }
202
+ primary: '#0070f3',
203
+ secondary: '#f3f4f6',
204
+ },
184
205
  };
185
- const props = { variant: "secondary" };
186
-
206
+ const props = { variant: 'secondary' };
207
+
187
208
  const result = resolveStyleMapping(mapping, props);
188
- expect(result).toBe("#f3f4f6");
209
+ expect(result).toBe('#f3f4f6');
189
210
  });
190
211
 
191
- test("should resolve numeric style mapping values", () => {
212
+ test('should resolve numeric style mapping values', () => {
192
213
  const mapping: StyleMapping = {
193
214
  _mapping: true,
194
- prop: "size",
215
+ prop: 'size',
195
216
  values: {
196
217
  small: 12,
197
- large: 24
198
- }
218
+ large: 24,
219
+ },
199
220
  };
200
- const props = { size: "small" };
201
-
221
+ const props = { size: 'small' };
222
+
202
223
  const result = resolveStyleMapping(mapping, props);
203
224
  expect(result).toBe(12);
204
225
  });
205
226
  });
206
227
 
207
- describe("Missing or invalid mappings", () => {
208
- test("should return undefined for missing prop value", () => {
228
+ describe('Missing or invalid mappings', () => {
229
+ test('should return undefined for missing prop value', () => {
209
230
  const mapping: StyleMapping = {
210
231
  _mapping: true,
211
- prop: "variant",
232
+ prop: 'variant',
212
233
  values: {
213
- primary: "#0070f3",
214
- secondary: "#f3f4f6"
215
- }
234
+ primary: '#0070f3',
235
+ secondary: '#f3f4f6',
236
+ },
216
237
  };
217
238
  const props = {}; // variant not provided
218
-
239
+
219
240
  const result = resolveStyleMapping(mapping, props);
220
241
  expect(result).toBeUndefined();
221
242
  });
222
243
 
223
- test("should return undefined for null prop value", () => {
244
+ test('should return undefined for null prop value', () => {
224
245
  const mapping: StyleMapping = {
225
246
  _mapping: true,
226
- prop: "variant",
247
+ prop: 'variant',
227
248
  values: {
228
- primary: "#0070f3"
229
- }
249
+ primary: '#0070f3',
250
+ },
230
251
  };
231
252
  const props = { variant: null };
232
-
253
+
233
254
  const result = resolveStyleMapping(mapping, props);
234
255
  expect(result).toBeUndefined();
235
256
  });
236
257
 
237
- test("should return undefined for prop value not in mapping", () => {
258
+ test('should return undefined for prop value not in mapping', () => {
238
259
  const mapping: StyleMapping = {
239
260
  _mapping: true,
240
- prop: "variant",
261
+ prop: 'variant',
241
262
  values: {
242
- primary: "#0070f3"
243
- }
263
+ primary: '#0070f3',
264
+ },
244
265
  };
245
- const props = { variant: "invalid" };
246
-
266
+ const props = { variant: 'invalid' };
267
+
247
268
  const result = resolveStyleMapping(mapping, props);
248
269
  expect(result).toBeUndefined();
249
270
  });
250
271
 
251
- test("should return undefined for invalid mapping object (no _mapping)", () => {
272
+ test('should return undefined for invalid mapping object (no _mapping)', () => {
252
273
  const mapping = {
253
- prop: "variant",
254
- values: { primary: "#0070f3" }
274
+ prop: 'variant',
275
+ values: { primary: '#0070f3' },
255
276
  };
256
- const props = { variant: "primary" };
257
-
277
+ const props = { variant: 'primary' };
278
+
258
279
  const result = resolveStyleMapping(mapping, props);
259
280
  expect(result).toBeUndefined();
260
281
  });
261
282
 
262
- test("should return undefined for non-object mapping", () => {
263
- const mapping = "not an object";
264
- const props = { variant: "primary" };
265
-
283
+ test('should return undefined for non-object mapping', () => {
284
+ const mapping = 'not an object';
285
+ const props = { variant: 'primary' };
286
+
266
287
  const result = resolveStyleMapping(mapping, props);
267
288
  expect(result).toBeUndefined();
268
289
  });
269
290
 
270
- test("should return undefined for null mapping", () => {
291
+ test('should return undefined for null mapping', () => {
271
292
  const mapping = null;
272
- const props = { variant: "primary" };
273
-
293
+ const props = { variant: 'primary' };
294
+
274
295
  const result = resolveStyleMapping(mapping, props);
275
296
  expect(result).toBeUndefined();
276
297
  });
277
298
 
278
- test("should return undefined for mapping with missing prop field", () => {
299
+ test('should return undefined for mapping with missing prop field', () => {
279
300
  const mapping = {
280
301
  _mapping: true,
281
- values: { primary: "#0070f3" }
302
+ values: { primary: '#0070f3' },
282
303
  };
283
- const props = { variant: "primary" };
284
-
304
+ const props = { variant: 'primary' };
305
+
285
306
  const result = resolveStyleMapping(mapping, props);
286
307
  expect(result).toBeUndefined();
287
308
  });
288
309
 
289
- test("should return undefined for mapping with missing values field", () => {
310
+ test('should return undefined for mapping with missing values field', () => {
290
311
  const mapping = {
291
312
  _mapping: true,
292
- prop: "variant"
313
+ prop: 'variant',
293
314
  };
294
- const props = { variant: "primary" };
295
-
315
+ const props = { variant: 'primary' };
316
+
296
317
  const result = resolveStyleMapping(mapping, props);
297
318
  expect(result).toBeUndefined();
298
319
  });
299
320
  });
300
321
  });
301
322
 
302
- describe("Template Engine - processStructure", () => {
323
+ describe('Template Engine - processStructure', () => {
303
324
  const mockComponentDef: StructuredComponentDefinition = {
304
325
  interface: {},
305
- structure: { type: 'node' as const, tag: 'div' }
326
+ structure: { type: 'node' as const, tag: 'div' },
306
327
  };
307
328
 
308
329
  // Helper to create TemplateContext
309
330
  const createContext = (props: Record<string, unknown>, componentDef = mockComponentDef): TemplateContext => ({
310
331
  props,
311
- componentDef
332
+ componentDef,
312
333
  });
313
334
 
314
- describe("String processing", () => {
315
- test("should process template strings in structure", () => {
316
- const structure = "{{name}}";
317
- const props = { name: "John" };
335
+ describe('String processing', () => {
336
+ test('should process template strings in structure', () => {
337
+ const structure = '{{name}}';
338
+ const props = { name: 'John' };
318
339
 
319
340
  const result = processStructure(structure, createContext(props));
320
- expect(result).toBe("John");
341
+ expect(result).toBe('John');
321
342
  });
322
343
 
323
- test("should process non-template strings", () => {
324
- const structure = "plain text";
344
+ test('should process non-template strings', () => {
345
+ const structure = 'plain text';
325
346
  const props = {};
326
347
 
327
348
  const result = processStructure(structure, createContext(props));
328
- expect(result).toBe("plain text");
349
+ expect(result).toBe('plain text');
329
350
  });
330
351
  });
331
352
 
332
- describe("Array processing", () => {
333
- test("should process array of strings", () => {
334
- const structure = ["Hello", "{{name}}", "World"] as any;
335
- const props = { name: "John" };
353
+ describe('Array processing', () => {
354
+ test('should process array of strings', () => {
355
+ const structure = ['Hello', '{{name}}', 'World'] as any;
356
+ const props = { name: 'John' };
336
357
 
337
358
  const result = processStructure(structure, createContext(props));
338
359
  expect(Array.isArray(result)).toBe(true);
339
360
  const resultArray = result as (string | ComponentNode)[];
340
- expect(resultArray).toEqual(["Hello", "John", "World"]);
361
+ expect(resultArray).toEqual(['Hello', 'John', 'World']);
341
362
  });
342
363
 
343
- test("should process array of component nodes", () => {
364
+ test('should process array of component nodes', () => {
344
365
  const structure: ComponentNode[] = [
345
- { type: "node", tag: "div", children: ["{{text}}"] },
346
- { type: "node", tag: "span", children: ["Static"] }
366
+ { type: 'node', tag: 'div', children: ['{{text}}'] },
367
+ { type: 'node', tag: 'span', children: ['Static'] },
347
368
  ];
348
- const props = { text: "Dynamic" };
369
+ const props = { text: 'Dynamic' };
349
370
 
350
371
  const result = processStructure(structure, createContext(props));
351
372
  expect(Array.isArray(result)).toBe(true);
352
373
  const resultArray = result as ComponentNode[];
353
- expect(resultArray[0].children).toEqual(["Dynamic"]);
354
- expect(resultArray[1].children).toEqual(["Static"]);
374
+ expect(resultArray[0].children).toEqual(['Dynamic']);
375
+ expect(resultArray[1].children).toEqual(['Static']);
355
376
  });
356
377
 
357
- test("should filter null values from arrays", () => {
358
- const structure = ["Hello", null, "World"] as any;
378
+ test('should filter null values from arrays', () => {
379
+ const structure = ['Hello', null, 'World'] as any;
359
380
  const props = {};
360
381
 
361
382
  const result = processStructure(structure, createContext(props));
@@ -365,217 +386,224 @@ describe("Template Engine - processStructure", () => {
365
386
  });
366
387
  });
367
388
 
368
- describe("Component node processing", () => {
369
- test("should process simple component node", () => {
389
+ describe('Component node processing', () => {
390
+ test('should process simple component node', () => {
370
391
  const structure: ComponentNode = {
371
- type: "node",
372
- tag: "div",
373
- children: ["Hello World"]
392
+ type: 'node',
393
+ tag: 'div',
394
+ children: ['Hello World'],
374
395
  };
375
396
  const props = {};
376
397
 
377
398
  const result = processStructure(structure, createContext(props));
378
399
  expect(typeof result === 'object' && !Array.isArray(result)).toBe(true);
379
400
  const node = result as ComponentNode;
380
- expect(node.tag).toBe("div");
381
- expect(node.children).toEqual(["Hello World"]);
401
+ expect(node.tag).toBe('div');
402
+ expect(node.children).toEqual(['Hello World']);
382
403
  });
383
404
 
384
- test("should process component node with template in children", () => {
405
+ test('should process component node with template in children', () => {
385
406
  const structure: ComponentNode = {
386
- type: "node",
387
- tag: "div",
388
- children: ["{{greeting}}"]
407
+ type: 'node',
408
+ tag: 'div',
409
+ children: ['{{greeting}}'],
389
410
  };
390
- const props = { greeting: "Hello" };
411
+ const props = { greeting: 'Hello' };
391
412
 
392
413
  const result = processStructure(structure, createContext(props));
393
414
  const node = result as ComponentNode;
394
- expect(node.children).toEqual(["Hello"]);
415
+ expect(node.children).toEqual(['Hello']);
395
416
  });
396
417
 
397
- test("should process template in tag property", () => {
418
+ test('should process template in tag property', () => {
398
419
  const structure: ComponentNode = {
399
- type: "node",
400
- tag: "h{{size}}",
401
- children: ["Heading"]
420
+ type: 'node',
421
+ tag: 'h{{size}}',
422
+ children: ['Heading'],
402
423
  };
403
424
  const props = { size: 1 };
404
425
 
405
426
  const result = processStructure(structure, createContext(props));
406
427
  const node = result as ComponentNode;
407
- expect(node.tag).toBe("h1");
408
- expect(node.children).toEqual(["Heading"]);
428
+ expect(node.tag).toBe('h1');
429
+ expect(node.children).toEqual(['Heading']);
409
430
  });
410
431
 
411
- test("should process template in tag with string prop", () => {
432
+ test('should process template in tag with string prop', () => {
412
433
  const structure: ComponentNode = {
413
- type: "node",
414
- tag: "h{{level}}",
415
- children: ["Title"]
434
+ type: 'node',
435
+ tag: 'h{{level}}',
436
+ children: ['Title'],
416
437
  };
417
- const props = { level: "2" };
438
+ const props = { level: '2' };
418
439
 
419
440
  const result = processStructure(structure, createContext(props));
420
441
  const node = result as ComponentNode;
421
- expect(node.tag).toBe("h2");
442
+ expect(node.tag).toBe('h2');
422
443
  });
423
444
 
424
- test("should process nested component structures", () => {
445
+ test('should process nested component structures', () => {
425
446
  const structure: ComponentNode = {
426
- type: "node",
427
- tag: "div",
447
+ type: 'node',
448
+ tag: 'div',
428
449
  children: [
429
450
  {
430
- type: "node",
431
- tag: "span",
432
- children: ["{{text}}"]
433
- }
434
- ]
451
+ type: 'node',
452
+ tag: 'span',
453
+ children: ['{{text}}'],
454
+ },
455
+ ],
435
456
  };
436
- const props = { text: "Nested" };
457
+ const props = { text: 'Nested' };
437
458
 
438
459
  const result = processStructure(structure, createContext(props));
439
460
  const node = result as ComponentNode;
440
461
  const children = node.children as (string | ComponentNode)[];
441
462
  expect(children?.[0]).toBeDefined();
442
463
  const child = children[0] as ComponentNode;
443
- expect(child.children).toEqual(["Nested"]);
464
+ expect(child.children).toEqual(['Nested']);
444
465
  });
445
466
  });
446
467
 
447
- describe("Style mapping resolution in structure", () => {
448
- test("should resolve style mappings in flat style objects", () => {
468
+ describe('Style mapping resolution in structure', () => {
469
+ test('should resolve style mappings in flat style objects', () => {
449
470
  const structure: ComponentNode = {
450
- type: "node",
451
- tag: "button",
471
+ type: 'node',
472
+ tag: 'button',
452
473
  style: {
453
474
  base: {
454
475
  background: {
455
476
  _mapping: true,
456
- prop: "variant",
477
+ prop: 'variant',
457
478
  values: {
458
- primary: "#0070f3",
459
- secondary: "#f3f4f6"
460
- }
479
+ primary: '#0070f3',
480
+ secondary: '#f3f4f6',
481
+ },
461
482
  } as StyleMapping,
462
- color: "#000"
463
- }
464
- }
483
+ color: '#000',
484
+ },
485
+ },
465
486
  };
466
- const props = { variant: "primary" };
487
+ const props = { variant: 'primary' };
467
488
 
468
489
  const result = processStructure(structure, createContext(props));
469
490
  const node = result as ComponentNode;
470
491
  const baseStyle = (node.style as any)?.base;
471
- expect(baseStyle?.background).toBe("#0070f3");
472
- expect(baseStyle?.color).toBe("#000");
492
+ expect(baseStyle?.background).toBe('#0070f3');
493
+ expect(baseStyle?.color).toBe('#000');
473
494
  });
474
495
 
475
- test("should resolve style mappings in responsive breakpoint styles (tablet/mobile)", () => {
496
+ test('should resolve style mappings in responsive breakpoint styles (tablet/mobile)', () => {
476
497
  const structure: ComponentNode = {
477
- type: "node",
478
- tag: "h1",
498
+ type: 'node',
499
+ tag: 'h1',
479
500
  style: {
480
501
  base: {
481
502
  fontSize: {
482
503
  _mapping: true,
483
- prop: "size",
484
- values: { "1": "48px", "2": "40px", "default": "48px" }
504
+ prop: 'size',
505
+ values: { '1': '48px', '2': '40px', default: '48px' },
485
506
  } as StyleMapping,
486
- fontWeight: "700"
507
+ fontWeight: '700',
487
508
  },
488
509
  tablet: {
489
510
  fontSize: {
490
511
  _mapping: true,
491
- prop: "size",
492
- values: { "1": "132px", "2": "35px", "default": "42px" }
512
+ prop: 'size',
513
+ values: { '1': '132px', '2': '35px', default: '42px' },
493
514
  } as StyleMapping,
494
515
  },
495
- mobile: {}
496
- }
516
+ mobile: {},
517
+ },
497
518
  };
498
- const props = { size: "1" };
519
+ const props = { size: '1' };
499
520
 
500
521
  const result = processStructure(structure, createContext(props));
501
522
  const node = result as ComponentNode;
502
523
  const style = node.style as any;
503
- expect(style?.base?.fontSize).toBe("48px");
504
- expect(style?.base?.fontWeight).toBe("700");
505
- expect(style?.tablet?.fontSize).toBe("132px");
524
+ expect(style?.base?.fontSize).toBe('48px');
525
+ expect(style?.base?.fontWeight).toBe('700');
526
+ expect(style?.tablet?.fontSize).toBe('132px');
506
527
  expect(style?.mobile).toEqual({});
507
528
  });
508
529
 
509
- test("should resolve responsive style mappings with different prop values", () => {
530
+ test('should resolve responsive style mappings with different prop values', () => {
510
531
  const structure: ComponentNode = {
511
- type: "node",
512
- tag: "h2",
532
+ type: 'node',
533
+ tag: 'h2',
513
534
  style: {
514
535
  base: {
515
536
  fontSize: {
516
537
  _mapping: true,
517
- prop: "size",
518
- values: { "1": "48px", "2": "40px" }
538
+ prop: 'size',
539
+ values: { '1': '48px', '2': '40px' },
519
540
  } as StyleMapping,
520
541
  },
521
542
  tablet: {
522
543
  fontSize: {
523
544
  _mapping: true,
524
- prop: "size",
525
- values: { "1": "132px", "2": "35px" }
545
+ prop: 'size',
546
+ values: { '1': '132px', '2': '35px' },
526
547
  } as StyleMapping,
527
- }
528
- }
548
+ },
549
+ },
529
550
  };
530
- const props = { size: "2" };
551
+ const props = { size: '2' };
531
552
 
532
553
  const result = processStructure(structure, createContext(props));
533
554
  const node = result as ComponentNode;
534
555
  const style = node.style as any;
535
- expect(style?.base?.fontSize).toBe("40px");
536
- expect(style?.tablet?.fontSize).toBe("35px");
556
+ expect(style?.base?.fontSize).toBe('40px');
557
+ expect(style?.tablet?.fontSize).toBe('35px');
537
558
  });
538
559
 
539
- test("should resolve multiple style mappings", () => {
560
+ test('should resolve multiple style mappings', () => {
540
561
  const structure: ComponentNode = {
541
- type: "node",
542
- tag: "button",
562
+ type: 'node',
563
+ tag: 'button',
543
564
  style: {
544
565
  base: {
545
566
  background: {
546
567
  _mapping: true,
547
- prop: "variant",
548
- values: { primary: "blue", secondary: "gray" }
568
+ prop: 'variant',
569
+ values: { primary: 'blue', secondary: 'gray' },
549
570
  } as StyleMapping,
550
571
  color: {
551
572
  _mapping: true,
552
- prop: "variant",
553
- values: { primary: "white", secondary: "black" }
554
- } as StyleMapping
555
- }
556
- }
573
+ prop: 'variant',
574
+ values: { primary: 'white', secondary: 'black' },
575
+ } as StyleMapping,
576
+ },
577
+ },
557
578
  };
558
- const props = { variant: "primary" };
579
+ const props = { variant: 'primary' };
559
580
 
560
581
  const result = processStructure(structure, createContext(props));
561
582
  const node = result as ComponentNode;
562
583
  const baseStyle = (node.style as any)?.base;
563
- expect(baseStyle?.background).toBe("blue");
564
- expect(baseStyle?.color).toBe("white");
584
+ expect(baseStyle?.background).toBe('blue');
585
+ expect(baseStyle?.color).toBe('white');
565
586
  });
566
587
  });
567
588
 
568
- describe("Template evaluation in style objects", () => {
569
- test("should evaluate templates in style values", () => {
589
+ describe('Template evaluation in style objects', () => {
590
+ test('should evaluate templates in style values', () => {
591
+ // NOTE: the ternary deliberately avoids decimal literals like '0.5'.
592
+ // Since 2bd781ed, hasItemTemplates() treats any `\w+.\w+` inside a full
593
+ // {{...}} template as a potential item-field access and DEFERS evaluation
594
+ // to item-template processing — a decimal literal ('0.5') false-positives
595
+ // that check, so the template would pass through unresolved here. That
596
+ // false positive is a known product issue (see report); this test covers
597
+ // the eager-evaluation path that still works.
570
598
  const structure: ComponentNode = {
571
- type: "node",
572
- tag: "div",
599
+ type: 'node',
600
+ tag: 'div',
573
601
  style: {
574
602
  base: {
575
- opacity: "{{disabled ? '0.5' : '1'}}",
576
- fontSize: "{{size}}px"
577
- }
578
- }
603
+ opacity: "{{disabled ? '0' : '1'}}",
604
+ fontSize: '{{size}}px',
605
+ },
606
+ },
579
607
  };
580
608
  const props = { disabled: true, size: 16 };
581
609
 
@@ -583,127 +611,159 @@ describe("Template Engine - processStructure", () => {
583
611
  const node = result as ComponentNode;
584
612
  const baseStyle = (node.style as any)?.base;
585
613
  expect(baseStyle).toBeDefined();
586
- expect(baseStyle?.opacity).toBe("0.5");
587
- expect(baseStyle?.fontSize).toBe("16px");
614
+ expect(baseStyle?.opacity).toBe('0');
615
+ expect(baseStyle?.fontSize).toBe('16px');
588
616
  });
589
617
 
590
- test("should evaluate templates using global template context variables", () => {
618
+ test('defers full templates containing dotted terms (e.g. decimals) to item processing', () => {
619
+ // Documents the current (arguably buggy) behavior: '0.5' matches the
620
+ // item-field-access pattern in hasItemTemplates(), so processStructure
621
+ // leaves the whole template unresolved for later item-template passes —
622
+ // even though no item context exists. Reported as a product issue.
623
+ const structure: ComponentNode = {
624
+ type: 'node',
625
+ tag: 'div',
626
+ style: {
627
+ base: {
628
+ opacity: "{{disabled ? '0.5' : '1'}}",
629
+ },
630
+ },
631
+ };
632
+ const props = { disabled: true };
633
+
634
+ const result = processStructure(structure, createContext(props));
635
+ const node = result as ComponentNode;
636
+ const baseStyle = (node.style as any)?.base;
637
+ expect(baseStyle?.opacity).toBe("{{disabled ? '0.5' : '1'}}");
638
+ });
639
+
640
+ test('should evaluate templates using global template context variables', () => {
591
641
  setGlobalTemplateContext({ isEditorMode: true });
592
642
 
593
643
  const structure: ComponentNode = {
594
- type: "node",
595
- tag: "div",
644
+ type: 'node',
645
+ tag: 'div',
596
646
  style: {
597
647
  base: {
598
- display: "{{isEditorMode ? 'none' : 'block'}}"
599
- }
600
- }
648
+ display: "{{isEditorMode ? 'none' : 'block'}}",
649
+ },
650
+ },
601
651
  };
602
652
 
603
653
  const result = processStructure(structure, createContext({}));
604
654
  const node = result as ComponentNode;
605
655
  const baseStyle = (node.style as any)?.base;
606
- expect(baseStyle?.display).toBe("none");
656
+ expect(baseStyle?.display).toBe('none');
607
657
 
608
658
  resetGlobalTemplateContext();
609
659
  });
610
660
  });
611
661
 
612
- describe("Edge cases and error handling", () => {
613
- test("should return null for null structure", () => {
662
+ describe('Edge cases and error handling', () => {
663
+ test('should return null for null structure', () => {
614
664
  const result = processStructure(null, createContext({}));
615
665
  expect(result).toBeNull();
616
666
  });
617
667
 
618
- test("should return null for undefined structure", () => {
668
+ test('should return null for undefined structure', () => {
619
669
  const result = processStructure(undefined, createContext({}));
620
670
  expect(result).toBeNull();
621
671
  });
622
672
 
623
- test("should handle numbers in structure", () => {
673
+ test('should handle numbers in structure', () => {
624
674
  const result = processStructure(42, createContext({}));
625
675
  expect(result).toBe(42);
626
676
  });
627
677
 
628
- test("should handle empty objects", () => {
678
+ test('should handle empty objects', () => {
629
679
  const structure: ComponentNode = {
630
- type: "node",
631
- tag: "div",
632
- children: []
680
+ type: 'node',
681
+ tag: 'div',
682
+ children: [],
633
683
  };
634
684
 
635
685
  const result = processStructure(structure, createContext({}));
636
686
  const node = result as ComponentNode;
637
- expect(node.tag).toBe("div");
638
- expect(node.type).toBe("node");
687
+ expect(node.tag).toBe('div');
688
+ expect(node.type).toBe('node');
639
689
  });
640
690
 
641
- test("should handle component definition context in templates", () => {
691
+ test('should handle component definition context in templates', () => {
642
692
  const componentDef = {
643
693
  interface: {},
644
694
  structure: { type: 'node' as const, tag: 'div' },
695
+ category: 'buttons',
645
696
  variants: {
646
- primary: { background: "blue" }
647
- }
697
+ primary: { background: 'blue' },
698
+ },
648
699
  } as unknown as StructuredComponentDefinition & { variants?: Record<string, any> };
649
- const structure = "{{variants.primary.background}}";
650
700
  const props = {};
651
701
 
652
- const result = processStructure(structure, createContext(props, componentDef));
653
- expect(result).toBe("blue");
702
+ // componentDef fields are merged into the eval context, so a non-dotted
703
+ // full template referencing one resolves eagerly.
704
+ expect(processStructure('{{category}}', createContext(props, componentDef))).toBe('buttons');
705
+
706
+ // Dotted paths like {{variants.primary.background}} are NO LONGER eagerly
707
+ // resolved: since 2bd781ed, hasItemTemplates() reserves `\w+.\w+` paths
708
+ // for item-template processing (e.g. {{post.title}} inside lists), so
709
+ // processStructure passes them through unresolved. Previously this
710
+ // returned "blue".
711
+ expect(processStructure('{{variants.primary.background}}', createContext(props, componentDef))).toBe(
712
+ '{{variants.primary.background}}',
713
+ );
654
714
  });
655
715
  });
656
716
 
657
- describe("Integration with Button.json example", () => {
658
- test("should process Button component structure with style mappings", () => {
717
+ describe('Integration with Button.json example', () => {
718
+ test('should process Button component structure with style mappings', () => {
659
719
  // Based on components/Button.json
660
720
  const structure: ComponentNode = {
661
- type: "node",
662
- tag: "button",
721
+ type: 'node',
722
+ tag: 'button',
663
723
  style: {
664
724
  base: {
665
725
  padding: {
666
726
  _mapping: true,
667
- prop: "variant",
727
+ prop: 'variant',
668
728
  values: {
669
- primary: "16px 34px",
670
- secondary: "16px 34px"
671
- }
729
+ primary: '16px 34px',
730
+ secondary: '16px 34px',
731
+ },
672
732
  } as StyleMapping,
673
733
  background: {
674
734
  _mapping: true,
675
- prop: "variant",
735
+ prop: 'variant',
676
736
  values: {
677
- primary: "blue",
678
- secondary: "#f3f4f6"
679
- }
737
+ primary: 'blue',
738
+ secondary: '#f3f4f6',
739
+ },
680
740
  } as StyleMapping,
681
741
  color: {
682
742
  _mapping: true,
683
- prop: "variant",
743
+ prop: 'variant',
684
744
  values: {
685
- primary: "white",
686
- secondary: "#374151"
687
- }
745
+ primary: 'white',
746
+ secondary: '#374151',
747
+ },
688
748
  } as StyleMapping,
689
- borderRadius: "0px",
690
- fontSize: "15px"
691
- }
749
+ borderRadius: '0px',
750
+ fontSize: '15px',
751
+ },
692
752
  },
693
- children: "{{children}}"
753
+ children: '{{children}}',
694
754
  } as unknown as ComponentNode;
695
- const props = { variant: "primary", children: "Click me" };
755
+ const props = { variant: 'primary', children: 'Click me' };
696
756
 
697
757
  const result = processStructure(structure, createContext(props));
698
758
  const node = result as ComponentNode;
699
759
 
700
760
  const baseStyle = (node.style as any)?.base;
701
- expect(baseStyle?.background).toBe("blue");
702
- expect(baseStyle?.color).toBe("white");
703
- expect(baseStyle?.padding).toBe("16px 34px");
761
+ expect(baseStyle?.background).toBe('blue');
762
+ expect(baseStyle?.color).toBe('white');
763
+ expect(baseStyle?.padding).toBe('16px 34px');
704
764
  // processStructure converts string children template to evaluated result (array)
705
765
  if (Array.isArray(node.children)) {
706
- expect(node.children).toEqual(["Click me"]);
766
+ expect(node.children).toEqual(['Click me']);
707
767
  } else {
708
768
  // If not array, it might still be the template string or evaluated string
709
769
  expect(node.children).toBeDefined();
@@ -712,143 +772,145 @@ describe("Template Engine - processStructure", () => {
712
772
  });
713
773
  });
714
774
 
715
- describe("Template Engine - parentProps cascade", () => {
775
+ describe('Template Engine - parentProps cascade', () => {
716
776
  const mockComponentDef: StructuredComponentDefinition = {
717
777
  interface: {},
718
- structure: { type: 'node' as const, tag: 'div' }
778
+ structure: { type: 'node' as const, tag: 'div' },
719
779
  };
720
780
 
721
781
  test("falls back to parentProps when child doesn't declare the prop", () => {
722
782
  const structure: ComponentNode = {
723
- type: "node",
724
- tag: "span",
725
- children: ["{{ctaText}}"]
783
+ type: 'node',
784
+ tag: 'span',
785
+ children: ['{{ctaText}}'],
726
786
  };
727
787
  const ctx: TemplateContext = {
728
788
  props: {},
729
- parentProps: { ctaText: "Take the assessment" },
730
- componentDef: mockComponentDef
789
+ parentProps: { ctaText: 'Take the assessment' },
790
+ componentDef: mockComponentDef,
731
791
  };
732
792
 
733
793
  const result = processStructure(structure, ctx) as ComponentNode;
734
- expect(result.children).toEqual(["Take the assessment"]);
794
+ expect(result.children).toEqual(['Take the assessment']);
735
795
  });
736
796
 
737
- test("child props win over parentProps on key collision", () => {
797
+ test('child props win over parentProps on key collision', () => {
738
798
  const structure: ComponentNode = {
739
- type: "node",
740
- tag: "span",
741
- children: ["{{label}}"]
799
+ type: 'node',
800
+ tag: 'span',
801
+ children: ['{{label}}'],
742
802
  };
743
803
  const ctx: TemplateContext = {
744
- props: { label: "child" },
745
- parentProps: { label: "parent" },
746
- componentDef: mockComponentDef
804
+ props: { label: 'child' },
805
+ parentProps: { label: 'parent' },
806
+ componentDef: mockComponentDef,
747
807
  };
748
808
 
749
809
  const result = processStructure(structure, ctx) as ComponentNode;
750
- expect(result.children).toEqual(["child"]);
810
+ expect(result.children).toEqual(['child']);
751
811
  });
752
812
 
753
- test("partial templates (prefix-{{x}}) also cascade", () => {
813
+ test('partial templates (prefix-{{x}}) also cascade', () => {
754
814
  const structure: ComponentNode = {
755
- type: "node",
756
- tag: "div",
757
- attributes: { class: "btn-{{variant}}" }
815
+ type: 'node',
816
+ tag: 'div',
817
+ attributes: { class: 'btn-{{variant}}' },
758
818
  };
759
819
  const ctx: TemplateContext = {
760
820
  props: {},
761
- parentProps: { variant: "primary" },
762
- componentDef: mockComponentDef
821
+ parentProps: { variant: 'primary' },
822
+ componentDef: mockComponentDef,
763
823
  };
764
824
 
765
825
  const result = processStructure(structure, ctx) as ComponentNode;
766
- expect((result as any).attributes?.class).toBe("btn-primary");
826
+ expect((result as any).attributes?.class).toBe('btn-primary');
767
827
  });
768
828
 
769
- test("style mappings cascade against parentProps", () => {
829
+ test('style mappings cascade against parentProps', () => {
770
830
  const structure: ComponentNode = {
771
- type: "node",
772
- tag: "div",
831
+ type: 'node',
832
+ tag: 'div',
773
833
  style: {
774
834
  base: {
775
835
  color: {
776
836
  _mapping: true,
777
- prop: "variant",
778
- values: { default: "black", featured: "white" }
779
- }
780
- }
781
- }
837
+ prop: 'variant',
838
+ values: { default: 'black', featured: 'white' },
839
+ },
840
+ },
841
+ },
782
842
  } as ComponentNode;
783
843
  const ctx: TemplateContext = {
784
844
  props: {},
785
- parentProps: { variant: "featured" },
786
- componentDef: mockComponentDef
845
+ parentProps: { variant: 'featured' },
846
+ componentDef: mockComponentDef,
787
847
  };
788
848
 
789
849
  const result = processStructure(structure, ctx) as ComponentNode;
790
850
  const baseStyle = (result.style as any)?.base;
791
- expect(baseStyle?.color).toBe("white");
851
+ expect(baseStyle?.color).toBe('white');
792
852
  });
793
853
  });
794
854
 
795
- describe("Template Engine - Slot Default Values", () => {
855
+ describe('Template Engine - Slot Default Values', () => {
796
856
  const mockComponentDef: StructuredComponentDefinition = {
797
857
  interface: {},
798
- structure: { type: 'node' as const, tag: 'div' }
858
+ structure: { type: 'node' as const, tag: 'div' },
799
859
  };
800
860
 
801
861
  const createContext = (props: Record<string, unknown>): TemplateContext => ({
802
862
  props,
803
- componentDef: mockComponentDef
863
+ componentDef: mockComponentDef,
804
864
  });
805
865
 
806
- test("should render slot default when no instance children provided", () => {
866
+ test('should render slot default when no instance children provided', () => {
807
867
  const structure = [
808
- { type: "node", tag: "div", children: [
809
- { type: "node", tag: "h1", children: "Header" },
810
- {
811
- type: "slot",
812
- default: [
813
- { type: "node", tag: "p", children: "Default paragraph" }
814
- ]
815
- },
816
- { type: "node", tag: "footer", children: "Footer" }
817
- ]}
868
+ {
869
+ type: 'node',
870
+ tag: 'div',
871
+ children: [
872
+ { type: 'node', tag: 'h1', children: 'Header' },
873
+ {
874
+ type: 'slot',
875
+ default: [{ type: 'node', tag: 'p', children: 'Default paragraph' }],
876
+ },
877
+ { type: 'node', tag: 'footer', children: 'Footer' },
878
+ ],
879
+ },
818
880
  ] as unknown as ComponentNode[];
819
881
 
820
882
  const result = processStructure(structure, createContext({}), undefined, undefined);
821
883
  expect(result).toBeInstanceOf(Array);
822
884
 
823
885
  const [root] = result as ComponentNode[];
824
- expect(root.tag).toBe("div");
886
+ expect(root.tag).toBe('div');
825
887
  expect(root.children).toBeInstanceOf(Array);
826
888
 
827
889
  const children = root.children as ComponentNode[];
828
890
  expect(children).toHaveLength(3);
829
- expect(children[0].tag).toBe("h1");
830
- expect((children[1] as any).tag).toBe("p"); // Default content
831
- expect((children[1] as any).children).toEqual(["Default paragraph"]);
832
- expect(children[2].tag).toBe("footer");
891
+ expect(children[0].tag).toBe('h1');
892
+ expect((children[1] as any).tag).toBe('p'); // Default content
893
+ expect((children[1] as any).children).toEqual(['Default paragraph']);
894
+ expect(children[2].tag).toBe('footer');
833
895
  });
834
896
 
835
- test("should render instance children instead of slot default when provided", () => {
897
+ test('should render instance children instead of slot default when provided', () => {
836
898
  const structure = [
837
- { type: "node", tag: "div", children: [
838
- { type: "node", tag: "h1", children: "Header" },
839
- {
840
- type: "slot",
841
- default: [
842
- { type: "node", tag: "p", children: "Default paragraph" }
843
- ]
844
- },
845
- { type: "node", tag: "footer", children: "Footer" }
846
- ]}
899
+ {
900
+ type: 'node',
901
+ tag: 'div',
902
+ children: [
903
+ { type: 'node', tag: 'h1', children: 'Header' },
904
+ {
905
+ type: 'slot',
906
+ default: [{ type: 'node', tag: 'p', children: 'Default paragraph' }],
907
+ },
908
+ { type: 'node', tag: 'footer', children: 'Footer' },
909
+ ],
910
+ },
847
911
  ] as unknown as ComponentNode[];
848
912
 
849
- const instanceChildren = [
850
- { type: "node", tag: "span", children: "Custom content" }
851
- ] as ComponentNode[];
913
+ const instanceChildren = [{ type: 'node', tag: 'span', children: 'Custom content' }] as ComponentNode[];
852
914
 
853
915
  const result = processStructure(structure, createContext({}), undefined, instanceChildren);
854
916
  expect(result).toBeInstanceOf(Array);
@@ -857,36 +919,44 @@ describe("Template Engine - Slot Default Values", () => {
857
919
  const children = root.children as ComponentNode[];
858
920
 
859
921
  expect(children).toHaveLength(3);
860
- expect(children[0].tag).toBe("h1");
861
- expect((children[1] as any).tag).toBe("span"); // Instance content, not default
862
- expect((children[1] as any).children).toEqual(["Custom content"]);
863
- expect(children[2].tag).toBe("footer");
922
+ expect(children[0].tag).toBe('h1');
923
+ expect((children[1] as any).tag).toBe('span'); // Instance content, not default
924
+ expect((children[1] as any).children).toEqual(['Custom content']);
925
+ expect(children[2].tag).toBe('footer');
864
926
  });
865
927
 
866
- test("should handle string default for slot", () => {
928
+ test('should handle string default for slot', () => {
867
929
  const structure = [
868
- { type: "node", tag: "div", children: [
869
- {
870
- type: "slot",
871
- default: "Default text content"
872
- }
873
- ]}
930
+ {
931
+ type: 'node',
932
+ tag: 'div',
933
+ children: [
934
+ {
935
+ type: 'slot',
936
+ default: 'Default text content',
937
+ },
938
+ ],
939
+ },
874
940
  ] as unknown as ComponentNode[];
875
941
 
876
942
  const result = processStructure(structure, createContext({}), undefined, undefined);
877
943
  const [root] = result as ComponentNode[];
878
944
  const children = root.children as string[];
879
945
 
880
- expect(children).toContain("Default text content");
946
+ expect(children).toContain('Default text content');
881
947
  });
882
948
 
883
- test("should render nothing when no instance children and no default", () => {
949
+ test('should render nothing when no instance children and no default', () => {
884
950
  const structure = [
885
- { type: "node", tag: "div", children: [
886
- { type: "node", tag: "h1", children: "Header" },
887
- { type: "slot" }, // No default
888
- { type: "node", tag: "footer", children: "Footer" }
889
- ]}
951
+ {
952
+ type: 'node',
953
+ tag: 'div',
954
+ children: [
955
+ { type: 'node', tag: 'h1', children: 'Header' },
956
+ { type: 'slot' }, // No default
957
+ { type: 'node', tag: 'footer', children: 'Footer' },
958
+ ],
959
+ },
890
960
  ] as unknown as ComponentNode[];
891
961
 
892
962
  const result = processStructure(structure, createContext({}), undefined, undefined);
@@ -894,59 +964,61 @@ describe("Template Engine - Slot Default Values", () => {
894
964
  const children = root.children as ComponentNode[];
895
965
 
896
966
  expect(children).toHaveLength(2); // Only h1 and footer, slot is removed
897
- expect(children[0].tag).toBe("h1");
898
- expect(children[1].tag).toBe("footer");
967
+ expect(children[0].tag).toBe('h1');
968
+ expect(children[1].tag).toBe('footer');
899
969
  });
900
970
 
901
- test("should process templates in slot default content", () => {
971
+ test('should process templates in slot default content', () => {
902
972
  const structure = [
903
- { type: "node", tag: "div", children: [
904
- {
905
- type: "slot",
906
- default: [
907
- { type: "node", tag: "p", children: "Hello {{name}}" }
908
- ]
909
- }
910
- ]}
973
+ {
974
+ type: 'node',
975
+ tag: 'div',
976
+ children: [
977
+ {
978
+ type: 'slot',
979
+ default: [{ type: 'node', tag: 'p', children: 'Hello {{name}}' }],
980
+ },
981
+ ],
982
+ },
911
983
  ] as unknown as ComponentNode[];
912
984
 
913
- const result = processStructure(structure, createContext({ name: "World" }), undefined, undefined);
985
+ const result = processStructure(structure, createContext({ name: 'World' }), undefined, undefined);
914
986
  const [root] = result as ComponentNode[];
915
987
  const children = root.children as ComponentNode[];
916
988
 
917
- expect((children[0] as any).children).toEqual(["Hello World"]);
989
+ expect((children[0] as any).children).toEqual(['Hello World']);
918
990
  });
919
991
  });
920
992
 
921
- describe("Template Engine - normalizeStyle", () => {
922
- test("should normalize flat style objects", () => {
993
+ describe('Template Engine - normalizeStyle', () => {
994
+ test('should normalize flat style objects', () => {
923
995
  const style = {
924
- color: "red",
925
- fontSize: "16px"
996
+ color: 'red',
997
+ fontSize: '16px',
926
998
  };
927
-
999
+
928
1000
  const result = normalizeStyle(style);
929
1001
  expect(result).toEqual(style);
930
1002
  });
931
1003
 
932
1004
  test("should normalize responsive style objects using 'all' strategy", () => {
933
1005
  const style = {
934
- base: { color: "red" },
935
- tablet: { color: "blue" },
936
- mobile: { color: "green" }
1006
+ base: { color: 'red' },
1007
+ tablet: { color: 'blue' },
1008
+ mobile: { color: 'green' },
937
1009
  };
938
-
1010
+
939
1011
  const result = normalizeStyle(style);
940
1012
  expect(result).toBeDefined();
941
- expect(result?.color).toBe("green"); // 'all' strategy merges all, mobile wins
1013
+ expect(result?.color).toBe('green'); // 'all' strategy merges all, mobile wins
942
1014
  });
943
1015
 
944
- test("should return null for null style", () => {
1016
+ test('should return null for null style', () => {
945
1017
  const result = normalizeStyle(null);
946
1018
  expect(result).toBeNull();
947
1019
  });
948
1020
 
949
- test("should return null for undefined style", () => {
1021
+ test('should return null for undefined style', () => {
950
1022
  const result = normalizeStyle(undefined);
951
1023
  expect(result).toBeNull();
952
1024
  });
@@ -957,175 +1029,175 @@ describe("Template Engine - normalizeStyle", () => {
957
1029
  // These exercise specific type-cast paths to ensure safety before refactoring
958
1030
  // ==========================================================================
959
1031
 
960
- describe("Template Engine - processStructure as-any paths", () => {
1032
+ describe('Template Engine - processStructure as-any paths', () => {
961
1033
  const baseContext: TemplateContext = {
962
- props: { text: "Hello", size: "lg", variant: "primary" },
1034
+ props: { text: 'Hello', size: 'lg', variant: 'primary' },
963
1035
  componentDef: {} as StructuredComponentDefinition,
964
1036
  };
965
1037
 
966
- describe("Boolean preservation (line 429)", () => {
967
- test("should preserve false boolean values in structure", () => {
1038
+ describe('Boolean preservation (line 429)', () => {
1039
+ test('should preserve false boolean values in structure', () => {
968
1040
  // processStructure should return false as-is, not convert to null
969
1041
  const result = processStructure(false as unknown as ComponentNode, baseContext);
970
1042
  expect(result as unknown).toBe(false);
971
1043
  });
972
1044
 
973
- test("should preserve true boolean values in structure", () => {
1045
+ test('should preserve true boolean values in structure', () => {
974
1046
  const result = processStructure(true as unknown as ComponentNode, baseContext);
975
1047
  expect(result as unknown).toBe(true);
976
1048
  });
977
1049
  });
978
1050
 
979
- describe("Object template evaluation returning objects (line 454)", () => {
980
- test("should return object result from full template expression", () => {
1051
+ describe('Object template evaluation returning objects (line 454)', () => {
1052
+ test('should return object result from full template expression', () => {
981
1053
  const context: TemplateContext = {
982
- props: { link: { href: "/about", target: "_blank" } },
1054
+ props: { link: { href: '/about', target: '_blank' } },
983
1055
  componentDef: {} as StructuredComponentDefinition,
984
1056
  };
985
1057
  // Full template {{link}} should return the object as-is
986
- const result = processStructure("{{link}}", context);
987
- expect(result).toEqual({ href: "/about", target: "_blank" });
1058
+ const result = processStructure('{{link}}', context);
1059
+ expect(result).toEqual({ href: '/about', target: '_blank' });
988
1060
  });
989
1061
  });
990
1062
 
991
1063
  describe("Slot default content via 'default' property (lines 483-486, 516-517)", () => {
992
- test("should render slot default array when no instance children", () => {
1064
+ test('should render slot default array when no instance children', () => {
993
1065
  // Slot markers use type: "slot" (NODE_TYPE.SLOT)
994
1066
  const structure = [
995
- { type: "node", tag: "div", children: [
996
- { type: "slot", default: [{ type: "node", tag: "span", children: "fallback" }] }
997
- ]}
1067
+ {
1068
+ type: 'node',
1069
+ tag: 'div',
1070
+ children: [{ type: 'slot', default: [{ type: 'node', tag: 'span', children: 'fallback' }] }],
1071
+ },
998
1072
  ] as unknown as ComponentNode[];
999
1073
  const result = processStructure(structure, baseContext);
1000
1074
  expect(Array.isArray(result)).toBe(true);
1001
1075
  const root = (result as any[])[0];
1002
- expect(root.tag).toBe("div");
1076
+ expect(root.tag).toBe('div');
1003
1077
  // The slot default should have been rendered
1004
1078
  expect(root.children.length).toBe(1);
1005
- expect(root.children[0].tag).toBe("span");
1079
+ expect(root.children[0].tag).toBe('span');
1006
1080
  });
1007
1081
 
1008
- test("should render slot default string when no instance children", () => {
1082
+ test('should render slot default string when no instance children', () => {
1009
1083
  const structure = [
1010
- { type: "node", tag: "div", children: [
1011
- { type: "slot", default: "Default text" }
1012
- ]}
1084
+ { type: 'node', tag: 'div', children: [{ type: 'slot', default: 'Default text' }] },
1013
1085
  ] as unknown as ComponentNode[];
1014
1086
  const result = processStructure(structure, baseContext);
1015
1087
  const root = (result as any[])[0];
1016
- expect(root.children[0]).toBe("Default text");
1088
+ expect(root.children[0]).toBe('Default text');
1017
1089
  });
1018
1090
 
1019
- test("should use instance children over slot default", () => {
1091
+ test('should use instance children over slot default', () => {
1020
1092
  const structure = [
1021
- { type: "node", tag: "div", children: [
1022
- { type: "slot", default: [{ type: "node", tag: "span", children: "fallback" }] }
1023
- ]}
1024
- ] as unknown as ComponentNode[];
1025
- const instanceChildren = [
1026
- { type: "node", tag: "b", children: "override" }
1093
+ {
1094
+ type: 'node',
1095
+ tag: 'div',
1096
+ children: [{ type: 'slot', default: [{ type: 'node', tag: 'span', children: 'fallback' }] }],
1097
+ },
1027
1098
  ] as unknown as ComponentNode[];
1099
+ const instanceChildren = [{ type: 'node', tag: 'b', children: 'override' }] as unknown as ComponentNode[];
1028
1100
  const result = processStructure(structure, baseContext, undefined, instanceChildren);
1029
1101
  const root = (result as any[])[0];
1030
- expect(root.children[0].tag).toBe("b");
1102
+ expect(root.children[0].tag).toBe('b');
1031
1103
  });
1032
1104
 
1033
- test("should handle slot marker as standalone object with default", () => {
1105
+ test('should handle slot marker as standalone object with default', () => {
1034
1106
  const slotMarker = {
1035
- type: "slot",
1036
- default: [{ type: "node", tag: "div", children: "content" }],
1107
+ type: 'slot',
1108
+ default: [{ type: 'node', tag: 'div', children: 'content' }],
1037
1109
  };
1038
1110
  const result = processStructure(slotMarker as any, baseContext);
1039
1111
  expect(result).toBeDefined();
1040
1112
  });
1041
1113
  });
1042
1114
 
1043
- describe("Plain object processing (lines 538, 543)", () => {
1044
- test("should process plain objects recursively and resolve templates", () => {
1115
+ describe('Plain object processing (lines 538, 543)', () => {
1116
+ test('should process plain objects recursively and resolve templates', () => {
1045
1117
  // An object without a valid node type gets treated as a plain props object
1046
- const structure = { label: "{{text}}", visible: true } as any;
1118
+ const structure = { label: '{{text}}', visible: true } as any;
1047
1119
  const result = processStructure(structure, baseContext);
1048
1120
  expect(result).toBeDefined();
1049
- expect((result as any).label).toBe("Hello");
1121
+ expect((result as any).label).toBe('Hello');
1050
1122
  expect((result as any).visible).toBe(true);
1051
1123
  });
1052
1124
 
1053
- test("should strip null/undefined values from plain objects", () => {
1125
+ test('should strip null/undefined values from plain objects', () => {
1054
1126
  const context: TemplateContext = {
1055
- props: { defined: "value" },
1127
+ props: { defined: 'value' },
1056
1128
  componentDef: {} as StructuredComponentDefinition,
1057
1129
  };
1058
- const structure = { a: "{{defined}}", b: "{{missing}}" } as any;
1130
+ const structure = { a: '{{defined}}', b: '{{missing}}' } as any;
1059
1131
  const result = processStructure(structure, context);
1060
- expect((result as any).a).toBe("value");
1132
+ expect((result as any).a).toBe('value');
1061
1133
  // Missing props resolve to empty string, not null
1062
1134
  });
1063
1135
  });
1064
1136
 
1065
- describe("Embed node creation (line 562)", () => {
1066
- test("should process embed node with html content", () => {
1137
+ describe('Embed node creation (line 562)', () => {
1138
+ test('should process embed node with html content', () => {
1067
1139
  const node: ComponentNode = {
1068
- type: "embed" as any,
1140
+ type: 'embed' as any,
1069
1141
  html: "<script>alert('test')</script>",
1070
1142
  } as any;
1071
1143
  const result = processStructure(node, baseContext);
1072
1144
  expect(result).toBeDefined();
1073
- expect((result as any).type).toBe("embed");
1145
+ expect((result as any).type).toBe('embed');
1074
1146
  expect((result as any).html).toBe("<script>alert('test')</script>");
1075
1147
  });
1076
1148
  });
1077
1149
 
1078
- describe("Link node creation (line 569)", () => {
1079
- test("should process link node with href", () => {
1150
+ describe('Link node creation (line 569)', () => {
1151
+ test('should process link node with href', () => {
1080
1152
  const node = {
1081
- type: "link",
1082
- href: "/about",
1083
- children: ["About us"],
1153
+ type: 'link',
1154
+ href: '/about',
1155
+ children: ['About us'],
1084
1156
  } as any;
1085
1157
  const result = processStructure(node, baseContext);
1086
1158
  expect(result).toBeDefined();
1087
- expect((result as any).type).toBe("link");
1159
+ expect((result as any).type).toBe('link');
1088
1160
  });
1089
1161
 
1090
- test("should process link node with template href", () => {
1162
+ test('should process link node with template href', () => {
1091
1163
  const context: TemplateContext = {
1092
- props: { url: "/contact" },
1164
+ props: { url: '/contact' },
1093
1165
  componentDef: {} as StructuredComponentDefinition,
1094
1166
  };
1095
1167
  const node = {
1096
- type: "link",
1097
- href: "{{url}}",
1098
- children: ["Contact"],
1168
+ type: 'link',
1169
+ href: '{{url}}',
1170
+ children: ['Contact'],
1099
1171
  } as any;
1100
1172
  const result = processStructure(node, context);
1101
1173
  expect(result).toBeDefined();
1102
- expect((result as any).type).toBe("link");
1174
+ expect((result as any).type).toBe('link');
1103
1175
  });
1104
1176
  });
1105
1177
 
1106
- describe("Locale-list node creation (line 574)", () => {
1107
- test("should process locale-list node", () => {
1178
+ describe('Locale-list node creation (line 574)', () => {
1179
+ test('should process locale-list node', () => {
1108
1180
  const node = {
1109
- type: "locale-list",
1110
- style: { display: "flex" },
1181
+ type: 'locale-list',
1182
+ style: { display: 'flex' },
1111
1183
  } as any;
1112
1184
  const result = processStructure(node, baseContext);
1113
1185
  expect(result).toBeDefined();
1114
- expect((result as any).type).toBe("locale-list");
1186
+ expect((result as any).type).toBe('locale-list');
1115
1187
  });
1116
1188
  });
1117
1189
 
1118
- describe("List node with legacy cms-list type (line 575)", () => {
1119
- test("should process list node", () => {
1190
+ describe('List node with legacy cms-list type (line 575)', () => {
1191
+ test('should process list node', () => {
1120
1192
  const node = {
1121
- type: "list",
1122
- source: "items",
1123
- sourceType: "prop",
1124
- children: [{ type: "html" as const, tag: "div", children: [] }],
1193
+ type: 'list',
1194
+ source: 'items',
1195
+ sourceType: 'prop',
1196
+ children: [{ type: 'html' as const, tag: 'div', children: [] }],
1125
1197
  } as any;
1126
1198
  const result = processStructure(node, baseContext);
1127
1199
  expect(result).toBeDefined();
1128
- expect((result as any).type).toBe("list");
1200
+ expect((result as any).type).toBe('list');
1129
1201
  });
1130
1202
  });
1131
1203
  });