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
@@ -8,7 +8,12 @@ import { join } from 'path';
8
8
  import type { CMSProvider, CMSSchemaInfo } from '../../shared/interfaces/contentProvider';
9
9
  import type { CMSSchema, CMSItem } from '../../shared/types';
10
10
  import { validateCMSItem, validateCMSDraftItem } from '../../shared/validation/validators';
11
- import { isSafePathSegment, isValidIdentifier, isReservedDraftFilename, CMS_DRAFT_SUFFIX } from '../../shared/pathSecurity';
11
+ import {
12
+ isSafePathSegment,
13
+ isValidIdentifier,
14
+ isReservedDraftFilename,
15
+ CMS_DRAFT_SUFFIX,
16
+ } from '../../shared/pathSecurity';
12
17
  import { readJsonFile, fileExists } from '../runtime';
13
18
 
14
19
  const DRAFT_FILE_SUFFIX = `${CMS_DRAFT_SUFFIX}.json`;
@@ -54,7 +59,9 @@ function normalizeItem(content: unknown, filename: string, isDraft = false): CMS
54
59
  * `_filename` differs from `_id` keep the field on disk so the on-disk
55
60
  * file-routing path can still find them.
56
61
  */
57
- function stripTransient<T extends Record<string, unknown>>(item: T): Omit<T, '_slug' | '_isDraft' | '_hasDraft' | '_url'> {
62
+ function stripTransient<T extends Record<string, unknown>>(
63
+ item: T,
64
+ ): Omit<T, '_slug' | '_isDraft' | '_hasDraft' | '_url'> {
58
65
  const { _slug, _isDraft, _hasDraft, _url, _filename, ...rest } = item;
59
66
  const out = rest as Omit<T, '_slug' | '_isDraft' | '_hasDraft' | '_url'>;
60
67
  if (typeof _filename === 'string' && _filename !== (out as Record<string, unknown>)._id) {
@@ -72,7 +79,7 @@ export class FileSystemCMSProvider implements CMSProvider {
72
79
 
73
80
  constructor(
74
81
  private templatesDir: string, // e.g., './templates' - CMS template pages
75
- private cmsDir: string // e.g., './cms' - items stored here
82
+ private cmsDir: string, // e.g., './cms' - items stored here
76
83
  ) {}
77
84
 
78
85
  /**
@@ -81,7 +88,9 @@ export class FileSystemCMSProvider implements CMSProvider {
81
88
  */
82
89
  private validateCollection(collection: string): void {
83
90
  if (!isValidIdentifier(collection)) {
84
- throw new Error(`Invalid collection name: "${collection}". Collection names must contain only letters, numbers, hyphens, and underscores.`);
91
+ throw new Error(
92
+ `Invalid collection name: "${collection}". Collection names must contain only letters, numbers, hyphens, and underscores.`,
93
+ );
85
94
  }
86
95
  }
87
96
 
@@ -91,7 +100,9 @@ export class FileSystemCMSProvider implements CMSProvider {
91
100
  */
92
101
  private validateFilename(filename: string): void {
93
102
  if (!isSafePathSegment(filename)) {
94
- throw new Error(`Invalid filename: "${filename}". Filenames cannot contain path separators or traversal sequences.`);
103
+ throw new Error(
104
+ `Invalid filename: "${filename}". Filenames cannot contain path separators or traversal sequences.`,
105
+ );
95
106
  }
96
107
  if (isReservedDraftFilename(filename)) {
97
108
  throw new Error(`Invalid filename: "${filename}". The "${CMS_DRAFT_SUFFIX}" suffix is reserved for draft files.`);
@@ -114,14 +125,14 @@ export class FileSystemCMSProvider implements CMSProvider {
114
125
  }
115
126
 
116
127
  const files = readdirSync(this.templatesDir);
117
- const jsonFiles = files.filter(f => f.endsWith('.json'));
128
+ const jsonFiles = files.filter((f) => f.endsWith('.json'));
118
129
 
119
130
  const results = await Promise.all(
120
- jsonFiles.map(async file => {
131
+ jsonFiles.map(async (file) => {
121
132
  const filePath = join(this.templatesDir, file);
122
133
  const content = await loadJSONFile(filePath);
123
134
  return { filePath, content };
124
- })
135
+ }),
125
136
  );
126
137
 
127
138
  for (const { filePath, content } of results) {
@@ -160,14 +171,14 @@ export class FileSystemCMSProvider implements CMSProvider {
160
171
 
161
172
  const files = readdirSync(collectionDir);
162
173
  // Published items only — exclude `*.draft.json` files (draft-version siblings)
163
- const jsonFiles = files.filter(f => f.endsWith('.json') && !f.endsWith(DRAFT_FILE_SUFFIX));
174
+ const jsonFiles = files.filter((f) => f.endsWith('.json') && !f.endsWith(DRAFT_FILE_SUFFIX));
164
175
 
165
176
  const results = await Promise.all(
166
- jsonFiles.map(async file => {
177
+ jsonFiles.map(async (file) => {
167
178
  const filePath = join(collectionDir, file);
168
179
  const content = await loadJSONFile(filePath);
169
180
  return { file, filePath, content };
170
- })
181
+ }),
171
182
  );
172
183
 
173
184
  const items: CMSItem[] = [];
@@ -225,7 +236,7 @@ export class FileSystemCMSProvider implements CMSProvider {
225
236
  */
226
237
  async getItemById(collection: string, id: string): Promise<CMSItem | null> {
227
238
  const items = await this.getItems(collection);
228
- return items.find(item => item._id === id) || null;
239
+ return items.find((item) => item._id === id) || null;
229
240
  }
230
241
 
231
242
  /**
@@ -340,14 +351,14 @@ export class FileSystemCMSProvider implements CMSProvider {
340
351
  const collectionDir = join(this.cmsDir, collection);
341
352
  if (!existsSync(collectionDir)) return [];
342
353
 
343
- const files = readdirSync(collectionDir).filter(f => f.endsWith(DRAFT_FILE_SUFFIX));
354
+ const files = readdirSync(collectionDir).filter((f) => f.endsWith(DRAFT_FILE_SUFFIX));
344
355
 
345
356
  const results = await Promise.all(
346
- files.map(async file => {
357
+ files.map(async (file) => {
347
358
  const filePath = join(collectionDir, file);
348
359
  const content = await loadJSONFile(filePath);
349
360
  return { file, content };
350
- })
361
+ }),
351
362
  );
352
363
 
353
364
  const drafts: CMSItem[] = [];
@@ -415,7 +426,7 @@ export class FileSystemCMSProvider implements CMSProvider {
415
426
  // (validateCMSItem) runs on Publish.
416
427
  const validation = validateCMSDraftItem(itemData);
417
428
  if (!validation.valid) {
418
- const messages = validation.errors.map(e => `${e.path}: ${e.message}`).join(', ');
429
+ const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join(', ');
419
430
  throw new Error(`Invalid draft: ${messages}`);
420
431
  }
421
432
 
@@ -460,7 +471,7 @@ export class FileSystemCMSProvider implements CMSProvider {
460
471
  const item = normalizeItem(content, filename);
461
472
  const validation = validateCMSItem(item);
462
473
  if (!validation.valid) {
463
- const messages = validation.errors.map(e => `${e.path}: ${e.message}`).join(', ');
474
+ const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join(', ');
464
475
  throw new Error(`Cannot publish invalid draft: ${messages}`);
465
476
  }
466
477
 
@@ -495,7 +506,9 @@ export class FileSystemCMSProvider implements CMSProvider {
495
506
  */
496
507
  async saveSchema(collectionId: string, pageData: unknown): Promise<void> {
497
508
  if (!isValidIdentifier(collectionId)) {
498
- throw new Error(`Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`);
509
+ throw new Error(
510
+ `Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`,
511
+ );
499
512
  }
500
513
  const { writeFile, mkdir } = await import('fs/promises');
501
514
 
@@ -531,7 +544,9 @@ export class FileSystemCMSProvider implements CMSProvider {
531
544
  */
532
545
  async updateSchema(collectionId: string, updates: Partial<CMSSchema>): Promise<void> {
533
546
  if (!isValidIdentifier(collectionId)) {
534
- throw new Error(`Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`);
547
+ throw new Error(
548
+ `Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`,
549
+ );
535
550
  }
536
551
  const { readFile, writeFile } = await import('fs/promises');
537
552
 
@@ -552,7 +567,7 @@ export class FileSystemCMSProvider implements CMSProvider {
552
567
  pageData.meta.cms = {
553
568
  ...pageData.meta.cms,
554
569
  ...updates,
555
- id: collectionId, // ID cannot be changed
570
+ id: collectionId, // ID cannot be changed
556
571
  };
557
572
 
558
573
  await writeFile(pageFilePath, JSON.stringify(pageData, null, 2), 'utf-8');
@@ -0,0 +1,84 @@
1
+ /**
2
+ * FileSystemPageProvider Tests
3
+ *
4
+ * Focus: normal nested page paths keep working, and page paths that try to escape the
5
+ * pages/templates directory (`..` traversal, NUL bytes) are rejected before any
6
+ * filesystem write/delete can touch an ancestor directory.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
10
+ import { FileSystemPageProvider } from './fileSystemPageProvider';
11
+ import { existsSync, rmSync, mkdirSync } from 'fs';
12
+ import { join } from 'path';
13
+
14
+ const ROOT = join('/tmp', 'page-provider-test-' + Date.now());
15
+ const PAGES_DIR = join(ROOT, 'project', 'pages');
16
+ const TEMPLATES_DIR = join(ROOT, 'project', 'templates');
17
+ // A sentinel directory OUTSIDE pagesDir that a traversal attack would try to reach.
18
+ const OUTSIDE_DIR = join(ROOT, 'project');
19
+
20
+ describe('FileSystemPageProvider', () => {
21
+ let provider: FileSystemPageProvider;
22
+
23
+ beforeEach(() => {
24
+ mkdirSync(PAGES_DIR, { recursive: true });
25
+ mkdirSync(TEMPLATES_DIR, { recursive: true });
26
+ provider = new FileSystemPageProvider(PAGES_DIR, TEMPLATES_DIR);
27
+ });
28
+
29
+ afterEach(() => {
30
+ rmSync(ROOT, { recursive: true, force: true });
31
+ });
32
+
33
+ describe('normal page paths', () => {
34
+ it('saves and reads a top-level page', async () => {
35
+ await provider.save('/about', '{"ok":true}');
36
+ expect(existsSync(join(PAGES_DIR, 'about.json'))).toBe(true);
37
+ expect(await provider.get('/about')).toBe('{"ok":true}');
38
+ expect(await provider.exists('/about')).toBe(true);
39
+ });
40
+
41
+ it('saves and reads a nested page, auto-creating folders', async () => {
42
+ await provider.save('/blog/post-1', '{"slug":"post-1"}');
43
+ expect(existsSync(join(PAGES_DIR, 'blog', 'post-1.json'))).toBe(true);
44
+ expect(await provider.get('/blog/post-1')).toBe('{"slug":"post-1"}');
45
+ });
46
+
47
+ it('maps the index route to index.json', async () => {
48
+ await provider.save('/', '{"home":true}');
49
+ expect(existsSync(join(PAGES_DIR, 'index.json'))).toBe(true);
50
+ });
51
+
52
+ it('routes template paths to the templates directory', async () => {
53
+ await provider.save('/templates/blog-post', '{"template":true}');
54
+ expect(existsSync(join(TEMPLATES_DIR, 'blog-post.json'))).toBe(true);
55
+ expect(await provider.get('/templates/blog-post')).toBe('{"template":true}');
56
+ });
57
+ });
58
+
59
+ describe('path traversal rejection', () => {
60
+ it('rejects save for a path escaping pagesDir', async () => {
61
+ await expect(provider.save('/../../evil', '{"x":1}')).rejects.toThrow(/resolves outside/);
62
+ // Nothing must have been written outside pagesDir.
63
+ expect(existsSync(join(ROOT, 'evil.json'))).toBe(false);
64
+ expect(existsSync(join(OUTSIDE_DIR, 'evil.json'))).toBe(false);
65
+ });
66
+
67
+ it('rejects delete for a path escaping pagesDir', async () => {
68
+ await expect(provider.delete('/../../evil')).rejects.toThrow(/resolves outside/);
69
+ });
70
+
71
+ it('rejects get/exists for a traversal path', async () => {
72
+ await expect(provider.get('/../../evil')).rejects.toThrow(/resolves outside/);
73
+ await expect(provider.exists('/../../evil')).rejects.toThrow(/resolves outside/);
74
+ });
75
+
76
+ it('rejects a template path escaping templatesDir', async () => {
77
+ await expect(provider.save('/templates/../../evil', '{"x":1}')).rejects.toThrow(/resolves outside/);
78
+ });
79
+
80
+ it('rejects a path containing a NUL byte', async () => {
81
+ await expect(provider.save('/foo\0bar', '{"x":1}')).rejects.toThrow(/NUL byte/);
82
+ });
83
+ });
84
+ });
@@ -4,10 +4,11 @@
4
4
  */
5
5
 
6
6
  import { existsSync, readdirSync, mkdirSync, rmdirSync } from 'fs';
7
- import { join, dirname } from 'path';
7
+ import { join, dirname, resolve, sep } from 'path';
8
8
  import type { PageProvider } from '../../shared/interfaces/contentProvider';
9
9
  import { isJSONFile, stripExtension, mapPageNameToPath, mapPathToPageName } from '../../shared/utils/fileUtils';
10
10
  import { readTextFile, fileExists } from '../runtime';
11
+ import { logRuntimeError } from '../../shared/errorLogger';
11
12
 
12
13
  /**
13
14
  * Load JSON file content from disk
@@ -18,7 +19,8 @@ async function loadJSONFile(filePath: string): Promise<string | null> {
18
19
  return await readTextFile(filePath);
19
20
  }
20
21
  return null;
21
- } catch {
22
+ } catch (error) {
23
+ logRuntimeError('fileSystemPageProvider.loadJSONFile', error, { filePath });
22
24
  return null;
23
25
  }
24
26
  }
@@ -31,7 +33,7 @@ async function loadJSONFile(filePath: string): Promise<string | null> {
31
33
  export class FileSystemPageProvider implements PageProvider {
32
34
  constructor(
33
35
  private pagesDir: string,
34
- private templatesDir?: string
36
+ private templatesDir?: string,
35
37
  ) {}
36
38
 
37
39
  async loadAll(): Promise<Map<string, string>> {
@@ -57,38 +59,55 @@ export class FileSystemPageProvider implements PageProvider {
57
59
  private async scanDirectory(dir: string, prefix: string, pages: Map<string, string>): Promise<void> {
58
60
  const entries = readdirSync(dir, { withFileTypes: true });
59
61
 
60
- const fileEntries = entries.filter(e => e.isFile() && isJSONFile(e.name));
61
- const dirEntries = entries.filter(e => e.isDirectory());
62
+ const fileEntries = entries.filter((e) => e.isFile() && isJSONFile(e.name));
63
+ const dirEntries = entries.filter((e) => e.isDirectory());
62
64
 
63
65
  await Promise.all([
64
- ...fileEntries.map(async entry => {
65
- const pageName = prefix
66
- ? `${prefix}/${stripExtension(entry.name)}`
67
- : stripExtension(entry.name);
66
+ ...fileEntries.map(async (entry) => {
67
+ const pageName = prefix ? `${prefix}/${stripExtension(entry.name)}` : stripExtension(entry.name);
68
68
  const content = await loadJSONFile(join(dir, entry.name));
69
69
  if (content) {
70
70
  const pagePath = mapPageNameToPath(pageName);
71
71
  pages.set(pagePath, content);
72
72
  }
73
73
  }),
74
- ...dirEntries.map(entry => {
74
+ ...dirEntries.map((entry) => {
75
75
  const subPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
76
76
  return this.scanDirectory(join(dir, entry.name), subPrefix, pages);
77
77
  }),
78
78
  ]);
79
79
  }
80
80
 
81
+ /**
82
+ * Assert that `candidate` resolves inside `baseDir`. Without this a page path
83
+ * like `/../../foo` maps to pageName `../../foo` and would be joined OUTSIDE the
84
+ * pages/templates dir — letting save() write, and delete() (plus its empty-parent
85
+ * rmdir loop) operate on, arbitrary ancestor directories. Throws on escape.
86
+ */
87
+ private assertWithin(baseDir: string, candidate: string): string {
88
+ const baseResolved = resolve(baseDir);
89
+ const resolved = resolve(candidate);
90
+ if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {
91
+ throw new Error(`Invalid page path: "${candidate}" resolves outside of "${baseDir}"`);
92
+ }
93
+ return candidate;
94
+ }
95
+
81
96
  /**
82
97
  * Resolve a page path to its filesystem file path
83
98
  * Routes /templates/* paths to the templates directory
84
99
  */
85
100
  private resolveFilePath(path: string): string {
86
101
  const pageName = mapPathToPageName(path);
102
+ // Reject NUL bytes outright (fs would throw, but fail fast with a clear error).
103
+ if (pageName.includes('\0')) {
104
+ throw new Error('Invalid page path: contains NUL byte');
105
+ }
87
106
  if (this.templatesDir && pageName.startsWith('templates/')) {
88
107
  const templateName = pageName.substring('templates/'.length);
89
- return join(this.templatesDir, `${templateName}.json`);
108
+ return this.assertWithin(this.templatesDir, join(this.templatesDir, `${templateName}.json`));
90
109
  }
91
- return join(this.pagesDir, `${pageName}.json`);
110
+ return this.assertWithin(this.pagesDir, join(this.pagesDir, `${pageName}.json`));
92
111
  }
93
112
 
94
113
  /**
@@ -7,12 +7,7 @@ import { describe, it, expect, beforeEach } from 'bun:test';
7
7
  import type { CMSProvider, CMSSchemaInfo } from '../../../shared/interfaces/contentProvider';
8
8
  import type { CMSItem, CMSSchema } from '../../../shared/types';
9
9
  import { CMSService } from '../../services/cmsService';
10
- import {
11
- handleCollectionsRoute,
12
- handleCollectionSchemaRoute,
13
- handleItemsRoute,
14
- handleItemRoute,
15
- } from './cms';
10
+ import { handleCollectionsRoute, handleCollectionSchemaRoute, handleItemsRoute, handleItemRoute } from './cms';
16
11
 
17
12
  // Note: Write handler tests (handleCreateItemRoute, handleUpdateItemRoute, handleDeleteItemRoute)
18
13
  // moved to @meno/studio
@@ -55,7 +50,14 @@ const mockProductSchemaInfo: CMSSchemaInfo = {
55
50
 
56
51
  const mockBlogItems: CMSItem[] = [
57
52
  { _id: '1', _slug: 'hello-world', slug: 'hello-world', title: 'Hello World', content: 'First post', featured: true },
58
- { _id: '2', _slug: 'getting-started', slug: 'getting-started', title: 'Getting Started', content: 'Guide', featured: false },
53
+ {
54
+ _id: '2',
55
+ _slug: 'getting-started',
56
+ slug: 'getting-started',
57
+ title: 'Getting Started',
58
+ content: 'Guide',
59
+ featured: false,
60
+ },
59
61
  ];
60
62
 
61
63
  describe('CMS API Routes - Read Operations', () => {
@@ -64,34 +66,44 @@ describe('CMS API Routes - Read Operations', () => {
64
66
 
65
67
  beforeEach(async () => {
66
68
  mockProvider = {
67
- getAllSchemas: async () => new Map([
68
- ['blog-posts', mockSchemaInfo],
69
- ['products', mockProductSchemaInfo],
70
- ]),
69
+ getAllSchemas: async () =>
70
+ new Map([
71
+ ['blog-posts', mockSchemaInfo],
72
+ ['products', mockProductSchemaInfo],
73
+ ]),
71
74
  getItems: async (collection: string) => {
72
75
  if (collection === 'blog-posts') return [...mockBlogItems];
73
76
  return [];
74
77
  },
75
78
  getItemBySlug: async (collection: string, slug: string) => {
76
79
  if (collection === 'blog-posts') {
77
- return mockBlogItems.find(i => i._slug === slug) || null;
80
+ return mockBlogItems.find((i) => i._slug === slug) || null;
78
81
  }
79
82
  return null;
80
83
  },
81
84
  getItemByFilename: async (collection: string, filename: string) => {
82
85
  if (collection === 'blog-posts') {
83
- return mockBlogItems.find(i => i._filename === filename || i._slug === filename) || null;
86
+ return mockBlogItems.find((i) => i._filename === filename || i._slug === filename) || null;
84
87
  }
85
88
  return null;
86
89
  },
87
90
  getItemById: async (collection: string, id: string) => {
88
91
  if (collection === 'blog-posts') {
89
- return mockBlogItems.find(i => i._id === id) || null;
92
+ return mockBlogItems.find((i) => i._id === id) || null;
90
93
  }
91
94
  return null;
92
95
  },
93
96
  saveItem: async () => {},
94
97
  deleteItem: async () => {},
98
+ // Draft-model methods (no drafts exist in this mock)
99
+ getDraft: async () => null,
100
+ getAllDrafts: async () => [],
101
+ hasDraft: async () => false,
102
+ saveDraft: async () => {},
103
+ discardDraft: async () => {},
104
+ publishDraft: async () => {
105
+ throw new Error('No draft exists');
106
+ },
95
107
  };
96
108
 
97
109
  cmsService = new CMSService(mockProvider);
@@ -26,10 +26,7 @@ export function handleCollectionsRoute(cmsService: CMSService): Response {
26
26
  /**
27
27
  * GET /api/cms/collections/:collection - Get schema for specific collection
28
28
  */
29
- export function handleCollectionSchemaRoute(
30
- collection: string,
31
- cmsService: CMSService
32
- ): Response {
29
+ export function handleCollectionSchemaRoute(collection: string, cmsService: CMSService): Response {
33
30
  const schema = cmsService.getSchema(collection);
34
31
 
35
32
  if (!schema) {
@@ -51,7 +48,7 @@ export function handleCollectionSchemaRoute(
51
48
  export async function handleItemsRoute(
52
49
  collection: string,
53
50
  cmsService: CMSService,
54
- searchParams?: URLSearchParams
51
+ searchParams?: URLSearchParams,
55
52
  ): Promise<Response> {
56
53
  // Parse pagination params
57
54
  const limit = searchParams?.get('limit') ? parseInt(searchParams.get('limit')!, 10) : undefined;
@@ -69,7 +66,7 @@ export async function handleItemsRoute(
69
66
  const schema = cmsService.getSchema(collection);
70
67
  const slugField = schema?.slugField || 'slug';
71
68
 
72
- filteredItems = allItems.filter(item => {
69
+ filteredItems = allItems.filter((item) => {
73
70
  // Search in title, slug, and filename
74
71
  const title = extractStringValue(item.title || '').toLowerCase();
75
72
  const slug = extractStringValue(item[slugField] || item._slug || '').toLowerCase();
@@ -95,27 +92,25 @@ export async function handleItemsRoute(
95
92
  items: itemsWithUrl,
96
93
  total: search ? filteredItems.length : total,
97
94
  limit,
98
- offset: offset || 0
95
+ offset: offset || 0,
99
96
  });
100
97
  }
101
98
 
102
99
  /**
103
100
  * GET /api/cms/:collection/:slug - Get specific item by slug
104
101
  */
105
- export async function handleItemRoute(
106
- collection: string,
107
- slug: string,
108
- cmsService: CMSService
109
- ): Promise<Response> {
102
+ export async function handleItemRoute(collection: string, slug: string, cmsService: CMSService): Promise<Response> {
110
103
  const match = await cmsService.matchRoute(
111
- cmsService.getSchema(collection)?.urlPattern.replace('{{slug}}', slug) || ''
104
+ cmsService.getSchema(collection)?.urlPattern.replace('{{slug}}', slug) || '',
112
105
  );
113
106
 
114
107
  // If no match via route, try direct provider access
115
108
  if (!match) {
116
109
  // For direct item access, we need to check if item exists
117
110
  const items = await cmsService.queryItems({ collection });
118
- const item = items.find(i => i._slug === slug || i[cmsService.getSchema(collection)?.slugField || 'slug'] === slug);
111
+ const item = items.find(
112
+ (i) => i._slug === slug || i[cmsService.getSchema(collection)?.slugField || 'slug'] === slug,
113
+ );
119
114
 
120
115
  if (!item) {
121
116
  return errorResponse('Item not found', 404);
@@ -16,10 +16,7 @@ import { formatJsonErrorMessage } from '../../../shared/jsonRepair';
16
16
  * Uses ETag revalidation so repeat page navigations get a 304 when the
17
17
  * registry hasn't changed.
18
18
  */
19
- export function handleComponentsRoute(
20
- req: Request,
21
- componentService: ComponentService,
22
- ): Response {
19
+ export function handleComponentsRoute(req: Request, componentService: ComponentService): Response {
23
20
  const componentsWithCategories = componentService.getAllComponentsWithCategories();
24
21
 
25
22
  // Transform to API format: { name: { ...definition, _category } }
@@ -27,7 +24,7 @@ export function handleComponentsRoute(
27
24
  for (const [name, info] of Object.entries(componentsWithCategories)) {
28
25
  result[name] = {
29
26
  ...info.definition,
30
- _category: info.category
27
+ _category: info.category,
31
28
  };
32
29
  }
33
30
 
@@ -44,10 +41,7 @@ export function handleComponentsRoute(
44
41
  * Handle component data API endpoint - GET /api/component-data/:name
45
42
  * Validates that component has structure before returning
46
43
  */
47
- export function handleComponentDataRoute(
48
- url: URL,
49
- componentService: ComponentService
50
- ): Response {
44
+ export function handleComponentDataRoute(url: URL, componentService: ComponentService): Response {
51
45
  const componentName = url.pathname.replace('/api/component-data/', '');
52
46
  const componentDef = componentService.getComponent(componentName);
53
47
 
@@ -56,14 +50,12 @@ export function handleComponentDataRoute(
56
50
  const loadErrors = componentService.getLoadErrors();
57
51
  const diagnostic = loadErrors.get(componentName);
58
52
  if (diagnostic) {
59
- const message = formatJsonErrorMessage(
60
- `${diagnostic.componentName}.json`,
61
- { message: diagnostic.description, line: diagnostic.line, column: diagnostic.column }
62
- );
63
- return jsonResponse(
64
- { error: 'json_parse_error', message, errorCode: 'JSON_PARSE_ERROR' },
65
- { status: 422 }
66
- );
53
+ const message = formatJsonErrorMessage(`${diagnostic.componentName}.json`, {
54
+ message: diagnostic.description,
55
+ line: diagnostic.line,
56
+ column: diagnostic.column,
57
+ });
58
+ return jsonResponse({ error: 'json_parse_error', message, errorCode: 'JSON_PARSE_ERROR' }, { status: 422 });
67
59
  }
68
60
  return jsonResponse({ error: 'Component not found' }, { status: 404 });
69
61
  }
@@ -73,11 +65,11 @@ export function handleComponentDataRoute(
73
65
  // Validate that component has structure
74
66
  if (!componentService.validateComponentStructure(componentDef)) {
75
67
  return jsonResponse(
76
- {
68
+ {
77
69
  error: 'Component structure is missing',
78
- message: `Component "${componentName}" does not have a valid structure definition. Please add a structure field to the component definition.`
79
- },
80
- { status: 400 }
70
+ message: `Component "${componentName}" does not have a valid structure definition. Please add a structure field to the component definition.`,
71
+ },
72
+ { status: 400 },
81
73
  );
82
74
  }
83
75
 
@@ -87,10 +79,7 @@ export function handleComponentDataRoute(
87
79
  /**
88
80
  * Handle component JavaScript API endpoint - GET /api/component-js/:name
89
81
  */
90
- export async function handleComponentJavaScriptRoute(
91
- url: URL,
92
- componentService: ComponentService
93
- ): Promise<Response> {
82
+ export async function handleComponentJavaScriptRoute(url: URL, componentService: ComponentService): Promise<Response> {
94
83
  const componentName = url.pathname.replace('/api/component-js/', '');
95
84
  const jsContent = await componentService.getComponentJavaScript(componentName);
96
85
  const corsHeaders = createCorsHeaders();
@@ -107,13 +96,18 @@ export async function handleComponentJavaScriptRoute(
107
96
  * Handle component usage API endpoint - GET /api/component-usage
108
97
  * Returns how many times each component is used across all pages and other components
109
98
  */
110
- export function handleComponentUsageRoute(
111
- pageService: PageService,
112
- componentService: ComponentService
113
- ): Response {
114
- const usageMap = new Map<string, { count: number; files: Map<string, { name: string; path: string; type: 'page' | 'component' }> }>();
115
-
116
- function walkTree(node: ComponentNode | undefined | null, fileName: string, filePath: string, fileType: 'page' | 'component') {
99
+ export function handleComponentUsageRoute(pageService: PageService, componentService: ComponentService): Response {
100
+ const usageMap = new Map<
101
+ string,
102
+ { count: number; files: Map<string, { name: string; path: string; type: 'page' | 'component' }> }
103
+ >();
104
+
105
+ function walkTree(
106
+ node: ComponentNode | undefined | null,
107
+ fileName: string,
108
+ filePath: string,
109
+ fileType: 'page' | 'component',
110
+ ) {
117
111
  if (!node || typeof node !== 'object') return;
118
112
  if (node.type === 'component' && 'component' in node && (node as any).component) {
119
113
  const compName = (node as any).component as string;
@@ -152,7 +146,10 @@ export function handleComponentUsageRoute(
152
146
  }
153
147
 
154
148
  // Convert to plain object
155
- const result: Record<string, { count: number; files: Array<{ name: string; path: string; type: 'page' | 'component' }> }> = {};
149
+ const result: Record<
150
+ string,
151
+ { count: number; files: Array<{ name: string; path: string; type: 'page' | 'component' }> }
152
+ > = {};
156
153
  for (const [name, data] of usageMap) {
157
154
  result[name] = {
158
155
  count: data.count,
@@ -165,4 +162,3 @@ export function handleComponentUsageRoute(
165
162
 
166
163
  // Note: Write handlers (handleSaveComponentRoute, handleSaveComponentJavaScriptRoute,
167
164
  // handleSaveComponentCSSRoute, handleComponentCategoryRoute) moved to @meno/studio
168
-
@@ -44,4 +44,3 @@ export async function handleComponentsConfigRoute(): Promise<Response> {
44
44
 
45
45
  // Note: handleSaveConfigRoute moved to @meno/studio
46
46
  // Note: handleSaveComponentsConfigRoute moved to @meno/studio
47
-