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
@@ -0,0 +1,81 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import { applyCmsFilters, applyCmsSorting, matchCmsCondition, isCmsFilterCondition } from './cmsQuery';
3
+
4
+ describe('cmsQuery — filters', () => {
5
+ const items = [
6
+ { _id: 'a', title: 'Alpha', views: 10, featured: true },
7
+ { _id: 'b', title: 'Beta', views: 30, featured: false },
8
+ { _id: 'c', title: 'Gamma', views: 20, featured: true },
9
+ ];
10
+
11
+ test('plain equality map filters on every key', () => {
12
+ expect(applyCmsFilters(items, { featured: true }).map((i) => i._id)).toEqual(['a', 'c']);
13
+ });
14
+
15
+ test('operators eq/neq/gt/gte/lt/lte/contains/in', () => {
16
+ expect(applyCmsFilters(items, { field: 'views', operator: 'gt', value: 15 }).map((i) => i._id)).toEqual(['b', 'c']);
17
+ expect(applyCmsFilters(items, { field: 'views', operator: 'lte', value: 20 }).map((i) => i._id)).toEqual([
18
+ 'a',
19
+ 'c',
20
+ ]);
21
+ expect(applyCmsFilters(items, { field: 'title', operator: 'contains', value: 'amm' }).map((i) => i._id)).toEqual([
22
+ 'c',
23
+ ]);
24
+ expect(applyCmsFilters(items, { field: '_id', operator: 'in', value: ['a', 'b'] }).map((i) => i._id)).toEqual([
25
+ 'a',
26
+ 'b',
27
+ ]);
28
+ });
29
+
30
+ test('array of conditions ANDs', () => {
31
+ const r = applyCmsFilters(items, [
32
+ { field: 'featured', operator: 'eq', value: true },
33
+ { field: 'views', operator: 'gte', value: 20 },
34
+ ]);
35
+ expect(r.map((i) => i._id)).toEqual(['c']);
36
+ });
37
+
38
+ test('unknown operator never matches; isCmsFilterCondition discriminates', () => {
39
+ expect(matchCmsCondition(items[0]!, { field: 'x', operator: 'weird' as never, value: 1 })).toBe(false);
40
+ expect(isCmsFilterCondition({ field: 'x' })).toBe(true);
41
+ expect(isCmsFilterCondition({ featured: true })).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('cmsQuery — sorting', () => {
46
+ test('numeric asc/desc, does not mutate input', () => {
47
+ const items = [{ n: 3 }, { n: 1 }, { n: 2 }];
48
+ expect(applyCmsSorting(items, { field: 'n', order: 'asc' }).map((i) => i.n)).toEqual([1, 2, 3]);
49
+ expect(applyCmsSorting(items, { field: 'n', order: 'desc' }).map((i) => i.n)).toEqual([3, 2, 1]);
50
+ expect(items.map((i) => i.n)).toEqual([3, 1, 2]); // original untouched
51
+ });
52
+
53
+ // Regression for the SSR-remote drift: the copy there had dropped this case, so a
54
+ // boolean field sorted differently for remote/Sanity items than for local ones.
55
+ test('boolean fields: desc → true first, asc → false first', () => {
56
+ const items = [
57
+ { id: 'x', on: false },
58
+ { id: 'y', on: true },
59
+ { id: 'z', on: false },
60
+ ];
61
+ expect(applyCmsSorting(items, { field: 'on', order: 'desc' })[0]!.id).toBe('y');
62
+ expect(applyCmsSorting(items, { field: 'on', order: 'asc' }).at(-1)!.id).toBe('y');
63
+ });
64
+
65
+ test('multi-key sort: primary then tiebreaker', () => {
66
+ const items = [
67
+ { cat: 'b', n: 1 },
68
+ { cat: 'a', n: 2 },
69
+ { cat: 'a', n: 1 },
70
+ ];
71
+ const r = applyCmsSorting(items, [
72
+ { field: 'cat', order: 'asc' },
73
+ { field: 'n', order: 'desc' },
74
+ ]);
75
+ expect(r).toEqual([
76
+ { cat: 'a', n: 2 },
77
+ { cat: 'a', n: 1 },
78
+ { cat: 'b', n: 1 },
79
+ ]);
80
+ });
81
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * CMS query primitives — the SINGLE source of truth for filtering and sorting
3
+ * CMS items. Pure (no IO), so it is shared by every consumer that has to apply a
4
+ * `CMSListQuery`'s `filter`/`sort` to a set of items:
5
+ * - `meno-core` CMSService (local file-backed collections)
6
+ * - `meno-core` SSR renderer (remote / Sanity-backed list items)
7
+ * - `meno-astro` runtime `getCollectionList` (built/SSR Astro pages)
8
+ *
9
+ * These used to be three hand-copied implementations; one (the SSR remote path)
10
+ * had already drifted and dropped the boolean-sort special case, so a boolean
11
+ * field sorted differently for remote items than for local ones. Keeping the
12
+ * logic here — operating on plain `Record<string, unknown>` rows so it works for
13
+ * both `CMSItem` and the astro runtime's item shape — prevents that recurring.
14
+ */
15
+
16
+ import type { CMSFilterCondition, CMSSortConfig } from './types/cms';
17
+
18
+ /** A CMS item, structurally — query primitives only ever index it by field name. */
19
+ type Row = Record<string, unknown>;
20
+
21
+ /** True when `obj` is a single {@link CMSFilterCondition} (vs a plain equality map). */
22
+ export function isCmsFilterCondition(obj: unknown): obj is CMSFilterCondition {
23
+ return typeof obj === 'object' && obj !== null && 'field' in obj;
24
+ }
25
+
26
+ /** Evaluate one filter condition against an item. Unknown operators never match. */
27
+ export function matchCmsCondition(item: Row, condition: CMSFilterCondition): boolean {
28
+ const value = item[condition.field];
29
+ switch (condition.operator || 'eq') {
30
+ case 'eq':
31
+ return value === condition.value;
32
+ case 'neq':
33
+ return value !== condition.value;
34
+ case 'gt':
35
+ return (value as number) > (condition.value as number);
36
+ case 'gte':
37
+ return (value as number) >= (condition.value as number);
38
+ case 'lt':
39
+ return (value as number) < (condition.value as number);
40
+ case 'lte':
41
+ return (value as number) <= (condition.value as number);
42
+ case 'contains':
43
+ return String(value).includes(String(condition.value));
44
+ case 'in':
45
+ return Array.isArray(condition.value) && condition.value.includes(value);
46
+ default:
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Filter items by a `CMSListQuery.filter`: a plain equality map (`{ featured: true }`
53
+ * → every key must match), a single condition, or an array of conditions (AND).
54
+ */
55
+ export function applyCmsFilters<T extends Row>(
56
+ items: T[],
57
+ filter: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>,
58
+ ): T[] {
59
+ if (!Array.isArray(filter) && !isCmsFilterCondition(filter)) {
60
+ const entries = Object.entries(filter);
61
+ return items.filter((item) => entries.every(([key, value]) => item[key] === value));
62
+ }
63
+ const conditions = Array.isArray(filter) ? filter : [filter as CMSFilterCondition];
64
+ return items.filter((item) => conditions.every((cond) => matchCmsCondition(item, cond)));
65
+ }
66
+
67
+ /**
68
+ * Stable multi-key sort. Booleans compare specially (desc → true first); other
69
+ * values compare as primitives. Returns a new array (input is not mutated).
70
+ */
71
+ export function applyCmsSorting<T extends Row>(items: T[], sort: CMSSortConfig | CMSSortConfig[]): T[] {
72
+ const sorts = Array.isArray(sort) ? sort : [sort];
73
+ return [...items].sort((a, b) => {
74
+ for (const s of sorts) {
75
+ const aVal = a[s.field];
76
+ const bVal = b[s.field];
77
+ const isDesc = s.order === 'desc';
78
+
79
+ // Boolean fields: desc puts `true` first, asc puts `false` first.
80
+ if (typeof aVal === 'boolean' && typeof bVal === 'boolean') {
81
+ if (aVal === bVal) continue;
82
+ return isDesc ? (aVal ? -1 : 1) : aVal ? 1 : -1;
83
+ }
84
+
85
+ let result = 0;
86
+ if ((aVal as string | number) < (bVal as string | number)) result = -1;
87
+ else if ((aVal as string | number) > (bVal as string | number)) result = 1;
88
+ if (result !== 0) return isDesc ? -result : result;
89
+ }
90
+ return 0;
91
+ });
92
+ }
@@ -7,7 +7,7 @@ import {
7
7
  parseSortExpression,
8
8
  serializeSortExpression,
9
9
  parseFilterExpression,
10
- serializeFilterExpression
10
+ serializeFilterExpression,
11
11
  } from './cmsQueryParser';
12
12
 
13
13
  describe('parseSortExpression', () => {
@@ -29,11 +29,11 @@ describe('parseSortExpression', () => {
29
29
  test('parses multiple fields', () => {
30
30
  expect(parseSortExpression('date desc, title')).toEqual([
31
31
  { field: 'date', order: 'desc' },
32
- { field: 'title', order: 'asc' }
32
+ { field: 'title', order: 'asc' },
33
33
  ]);
34
34
  expect(parseSortExpression('date desc, title asc')).toEqual([
35
35
  { field: 'date', order: 'desc' },
36
- { field: 'title', order: 'asc' }
36
+ { field: 'title', order: 'asc' },
37
37
  ]);
38
38
  });
39
39
 
@@ -41,7 +41,7 @@ describe('parseSortExpression', () => {
41
41
  expect(parseSortExpression(' date desc ')).toEqual({ field: 'date', order: 'desc' });
42
42
  expect(parseSortExpression('date desc, title asc')).toEqual([
43
43
  { field: 'date', order: 'desc' },
44
- { field: 'title', order: 'asc' }
44
+ { field: 'title', order: 'asc' },
45
45
  ]);
46
46
  });
47
47
  });
@@ -60,10 +60,12 @@ describe('serializeSortExpression', () => {
60
60
  });
61
61
 
62
62
  test('serializes multiple fields', () => {
63
- expect(serializeSortExpression([
64
- { field: 'date', order: 'desc' },
65
- { field: 'title', order: 'asc' }
66
- ])).toBe('date desc, title');
63
+ expect(
64
+ serializeSortExpression([
65
+ { field: 'date', order: 'desc' },
66
+ { field: 'title', order: 'asc' },
67
+ ]),
68
+ ).toBe('date desc, title');
67
69
  });
68
70
  });
69
71
 
@@ -98,32 +100,32 @@ describe('parseFilterExpression', () => {
98
100
  expect(parseFilterExpression('price > 100')).toEqual({
99
101
  field: 'price',
100
102
  operator: 'gt',
101
- value: 100
103
+ value: 100,
102
104
  });
103
105
  expect(parseFilterExpression('price >= 100')).toEqual({
104
106
  field: 'price',
105
107
  operator: 'gte',
106
- value: 100
108
+ value: 100,
107
109
  });
108
110
  expect(parseFilterExpression('price < 100')).toEqual({
109
111
  field: 'price',
110
112
  operator: 'lt',
111
- value: 100
113
+ value: 100,
112
114
  });
113
115
  expect(parseFilterExpression('price <= 100')).toEqual({
114
116
  field: 'price',
115
117
  operator: 'lte',
116
- value: 100
118
+ value: 100,
117
119
  });
118
120
  expect(parseFilterExpression('status != draft')).toEqual({
119
121
  field: 'status',
120
122
  operator: 'neq',
121
- value: 'draft'
123
+ value: 'draft',
122
124
  });
123
125
  expect(parseFilterExpression('status <> draft')).toEqual({
124
126
  field: 'status',
125
127
  operator: 'neq',
126
- value: 'draft'
128
+ value: 'draft',
127
129
  });
128
130
  });
129
131
 
@@ -131,12 +133,12 @@ describe('parseFilterExpression', () => {
131
133
  expect(parseFilterExpression('title contains hello')).toEqual({
132
134
  field: 'title',
133
135
  operator: 'contains',
134
- value: 'hello'
136
+ value: 'hello',
135
137
  });
136
138
  expect(parseFilterExpression('title ~ hello')).toEqual({
137
139
  field: 'title',
138
140
  operator: 'contains',
139
- value: 'hello'
141
+ value: 'hello',
140
142
  });
141
143
  });
142
144
 
@@ -144,35 +146,35 @@ describe('parseFilterExpression', () => {
144
146
  expect(parseFilterExpression('category in news, blog')).toEqual({
145
147
  field: 'category',
146
148
  operator: 'in',
147
- value: ['news', 'blog']
149
+ value: ['news', 'blog'],
148
150
  });
149
151
  expect(parseFilterExpression('status in draft, published, archived')).toEqual({
150
152
  field: 'status',
151
153
  operator: 'in',
152
- value: ['draft', 'published', 'archived']
154
+ value: ['draft', 'published', 'archived'],
153
155
  });
154
156
  });
155
157
 
156
158
  test('parses template values', () => {
157
159
  expect(parseFilterExpression('category = {{cms.category}}')).toEqual({
158
- category: '{{cms.category}}'
160
+ category: '{{cms.category}}',
159
161
  });
160
162
  expect(parseFilterExpression('author = {{cms.authorId}}')).toEqual({
161
- author: '{{cms.authorId}}'
163
+ author: '{{cms.authorId}}',
162
164
  });
163
165
  });
164
166
 
165
167
  test('parses multiple conditions (newline)', () => {
166
168
  expect(parseFilterExpression('featured = true\nprice > 100')).toEqual([
167
169
  { field: 'featured', value: true },
168
- { field: 'price', operator: 'gt', value: 100 }
170
+ { field: 'price', operator: 'gt', value: 100 },
169
171
  ]);
170
172
  });
171
173
 
172
174
  test('parses multiple conditions (semicolon)', () => {
173
175
  expect(parseFilterExpression('featured = true; price > 100')).toEqual([
174
176
  { field: 'featured', value: true },
175
- { field: 'price', operator: 'gt', value: 100 }
177
+ { field: 'price', operator: 'gt', value: 100 },
176
178
  ]);
177
179
  });
178
180
 
@@ -198,20 +200,20 @@ describe('parseFilterExpression', () => {
198
200
  expect(parseFilterExpression('price gt 100')).toEqual({
199
201
  field: 'price',
200
202
  operator: 'gt',
201
- value: 100
203
+ value: 100,
202
204
  });
203
205
  expect(parseFilterExpression('price gte 100')).toEqual({
204
206
  field: 'price',
205
207
  operator: 'gte',
206
- value: 100
208
+ value: 100,
207
209
  });
208
210
  expect(parseFilterExpression('status eq active')).toEqual({
209
- status: 'active'
211
+ status: 'active',
210
212
  });
211
213
  expect(parseFilterExpression('status neq draft')).toEqual({
212
214
  field: 'status',
213
215
  operator: 'neq',
214
- value: 'draft'
216
+ value: 'draft',
215
217
  });
216
218
  });
217
219
  });
@@ -227,32 +229,38 @@ describe('serializeFilterExpression', () => {
227
229
  });
228
230
 
229
231
  test('serializes condition with operator', () => {
230
- expect(serializeFilterExpression({
231
- field: 'price',
232
- operator: 'gt',
233
- value: 100
234
- })).toBe('price > 100');
235
- expect(serializeFilterExpression({
236
- field: 'status',
237
- operator: 'neq',
238
- value: 'draft'
239
- })).toBe('status != draft');
232
+ expect(
233
+ serializeFilterExpression({
234
+ field: 'price',
235
+ operator: 'gt',
236
+ value: 100,
237
+ }),
238
+ ).toBe('price > 100');
239
+ expect(
240
+ serializeFilterExpression({
241
+ field: 'status',
242
+ operator: 'neq',
243
+ value: 'draft',
244
+ }),
245
+ ).toBe('status != draft');
240
246
  });
241
247
 
242
248
  test('serializes multiple conditions', () => {
243
249
  const result = serializeFilterExpression([
244
250
  { field: 'featured', value: true },
245
- { field: 'price', operator: 'gt', value: 100 }
251
+ { field: 'price', operator: 'gt', value: 100 },
246
252
  ]);
247
253
  expect(result).toBe('featured = true\nprice > 100');
248
254
  });
249
255
 
250
256
  test('serializes in operator', () => {
251
- expect(serializeFilterExpression({
252
- field: 'category',
253
- operator: 'in',
254
- value: ['news', 'blog']
255
- })).toBe('category in news, blog');
257
+ expect(
258
+ serializeFilterExpression({
259
+ field: 'category',
260
+ operator: 'in',
261
+ value: ['news', 'blog'],
262
+ }),
263
+ ).toBe('category in news, blog');
256
264
  });
257
265
 
258
266
  test('serializes template values', () => {
@@ -22,15 +22,18 @@ import type { CMSFilterCondition, CMSSortConfig, CMSFilterOperator } from './typ
22
22
  export function parseSortExpression(expr: string): CMSSortConfig | CMSSortConfig[] | undefined {
23
23
  if (!expr.trim()) return undefined;
24
24
 
25
- const parts = expr.split(',').map(p => p.trim()).filter(Boolean);
26
- const sorts: CMSSortConfig[] = parts.map(part => {
25
+ const parts = expr
26
+ .split(',')
27
+ .map((p) => p.trim())
28
+ .filter(Boolean);
29
+ const sorts: CMSSortConfig[] = parts.map((part) => {
27
30
  const tokens = part.split(/\s+/);
28
- const field = tokens[0];
31
+ const field = tokens[0] ?? '';
29
32
  const order = tokens[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc';
30
33
  return { field, order };
31
34
  });
32
35
 
33
- return sorts.length === 1 ? sorts[0] : sorts;
36
+ return sorts.length === 1 ? (sorts[0] as CMSSortConfig) : sorts;
34
37
  }
35
38
 
36
39
  /**
@@ -39,19 +42,22 @@ export function parseSortExpression(expr: string): CMSSortConfig | CMSSortConfig
39
42
  export function serializeSortExpression(sort: CMSSortConfig | CMSSortConfig[] | undefined): string {
40
43
  if (!sort) return '';
41
44
  const sorts = Array.isArray(sort) ? sort : [sort];
42
- return sorts.map(s => `${s.field}${s.order === 'desc' ? ' desc' : ''}`).join(', ');
45
+ return sorts.map((s) => `${s.field}${s.order === 'desc' ? ' desc' : ''}`).join(', ');
43
46
  }
44
47
 
45
48
  /**
46
49
  * Parse filter expression: "featured = true; price > 100"
47
50
  */
48
51
  export function parseFilterExpression(
49
- expr: string
52
+ expr: string,
50
53
  ): CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined {
51
54
  if (!expr.trim()) return undefined;
52
55
 
53
56
  // Split by newline or semicolon
54
- const lines = expr.split(/[;\n]/).map(l => l.trim()).filter(Boolean);
57
+ const lines = expr
58
+ .split(/[;\n]/)
59
+ .map((l) => l.trim())
60
+ .filter(Boolean);
55
61
  const conditions: CMSFilterCondition[] = [];
56
62
 
57
63
  for (const line of lines) {
@@ -62,11 +68,12 @@ export function parseFilterExpression(
62
68
  if (conditions.length === 0) return undefined;
63
69
 
64
70
  // Single simple equality can use shorthand: { field: value }
65
- if (conditions.length === 1 && (!conditions[0].operator || conditions[0].operator === 'eq')) {
66
- return { [conditions[0].field]: conditions[0].value };
71
+ const first = conditions[0];
72
+ if (conditions.length === 1 && first && (!first.operator || first.operator === 'eq')) {
73
+ return { [first.field]: first.value };
67
74
  }
68
75
 
69
- return conditions.length === 1 ? conditions[0] : conditions;
76
+ return conditions.length === 1 && first ? first : conditions;
70
77
  }
71
78
 
72
79
  function parseFilterLine(line: string): CMSFilterCondition | null {
@@ -75,7 +82,10 @@ function parseFilterLine(line: string): CMSFilterCondition | null {
75
82
  const match = line.match(/^(\w+)\s*(==|!=|<>|>=|<=|=|>|<|contains|~|in|gte|lte|neq|gt|lt|eq)\s*(.+)$/i);
76
83
  if (!match) return null;
77
84
 
78
- const [, field, op, rawValue] = match;
85
+ // The regex matched (guarded above), so groups 1–3 are present; default for the type system.
86
+ const field = match[1] ?? '';
87
+ const op = match[2] ?? '';
88
+ const rawValue = match[3] ?? '';
79
89
  const operator = normalizeOperator(op);
80
90
  const value = parseValue(rawValue.trim(), operator);
81
91
 
@@ -90,14 +100,23 @@ function parseFilterLine(line: string): CMSFilterCondition | null {
90
100
  function normalizeOperator(op: string): CMSFilterOperator {
91
101
  const normalized = op.toLowerCase();
92
102
  const map: Record<string, CMSFilterOperator> = {
93
- '=': 'eq', '==': 'eq', 'eq': 'eq',
94
- '!=': 'neq', '<>': 'neq', 'neq': 'neq',
95
- '>': 'gt', 'gt': 'gt',
96
- '>=': 'gte', 'gte': 'gte',
97
- '<': 'lt', 'lt': 'lt',
98
- '<=': 'lte', 'lte': 'lte',
99
- 'contains': 'contains', '~': 'contains',
100
- 'in': 'in'
103
+ '=': 'eq',
104
+ '==': 'eq',
105
+ eq: 'eq',
106
+ '!=': 'neq',
107
+ '<>': 'neq',
108
+ neq: 'neq',
109
+ '>': 'gt',
110
+ gt: 'gt',
111
+ '>=': 'gte',
112
+ gte: 'gte',
113
+ '<': 'lt',
114
+ lt: 'lt',
115
+ '<=': 'lte',
116
+ lte: 'lte',
117
+ contains: 'contains',
118
+ '~': 'contains',
119
+ in: 'in',
101
120
  };
102
121
  return map[normalized] || 'eq';
103
122
  }
@@ -110,7 +129,7 @@ function parseValue(raw: string, operator: CMSFilterOperator): unknown {
110
129
 
111
130
  // Handle 'in' operator - comma separated list
112
131
  if (operator === 'in') {
113
- return raw.split(',').map(v => parseScalarValue(v.trim()));
132
+ return raw.split(',').map((v) => parseScalarValue(v.trim()));
114
133
  }
115
134
 
116
135
  return parseScalarValue(raw);
@@ -123,7 +142,7 @@ function parseScalarValue(raw: string): unknown {
123
142
 
124
143
  // Number
125
144
  const num = Number(raw);
126
- if (!isNaN(num) && raw !== '') return num;
145
+ if (!Number.isNaN(num) && raw !== '') return num;
127
146
 
128
147
  // String (strip quotes if present)
129
148
  if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
@@ -137,7 +156,7 @@ function parseScalarValue(raw: string): unknown {
137
156
  * Serialize filter config back to expression
138
157
  */
139
158
  export function serializeFilterExpression(
140
- filter: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined
159
+ filter: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined,
141
160
  ): string {
142
161
  if (!filter) return '';
143
162
 
@@ -150,26 +169,29 @@ export function serializeFilterExpression(
150
169
 
151
170
  // Array or single condition
152
171
  const conditions = Array.isArray(filter) ? filter : [filter as CMSFilterCondition];
153
- return conditions.map(c => {
154
- const op = c.operator || 'eq';
155
- const opStr = operatorToString(op);
156
- const valStr = op === 'in' && Array.isArray(c.value)
157
- ? c.value.map(v => serializeValue(v)).join(', ')
158
- : serializeValue(c.value);
159
- return `${c.field} ${opStr} ${valStr}`;
160
- }).join('\n');
172
+ return conditions
173
+ .map((c) => {
174
+ const op = c.operator || 'eq';
175
+ const opStr = operatorToString(op);
176
+ const valStr =
177
+ op === 'in' && Array.isArray(c.value)
178
+ ? c.value.map((v) => serializeValue(v)).join(', ')
179
+ : serializeValue(c.value);
180
+ return `${c.field} ${opStr} ${valStr}`;
181
+ })
182
+ .join('\n');
161
183
  }
162
184
 
163
185
  function operatorToString(op: CMSFilterOperator): string {
164
186
  const map: Record<CMSFilterOperator, string> = {
165
- 'eq': '=',
166
- 'neq': '!=',
167
- 'gt': '>',
168
- 'gte': '>=',
169
- 'lt': '<',
170
- 'lte': '<=',
171
- 'contains': 'contains',
172
- 'in': 'in'
187
+ eq: '=',
188
+ neq: '!=',
189
+ gt: '>',
190
+ gte: '>=',
191
+ lt: '<',
192
+ lte: '<=',
193
+ contains: 'contains',
194
+ in: 'in',
173
195
  };
174
196
  return map[op] || '=';
175
197
  }
@@ -39,9 +39,9 @@ export function parseCssRgb(value: string): RgbColor | null {
39
39
  const m = value.trim().match(RGB_RE);
40
40
  if (!m) return null;
41
41
  return {
42
- r: clamp(parseInt(m[1], 10), 0, 255),
43
- g: clamp(parseInt(m[2], 10), 0, 255),
44
- b: clamp(parseInt(m[3], 10), 0, 255),
42
+ r: clamp(parseInt(m[1] ?? '', 10), 0, 255),
43
+ g: clamp(parseInt(m[2] ?? '', 10), 0, 255),
44
+ b: clamp(parseInt(m[3] ?? '', 10), 0, 255),
45
45
  };
46
46
  }
47
47
 
@@ -50,9 +50,9 @@ export function parseCssHsl(value: string): HslColor | null {
50
50
  const m = value.trim().match(HSL_RE);
51
51
  if (!m) return null;
52
52
  return {
53
- h: clamp(parseFloat(m[1]), 0, 360),
54
- s: clamp(parseFloat(m[2]), 0, 100),
55
- l: clamp(parseFloat(m[3]), 0, 100),
53
+ h: clamp(parseFloat(m[1] ?? ''), 0, 360),
54
+ s: clamp(parseFloat(m[2] ?? ''), 0, 100),
55
+ l: clamp(parseFloat(m[3] ?? ''), 0, 100),
56
56
  };
57
57
  }
58
58
 
@@ -74,9 +74,9 @@ export function hslToCssString(hsl: HslColor): string {
74
74
  function expandHex(hex: string): string {
75
75
  const h = hex.replace('#', '');
76
76
  if (h.length === 3) {
77
- return '#' + h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
77
+ return `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`;
78
78
  }
79
- return '#' + h;
79
+ return `#${h}`;
80
80
  }
81
81
 
82
82
  /** Convert a hex color string to an RgbColor */
@@ -94,7 +94,7 @@ export function rgbToHex(rgb: RgbColor): string {
94
94
  const r = clamp(Math.round(rgb.r), 0, 255);
95
95
  const g = clamp(Math.round(rgb.g), 0, 255);
96
96
  const b = clamp(Math.round(rgb.b), 0, 255);
97
- return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
97
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
98
98
  }
99
99
 
100
100
  // ── RGB ↔ HSL ──────────────────────────────────────────────