meno-core 1.0.53 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +66 -261
  3. package/build-astro.ts +150 -128
  4. package/dist/bin/cli.js +42 -185
  5. package/dist/bin/cli.js.map +2 -2
  6. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  7. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  8. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
  9. package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
  10. package/dist/chunks/chunk-4ZRU52J2.js +169 -0
  11. package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
  12. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
  13. package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
  14. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
  15. package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
  16. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
  17. package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
  18. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
  19. package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
  20. package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
  21. package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
  22. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
  23. package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
  24. package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
  25. package/dist/lib/client/index.js +1003 -475
  26. package/dist/lib/client/index.js.map +3 -3
  27. package/dist/lib/server/index.js +14393 -6371
  28. package/dist/lib/server/index.js.map +4 -4
  29. package/dist/lib/shared/index.js +342 -101
  30. package/dist/lib/shared/index.js.map +4 -4
  31. package/dist/lib/shared/richtext/index.js +1 -1
  32. package/dist/lib/test-utils/index.js +64 -80
  33. package/dist/lib/test-utils/index.js.map +2 -2
  34. package/entries/client-router.tsx +10 -8
  35. package/lib/client/ClientInitializer.ts +8 -8
  36. package/lib/client/ErrorBoundary.test.tsx +246 -186
  37. package/lib/client/ErrorBoundary.tsx +183 -132
  38. package/lib/client/componentRegistry.test.ts +96 -108
  39. package/lib/client/componentRegistry.ts +1 -3
  40. package/lib/client/contexts/ThemeContext.tsx +3 -2
  41. package/lib/client/core/ComponentBuilder.test.ts +663 -560
  42. package/lib/client/core/ComponentBuilder.ts +427 -155
  43. package/lib/client/core/ComponentRenderer.test.tsx +2 -4
  44. package/lib/client/core/ComponentRenderer.tsx +46 -33
  45. package/lib/client/core/builders/embedBuilder.ts +242 -54
  46. package/lib/client/core/builders/linkBuilder.ts +72 -45
  47. package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
  48. package/lib/client/core/builders/listBuilder.ts +155 -89
  49. package/lib/client/core/builders/localeListBuilder.ts +97 -62
  50. package/lib/client/core/builders/types.ts +5 -5
  51. package/lib/client/core/cmsTemplateProcessor.ts +8 -9
  52. package/lib/client/elementRegistry.ts +3 -3
  53. package/lib/client/fontFamiliesService.test.ts +2 -10
  54. package/lib/client/fontFamiliesService.ts +3 -3
  55. package/lib/client/hmr/HMRManager.tsx +47 -30
  56. package/lib/client/hmrCssReload.ts +49 -14
  57. package/lib/client/hmrWebSocket.ts +14 -19
  58. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  59. package/lib/client/hooks/useColorVariables.ts +23 -18
  60. package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
  61. package/lib/client/hooks/useVariables.ts +6 -4
  62. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  63. package/lib/client/hydration/HydrationUtils.ts +3 -4
  64. package/lib/client/i18nConfigService.test.ts +2 -7
  65. package/lib/client/i18nConfigService.ts +2 -2
  66. package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
  67. package/lib/client/meno-filter/MenoFilter.ts +14 -15
  68. package/lib/client/meno-filter/bindings.ts +18 -43
  69. package/lib/client/meno-filter/init.ts +4 -4
  70. package/lib/client/meno-filter/renderer.ts +30 -32
  71. package/lib/client/meno-filter/script.generated.ts +1 -3
  72. package/lib/client/meno-filter/ui.ts +4 -6
  73. package/lib/client/meno-filter/updates.ts +18 -23
  74. package/lib/client/meno-filter/utils.ts +1 -1
  75. package/lib/client/navigation.test.ts +174 -178
  76. package/lib/client/navigation.ts +0 -1
  77. package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
  78. package/lib/client/responsiveStyleResolver.test.ts +234 -232
  79. package/lib/client/responsiveStyleResolver.ts +74 -20
  80. package/lib/client/routing/RouteLoader.test.ts +24 -25
  81. package/lib/client/routing/RouteLoader.ts +27 -35
  82. package/lib/client/routing/Router.tsx +210 -153
  83. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  84. package/lib/client/scripts/ScriptExecutor.ts +18 -36
  85. package/lib/client/services/PrefetchService.test.ts +3 -3
  86. package/lib/client/services/PrefetchService.ts +23 -29
  87. package/lib/client/styleProcessor.test.ts +9 -9
  88. package/lib/client/styleProcessor.ts +24 -15
  89. package/lib/client/styles/StyleInjector.test.ts +122 -115
  90. package/lib/client/styles/StyleInjector.ts +10 -7
  91. package/lib/client/styles/UtilityClassCollector.ts +62 -28
  92. package/lib/client/styles/cspNonce.test.ts +2 -5
  93. package/lib/client/templateEngine.test.ts +554 -488
  94. package/lib/client/templateEngine.ts +203 -73
  95. package/lib/client/theme.test.ts +1 -1
  96. package/lib/client/theme.ts +0 -1
  97. package/lib/client/utils/toast.ts +0 -1
  98. package/lib/server/__integration__/api-routes.test.ts +8 -4
  99. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  100. package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
  101. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  102. package/lib/server/__integration__/static-assets.test.ts +3 -3
  103. package/lib/server/__integration__/test-helpers.ts +87 -74
  104. package/lib/server/ab/generateFunctions.ts +12 -10
  105. package/lib/server/astro/cmsPageEmitter.ts +26 -42
  106. package/lib/server/astro/componentEmitter.ts +35 -43
  107. package/lib/server/astro/cssCollector.ts +10 -26
  108. package/lib/server/astro/nodeToAstro.test.ts +1754 -34
  109. package/lib/server/astro/nodeToAstro.ts +232 -216
  110. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
  111. package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
  112. package/lib/server/astro/pageEmitter.ts +9 -13
  113. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  114. package/lib/server/astro/tailwindMapper.ts +33 -40
  115. package/lib/server/astro/templateTransformer.ts +17 -20
  116. package/lib/server/createServer.ts +7 -19
  117. package/lib/server/cssGenerator.test.ts +35 -44
  118. package/lib/server/cssGenerator.ts +8 -19
  119. package/lib/server/fileWatcher.test.ts +164 -13
  120. package/lib/server/fileWatcher.ts +209 -124
  121. package/lib/server/index.ts +14 -17
  122. package/lib/server/jsonLoader.test.ts +39 -2
  123. package/lib/server/jsonLoader.ts +39 -34
  124. package/lib/server/middleware/cors.test.ts +21 -21
  125. package/lib/server/middleware/cors.ts +28 -4
  126. package/lib/server/middleware/errorHandler.test.ts +7 -5
  127. package/lib/server/middleware/errorHandler.ts +3 -8
  128. package/lib/server/middleware/index.ts +0 -1
  129. package/lib/server/middleware/logger.test.ts +10 -8
  130. package/lib/server/middleware/logger.ts +17 -29
  131. package/lib/server/migrateTemplates.ts +3 -3
  132. package/lib/server/pageCache.test.ts +76 -77
  133. package/lib/server/pageCache.ts +0 -1
  134. package/lib/server/projectContext.ts +5 -4
  135. package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
  136. package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
  137. package/lib/server/routes/api/cms.test.ts +26 -14
  138. package/lib/server/routes/api/cms.ts +9 -14
  139. package/lib/server/routes/api/colors.test.ts +2 -2
  140. package/lib/server/routes/api/components.ts +31 -35
  141. package/lib/server/routes/api/config.ts +3 -4
  142. package/lib/server/routes/api/core-routes.ts +49 -76
  143. package/lib/server/routes/api/enums.test.ts +2 -2
  144. package/lib/server/routes/api/functions.ts +9 -17
  145. package/lib/server/routes/api/index.ts +3 -6
  146. package/lib/server/routes/api/pages.ts +8 -11
  147. package/lib/server/routes/api/shared.test.ts +1 -1
  148. package/lib/server/routes/api/shared.ts +12 -9
  149. package/lib/server/routes/api/variables.test.ts +3 -5
  150. package/lib/server/routes/api/variables.ts +1 -1
  151. package/lib/server/routes/index.ts +25 -28
  152. package/lib/server/routes/pages.ts +35 -31
  153. package/lib/server/routes/static.ts +17 -6
  154. package/lib/server/runtime/bundler.ts +98 -61
  155. package/lib/server/runtime/fs.ts +16 -26
  156. package/lib/server/runtime/httpServer.ts +8 -12
  157. package/lib/server/services/ColorService.test.ts +3 -3
  158. package/lib/server/services/ColorService.ts +37 -29
  159. package/lib/server/services/EnumService.test.ts +4 -8
  160. package/lib/server/services/EnumService.ts +11 -3
  161. package/lib/server/services/VariableService.test.ts +8 -12
  162. package/lib/server/services/VariableService.ts +11 -3
  163. package/lib/server/services/cmsService.test.ts +158 -120
  164. package/lib/server/services/cmsService.ts +36 -146
  165. package/lib/server/services/componentService.test.ts +125 -45
  166. package/lib/server/services/componentService.ts +70 -46
  167. package/lib/server/services/configService.test.ts +22 -45
  168. package/lib/server/services/configService.ts +50 -27
  169. package/lib/server/services/fileWatcherService.ts +42 -8
  170. package/lib/server/services/index.ts +0 -1
  171. package/lib/server/services/pageService.test.ts +8 -10
  172. package/lib/server/services/pageService.ts +32 -18
  173. package/lib/server/ssr/attributeBuilder.ts +18 -8
  174. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  175. package/lib/server/ssr/clientDataInjector.ts +7 -21
  176. package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
  177. package/lib/server/ssr/cssCollector.ts +1 -1
  178. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  179. package/lib/server/ssr/errorOverlay.ts +3 -9
  180. package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
  181. package/lib/server/ssr/htmlGenerator.test.ts +120 -44
  182. package/lib/server/ssr/htmlGenerator.ts +126 -91
  183. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  184. package/lib/server/ssr/imageMetadata.ts +27 -21
  185. package/lib/server/ssr/jsCollector.test.ts +8 -18
  186. package/lib/server/ssr/jsCollector.ts +3 -8
  187. package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
  188. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  189. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  190. package/lib/server/ssr/ssrRenderer.test.ts +316 -256
  191. package/lib/server/ssr/ssrRenderer.ts +795 -243
  192. package/lib/server/ssrRenderer.test.ts +1048 -954
  193. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  194. package/lib/server/utils/jsonLineMapper.ts +7 -7
  195. package/lib/server/validateStyleCoverage.ts +18 -21
  196. package/lib/server/websocketManager.ts +28 -19
  197. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  198. package/lib/shared/attributeNodeUtils.ts +5 -12
  199. package/lib/shared/breakpoints.test.ts +5 -5
  200. package/lib/shared/breakpoints.ts +4 -11
  201. package/lib/shared/cmsQuery.test.ts +81 -0
  202. package/lib/shared/cmsQuery.ts +92 -0
  203. package/lib/shared/cmsQueryParser.test.ts +50 -42
  204. package/lib/shared/cmsQueryParser.ts +60 -38
  205. package/lib/shared/colorConversions.ts +9 -9
  206. package/lib/shared/colorVariableUtils.test.ts +27 -5
  207. package/lib/shared/colorVariableUtils.ts +21 -12
  208. package/lib/shared/componentRefs.ts +1 -5
  209. package/lib/shared/constants.test.ts +3 -3
  210. package/lib/shared/constants.ts +8 -8
  211. package/lib/shared/cssGeneration.test.ts +285 -144
  212. package/lib/shared/cssGeneration.ts +217 -531
  213. package/lib/shared/cssNamedColors.ts +152 -30
  214. package/lib/shared/cssProperties.test.ts +69 -70
  215. package/lib/shared/cssProperties.ts +298 -114
  216. package/lib/shared/elementClassName.test.ts +109 -109
  217. package/lib/shared/elementClassName.ts +3 -3
  218. package/lib/shared/elementUtils.ts +12 -16
  219. package/lib/shared/errorLogger.test.ts +12 -12
  220. package/lib/shared/errorLogger.ts +2 -10
  221. package/lib/shared/errors.test.ts +2 -13
  222. package/lib/shared/errors.ts +2 -8
  223. package/lib/shared/expressionEvaluator.test.ts +145 -0
  224. package/lib/shared/expressionEvaluator.ts +52 -24
  225. package/lib/shared/fontLoader.test.ts +32 -18
  226. package/lib/shared/fontLoader.ts +2 -2
  227. package/lib/shared/friendlyError.ts +2 -3
  228. package/lib/shared/gradientUtils.test.ts +3 -7
  229. package/lib/shared/gradientUtils.ts +17 -14
  230. package/lib/shared/hrefRefs.test.ts +2 -2
  231. package/lib/shared/hrefRefs.ts +2 -10
  232. package/lib/shared/i18n.test.ts +4 -4
  233. package/lib/shared/i18n.ts +18 -38
  234. package/lib/shared/index.ts +27 -0
  235. package/lib/shared/inlineSvgStyleRules.ts +14 -13
  236. package/lib/shared/interactiveStyleMappings.test.ts +32 -54
  237. package/lib/shared/interactiveStyleMappings.ts +11 -24
  238. package/lib/shared/interactiveStyles.test.ts +165 -188
  239. package/lib/shared/interfaces/contentProvider.ts +1 -1
  240. package/lib/shared/itemTemplateUtils.test.ts +11 -17
  241. package/lib/shared/itemTemplateUtils.ts +27 -39
  242. package/lib/shared/jsonRepair.ts +16 -10
  243. package/lib/shared/libraryLoader.test.ts +21 -55
  244. package/lib/shared/libraryLoader.ts +7 -22
  245. package/lib/shared/linkUtils.ts +1 -1
  246. package/lib/shared/logger.test.ts +66 -0
  247. package/lib/shared/logger.ts +94 -0
  248. package/lib/shared/markdown.ts +25 -0
  249. package/lib/shared/netlifyLocale404.test.ts +179 -0
  250. package/lib/shared/netlifyLocale404.ts +110 -0
  251. package/lib/shared/nodeUtils.test.ts +28 -16
  252. package/lib/shared/nodeUtils.ts +76 -23
  253. package/lib/shared/pathArrayUtils.test.ts +1 -2
  254. package/lib/shared/pathArrayUtils.ts +1 -1
  255. package/lib/shared/pathSecurity.ts +2 -2
  256. package/lib/shared/pathUtils.test.ts +4 -6
  257. package/lib/shared/pathUtils.ts +42 -49
  258. package/lib/shared/paths/Path.test.ts +2 -2
  259. package/lib/shared/paths/Path.ts +0 -1
  260. package/lib/shared/paths/PathConverter.test.ts +1 -1
  261. package/lib/shared/paths/PathConverter.ts +17 -20
  262. package/lib/shared/paths/PathUtils.ts +13 -13
  263. package/lib/shared/paths/PathValidator.test.ts +2 -15
  264. package/lib/shared/paths/PathValidator.ts +12 -10
  265. package/lib/shared/paths/index.ts +1 -2
  266. package/lib/shared/permissions.test.ts +168 -0
  267. package/lib/shared/permissions.ts +162 -0
  268. package/lib/shared/propResolver.test.ts +240 -244
  269. package/lib/shared/propResolver.ts +15 -26
  270. package/lib/shared/pxToRem.test.ts +7 -6
  271. package/lib/shared/pxToRem.ts +2 -5
  272. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
  273. package/lib/shared/registry/ClientRegistry.ts +0 -2
  274. package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
  275. package/lib/shared/registry/ComponentRegistry.ts +12 -9
  276. package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
  277. package/lib/shared/registry/RegistryManager.ts +1 -2
  278. package/lib/shared/registry/SSRRegistry.ts +0 -2
  279. package/lib/shared/registry/createNodeType.ts +7 -9
  280. package/lib/shared/registry/defineNodeType.ts +2 -6
  281. package/lib/shared/registry/fieldPresets.ts +3 -1
  282. package/lib/shared/registry/index.ts +0 -1
  283. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  284. package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
  285. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  286. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
  287. package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
  288. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  289. package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
  290. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  291. package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
  292. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
  293. package/lib/shared/registry/nodeTypes/index.ts +26 -3
  294. package/lib/shared/responsiveScaling.test.ts +16 -34
  295. package/lib/shared/responsiveScaling.ts +61 -42
  296. package/lib/shared/responsiveStyleUtils.test.ts +0 -1
  297. package/lib/shared/responsiveStyleUtils.ts +11 -13
  298. package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
  299. package/lib/shared/richtext/htmlToTiptap.ts +2 -4
  300. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  301. package/lib/shared/richtext/types.ts +1 -8
  302. package/lib/shared/slugTranslator.test.ts +13 -13
  303. package/lib/shared/slugTranslator.ts +12 -16
  304. package/lib/shared/slugify.ts +9 -15
  305. package/lib/shared/styleNodeUtils.test.ts +8 -8
  306. package/lib/shared/styleNodeUtils.ts +18 -20
  307. package/lib/shared/styleUtils.test.ts +87 -61
  308. package/lib/shared/styleUtils.ts +5 -6
  309. package/lib/shared/themeDefaults.test.ts +11 -14
  310. package/lib/shared/themeDefaults.ts +3 -4
  311. package/lib/shared/tree/PathBuilder.test.ts +63 -145
  312. package/lib/shared/tree/PathBuilder.ts +59 -47
  313. package/lib/shared/treePathUtils.test.ts +51 -12
  314. package/lib/shared/treePathUtils.ts +91 -60
  315. package/lib/shared/types/api.ts +105 -8
  316. package/lib/shared/types/cms.test.ts +34 -0
  317. package/lib/shared/types/cms.ts +90 -34
  318. package/lib/shared/types/comment.ts +49 -16
  319. package/lib/shared/types/components.ts +51 -25
  320. package/lib/shared/types/errors.test.ts +1 -6
  321. package/lib/shared/types/errors.ts +3 -7
  322. package/lib/shared/types/experiments.ts +28 -28
  323. package/lib/shared/types/index.ts +25 -2
  324. package/lib/shared/types/permissions.ts +139 -0
  325. package/lib/shared/types/styles.ts +0 -1
  326. package/lib/shared/types/variables.test.ts +4 -13
  327. package/lib/shared/types/variables.ts +51 -30
  328. package/lib/shared/types.ts +1 -2
  329. package/lib/shared/utilityClassConfig.ts +751 -321
  330. package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
  331. package/lib/shared/utilityClassMapper.test.ts +433 -95
  332. package/lib/shared/utilityClassMapper.ts +322 -249
  333. package/lib/shared/utilityClassNames.test.ts +38 -0
  334. package/lib/shared/utilityClassNames.ts +458 -0
  335. package/lib/shared/utils.test.ts +2 -10
  336. package/lib/shared/utils.ts +19 -10
  337. package/lib/shared/validation/cmsValidators.ts +2 -1
  338. package/lib/shared/validation/commentValidators.test.ts +53 -0
  339. package/lib/shared/validation/commentValidators.ts +12 -1
  340. package/lib/shared/validation/index.ts +1 -0
  341. package/lib/shared/validation/permissionsValidators.test.ts +49 -0
  342. package/lib/shared/validation/permissionsValidators.ts +49 -0
  343. package/lib/shared/validation/propValidator.test.ts +19 -21
  344. package/lib/shared/validation/propValidator.ts +13 -18
  345. package/lib/shared/validation/schemas.test.ts +64 -32
  346. package/lib/shared/validation/schemas.ts +616 -345
  347. package/lib/shared/validation/validators.test.ts +3 -8
  348. package/lib/shared/validation/validators.ts +89 -68
  349. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  350. package/lib/shared/viewportUnits.test.ts +24 -23
  351. package/lib/shared/viewportUnits.ts +12 -5
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
  354. package/lib/test-utils/factories/DomMockFactory.ts +17 -23
  355. package/lib/test-utils/factories/EventMockFactory.ts +7 -13
  356. package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
  357. package/lib/test-utils/factories/ServerMockFactory.ts +6 -10
  358. package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
  359. package/lib/test-utils/fixtures.ts +45 -45
  360. package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
  361. package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
  362. package/lib/test-utils/helpers.ts +1 -5
  363. package/lib/test-utils/index.ts +0 -4
  364. package/lib/test-utils/mockFactories.ts +13 -19
  365. package/lib/test-utils/mocks.ts +6 -4
  366. package/package.json +3 -5
  367. package/scripts/build-for-publish.mjs +2 -4
  368. package/scripts/build-meno-filter.ts +4 -7
  369. package/tsconfig.json +1 -0
  370. package/vite.config.ts +4 -4
  371. package/build-next.ts +0 -1361
  372. package/build-static.test.ts +0 -424
  373. package/build-static.ts +0 -1074
  374. package/dist/build-static.js +0 -36
  375. package/dist/chunks/chunk-2MHDV5BF.js +0 -251
  376. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  377. package/dist/chunks/chunk-AZQYF6KE.js +0 -559
  378. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  379. package/dist/chunks/chunk-GZHGVVW3.js +0 -322
  380. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  381. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  382. package/dist/chunks/chunk-I7YIGZXT.js +0 -467
  383. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  384. package/dist/chunks/chunk-IGYR22T6.js +0 -6192
  385. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  386. package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
  387. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  388. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  389. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  390. package/dist/chunks/chunk-QB2LNO4W.js +0 -77
  391. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  392. package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
  393. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  394. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  395. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  396. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  397. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  398. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  399. package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
  400. package/dist/chunks/constants-STK2YBIW.js +0 -46
  401. package/dist/chunks/constants-STK2YBIW.js.map +0 -7
  402. package/dist/chunks/fs-JGINUXGL.js.map +0 -7
  403. package/dist/entries/server-router.js +0 -67
  404. package/dist/entries/server-router.js.map +0 -7
  405. package/entries/server-router.tsx +0 -81
  406. package/lib/server/providers/fileSystemPageProvider.ts +0 -160
  407. package/lib/server/webflow/buildWebflow.ts +0 -635
  408. package/lib/server/webflow/index.ts +0 -23
  409. package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
  410. package/lib/server/webflow/nodeToWebflow.ts +0 -2435
  411. package/lib/server/webflow/styleMapper.test.ts +0 -389
  412. package/lib/server/webflow/styleMapper.ts +0 -695
  413. package/lib/server/webflow/templateWrapper.ts +0 -49
  414. package/lib/server/webflow/types.ts +0 -396
  415. /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
@@ -1,3821 +0,0 @@
1
- import {
2
- CachedConfigLoader,
3
- colorService,
4
- formatJsonErrorMessage,
5
- generateFontCSS,
6
- generateSSRHTML,
7
- generateVariablesCSS,
8
- loadBreakpointConfig,
9
- loadComponentDirectory,
10
- loadI18nConfig,
11
- loadJSONFile,
12
- loadProjectConfig,
13
- mapPageNameToPath,
14
- parseJSON,
15
- variableService
16
- } from "./chunk-IGYR22T6.js";
17
- import {
18
- configService
19
- } from "./chunk-2MHDV5BF.js";
20
- import {
21
- bundleFile,
22
- createRuntimeServer,
23
- getPackageRoot,
24
- getProjectRoot,
25
- packagePaths,
26
- projectPaths,
27
- resolvePackagePath,
28
- resolveProjectPath
29
- } from "./chunk-I7YIGZXT.js";
30
- import {
31
- fileExists,
32
- hashContent,
33
- readTextFile,
34
- serveFile,
35
- writeFile
36
- } from "./chunk-WQFG7PAH.js";
37
- import {
38
- extractStringValue,
39
- rewriteComponentRefs
40
- } from "./chunk-QB2LNO4W.js";
41
- import {
42
- buildSlugIndex,
43
- isPathWithinRoot,
44
- resolveSlugToPageId
45
- } from "./chunk-X754AHS5.js";
46
- import {
47
- addItemUrls,
48
- toFriendlyError
49
- } from "./chunk-JGWFTO6P.js";
50
- import {
51
- parseLocaleFromPath
52
- } from "./chunk-AZQYF6KE.js";
53
- import {
54
- API_ROUTES,
55
- HMR_ROUTE,
56
- MAX_PORT_ATTEMPTS,
57
- SERVER_PORT,
58
- WEBSOCKET_STATES,
59
- init_constants
60
- } from "./chunk-YBLHKYFF.js";
61
-
62
- // lib/server/pageCache.ts
63
- var PageCache = class {
64
- cache = /* @__PURE__ */ new Map();
65
- /**
66
- * Store page content and line map in cache
67
- */
68
- set(path2, content, lineMap) {
69
- this.cache.set(path2, { content, lineMap });
70
- }
71
- /**
72
- * Get cached page data
73
- */
74
- get(path2) {
75
- return this.cache.get(path2);
76
- }
77
- /**
78
- * Get page content from cache
79
- */
80
- getContent(path2) {
81
- return this.cache.get(path2)?.content;
82
- }
83
- /**
84
- * Get line map from cache
85
- */
86
- getLineMap(path2) {
87
- return this.cache.get(path2)?.lineMap;
88
- }
89
- /**
90
- * Get line range for a specific element path
91
- */
92
- getLineRange(pagePath, elementPath) {
93
- return this.cache.get(pagePath)?.lineMap.get(elementPath);
94
- }
95
- /**
96
- * Delete page from cache
97
- */
98
- delete(path2) {
99
- this.cache.delete(path2);
100
- }
101
- /**
102
- * Check if page exists in cache
103
- */
104
- has(path2) {
105
- return this.cache.has(path2);
106
- }
107
- /**
108
- * Get all page paths
109
- */
110
- keys() {
111
- return Array.from(this.cache.keys());
112
- }
113
- /**
114
- * Clear all cached pages
115
- */
116
- clear() {
117
- this.cache.clear();
118
- }
119
- /**
120
- * Get cache size
121
- */
122
- size() {
123
- return this.cache.size;
124
- }
125
- /**
126
- * Get all pages content as object (without line maps)
127
- */
128
- toObject() {
129
- const obj = {};
130
- this.cache.forEach((value, key) => {
131
- obj[key] = value.content;
132
- });
133
- return obj;
134
- }
135
- };
136
-
137
- // lib/server/websocketManager.ts
138
- init_constants();
139
- var WebSocketManager = class {
140
- clients = /* @__PURE__ */ new Set();
141
- /**
142
- * Add a WebSocket client
143
- */
144
- addClient(ws) {
145
- this.clients.add(ws);
146
- }
147
- /**
148
- * Remove a WebSocket client
149
- */
150
- removeClient(ws) {
151
- this.clients.delete(ws);
152
- }
153
- /**
154
- * Broadcast message to all connected clients
155
- */
156
- broadcast(message) {
157
- const messageStr = typeof message === "string" ? message : JSON.stringify(message);
158
- for (const ws of this.clients) {
159
- if (ws.readyState === WEBSOCKET_STATES.OPEN) {
160
- ws.send(messageStr);
161
- }
162
- }
163
- }
164
- /**
165
- * Broadcast update notification
166
- */
167
- broadcastUpdate(path2) {
168
- this.broadcast({
169
- type: "hmr:update",
170
- path: path2 || "all"
171
- });
172
- }
173
- /**
174
- * Broadcast colors/themes update notification
175
- */
176
- broadcastColorsUpdate() {
177
- this.broadcast({
178
- type: "hmr:colors-update"
179
- });
180
- }
181
- /**
182
- * Broadcast variables update notification
183
- */
184
- broadcastVariablesUpdate() {
185
- this.broadcast({
186
- type: "hmr:variables-update"
187
- });
188
- }
189
- /**
190
- * Broadcast fonts update notification
191
- */
192
- broadcastFontsUpdate() {
193
- this.broadcast({
194
- type: "hmr:fonts-update"
195
- });
196
- }
197
- /**
198
- * Broadcast libraries update notification
199
- */
200
- broadcastLibrariesUpdate() {
201
- this.broadcast({
202
- type: "hmr:libraries-update"
203
- });
204
- }
205
- /**
206
- * Broadcast enums update notification
207
- */
208
- broadcastEnumsUpdate() {
209
- this.broadcast({
210
- type: "hmr:enums-update"
211
- });
212
- }
213
- /**
214
- * Broadcast CMS content update notification
215
- */
216
- broadcastCMSUpdate(collection) {
217
- this.broadcast({
218
- type: "hmr:cms-update",
219
- collection
220
- });
221
- }
222
- /**
223
- * Broadcast CMS collections-list update notification.
224
- * Emitted when a template file is added, removed, or its schema changes —
225
- * tells connected clients to re-fetch the collections list.
226
- */
227
- broadcastCollectionsUpdate() {
228
- this.broadcast({
229
- type: "hmr:cms-collections-update"
230
- });
231
- }
232
- /**
233
- * Broadcast project.config.json update notification.
234
- * Emitted when project.config.json changes (e.g. an AI tool adds a new
235
- * locale) — tells connected clients to re-fetch config-derived state
236
- * such as the i18n locale list.
237
- */
238
- broadcastConfigUpdate() {
239
- this.broadcast({
240
- type: "hmr:config-update"
241
- });
242
- }
243
- /**
244
- * Get number of connected clients
245
- */
246
- getClientCount() {
247
- return this.clients.size;
248
- }
249
- /**
250
- * Clear all clients
251
- */
252
- clear() {
253
- this.clients.clear();
254
- }
255
- /**
256
- * Get all clients
257
- */
258
- getClients() {
259
- return new Set(this.clients);
260
- }
261
- /**
262
- * Check if any clients are connected
263
- */
264
- hasClients() {
265
- return this.clients.size > 0;
266
- }
267
- };
268
-
269
- // lib/server/utils/jsonLineMapper.ts
270
- function buildLineMap(jsonText) {
271
- const lineMap = /* @__PURE__ */ new Map();
272
- const lines = jsonText.split("\n");
273
- const charToLine = [];
274
- let currentLine = 1;
275
- for (let i = 0; i < jsonText.length; i++) {
276
- charToLine[i] = currentLine;
277
- if (jsonText[i] === "\n") {
278
- currentLine++;
279
- }
280
- }
281
- let pos = 0;
282
- function skipWhitespace() {
283
- while (pos < jsonText.length && /\s/.test(jsonText[pos])) {
284
- pos++;
285
- }
286
- }
287
- function parseString() {
288
- if (jsonText[pos] !== '"') return "";
289
- pos++;
290
- let result = "";
291
- while (pos < jsonText.length && jsonText[pos] !== '"') {
292
- if (jsonText[pos] === "\\") {
293
- pos++;
294
- if (pos < jsonText.length) {
295
- result += jsonText[pos];
296
- pos++;
297
- }
298
- } else {
299
- result += jsonText[pos];
300
- pos++;
301
- }
302
- }
303
- pos++;
304
- return result;
305
- }
306
- function parseNumber() {
307
- while (pos < jsonText.length && /[\d.eE\-+]/.test(jsonText[pos])) {
308
- pos++;
309
- }
310
- }
311
- function parseValue(path2, trackChildren) {
312
- skipWhitespace();
313
- const startPos = pos;
314
- if (jsonText[pos] === "{") {
315
- parseObject(path2, trackChildren);
316
- } else if (jsonText[pos] === "[") {
317
- parseArray(path2, trackChildren);
318
- } else if (jsonText[pos] === '"') {
319
- parseString();
320
- } else if (jsonText[pos] === "t") {
321
- pos += 4;
322
- } else if (jsonText[pos] === "f") {
323
- pos += 5;
324
- } else if (jsonText[pos] === "n") {
325
- pos += 4;
326
- } else {
327
- parseNumber();
328
- }
329
- return { start: startPos, end: pos };
330
- }
331
- function recordTrackedRoot(path2) {
332
- const { start, end } = parseValue(path2, true);
333
- lineMap.set("", {
334
- startLine: charToLine[start],
335
- endLine: charToLine[end - 1] || charToLine[start]
336
- });
337
- }
338
- function parseObject(path2, trackChildren) {
339
- pos++;
340
- skipWhitespace();
341
- while (pos < jsonText.length && jsonText[pos] !== "}") {
342
- skipWhitespace();
343
- const key = parseString();
344
- skipWhitespace();
345
- pos++;
346
- skipWhitespace();
347
- if (key === "root" && path2.length === 0) {
348
- recordTrackedRoot(path2);
349
- } else if (key === "component" && path2.length === 0) {
350
- parseComponentWrapper(path2);
351
- } else if (key === "children" && trackChildren) {
352
- parseValue(path2, true);
353
- } else {
354
- parseValue(path2, false);
355
- }
356
- skipWhitespace();
357
- if (jsonText[pos] === ",") pos++;
358
- skipWhitespace();
359
- }
360
- pos++;
361
- }
362
- function parseComponentWrapper(path2) {
363
- if (jsonText[pos] !== "{") {
364
- parseValue(path2, false);
365
- return;
366
- }
367
- pos++;
368
- skipWhitespace();
369
- while (pos < jsonText.length && jsonText[pos] !== "}") {
370
- skipWhitespace();
371
- const key = parseString();
372
- skipWhitespace();
373
- pos++;
374
- skipWhitespace();
375
- if (key === "structure") {
376
- recordTrackedRoot(path2);
377
- } else {
378
- parseValue(path2, false);
379
- }
380
- skipWhitespace();
381
- if (jsonText[pos] === ",") pos++;
382
- skipWhitespace();
383
- }
384
- pos++;
385
- }
386
- function parseArray(path2, trackChildren) {
387
- pos++;
388
- skipWhitespace();
389
- let index = 0;
390
- while (pos < jsonText.length && jsonText[pos] !== "]") {
391
- const childPath = [...path2, index];
392
- const { start, end } = parseValue(childPath, trackChildren);
393
- if (trackChildren) {
394
- const pathStr = childPath.join(",");
395
- lineMap.set(pathStr, {
396
- startLine: charToLine[start],
397
- endLine: charToLine[end - 1] || charToLine[start]
398
- });
399
- }
400
- index++;
401
- skipWhitespace();
402
- if (jsonText[pos] === ",") pos++;
403
- skipWhitespace();
404
- }
405
- pos++;
406
- }
407
- try {
408
- skipWhitespace();
409
- parseValue([], false);
410
- } catch (e) {
411
- console.error("Failed to build line map:", e);
412
- }
413
- return lineMap;
414
- }
415
- function lineMapToObject(lineMap) {
416
- const obj = {};
417
- lineMap.forEach((value, key) => {
418
- obj[key] = value;
419
- });
420
- return obj;
421
- }
422
-
423
- // lib/server/services/pageService.ts
424
- import { existsSync, readdirSync, mkdirSync, rmdirSync } from "fs";
425
- import { join } from "path";
426
- var PageService = class {
427
- /**
428
- * Creates a new PageService instance
429
- *
430
- * @param pageCache - PageCache instance to use for caching pages
431
- * @param provider - Optional PageProvider for loading pages (enables DI for testing/CMS)
432
- */
433
- constructor(pageCache, provider) {
434
- this.pageCache = pageCache;
435
- this.provider = provider;
436
- }
437
- /**
438
- * Set the page provider
439
- * Allows setting provider after construction for backward compatibility
440
- *
441
- * @param provider - PageProvider implementation
442
- */
443
- setProvider(provider) {
444
- this.provider = provider;
445
- }
446
- /**
447
- * Load all pages from the data provider
448
- *
449
- * Uses the provider to load pages. If no provider is set, does nothing.
450
- * Pages are cached in memory for fast access.
451
- *
452
- * @returns Promise that resolves when all pages are loaded
453
- *
454
- * @example
455
- * ```typescript
456
- * await pageService.loadAllPages();
457
- * const paths = pageService.getAllPagePaths();
458
- * ```
459
- */
460
- async loadAllPages() {
461
- if (!this.provider) {
462
- return;
463
- }
464
- const pages = await this.provider.loadAll();
465
- for (const [path2, content] of pages) {
466
- const lineMap = buildLineMap(content);
467
- this.pageCache.set(path2, content, lineMap);
468
- }
469
- }
470
- /**
471
- * Get page content from cache
472
- *
473
- * @param path - Page path (e.g., "/" or "/about")
474
- * @returns Raw JSON string content, or undefined if page not found
475
- *
476
- * @example
477
- * ```typescript
478
- * const content = pageService.getPage('/');
479
- * if (content) {
480
- * const data = JSON.parse(content);
481
- * }
482
- * ```
483
- */
484
- getPage(path2) {
485
- return this.pageCache.getContent(path2);
486
- }
487
- /**
488
- * Get parsed page data
489
- *
490
- * Retrieves page content from cache and parses it as JSON. Returns null if
491
- * the page doesn't exist or if parsing fails.
492
- *
493
- * @param path - Page path (e.g., "/" or "/about")
494
- * @returns Parsed JSONPage object, or null if not found or invalid
495
- *
496
- * @example
497
- * ```typescript
498
- * const pageData = pageService.getPageData('/');
499
- * if (pageData) {
500
- * console.log(pageData.meta.title);
501
- * }
502
- * ```
503
- */
504
- getPageData(path2) {
505
- const content = this.pageCache.getContent(path2);
506
- if (!content) {
507
- return null;
508
- }
509
- try {
510
- return parseJSON(content);
511
- } catch {
512
- return null;
513
- }
514
- }
515
- /**
516
- * Save page content
517
- *
518
- * Saves page data via the provider and updates the cache. The page path
519
- * is converted to a filename (e.g., "/" -> "index.json", "/about" -> "about.json").
520
- *
521
- * @param path - Page path (e.g., "/" or "/about")
522
- * @param data - JSONPage object to save
523
- * @returns Promise that resolves when the page is saved
524
- *
525
- * @throws {Error} If provider is not set or file write fails
526
- *
527
- * @example
528
- * ```typescript
529
- * await pageService.savePage('/', {
530
- * meta: { title: 'Home' },
531
- * root: { type: 'node', tag: 'div', children: [] }
532
- * });
533
- * ```
534
- */
535
- async savePage(path2, data) {
536
- if (!this.provider) {
537
- throw new Error("PageProvider not set");
538
- }
539
- const { _lineMap, ...dataToSave } = data;
540
- const content = JSON.stringify(dataToSave, null, 2);
541
- await this.provider.save(path2, content);
542
- const lineMap = buildLineMap(content);
543
- this.pageCache.set(path2, content, lineMap);
544
- }
545
- /**
546
- * Re-read a single page from disk via the provider and refresh the cache.
547
- *
548
- * Used by the file watcher for live reload. Provider-based (not a hardcoded
549
- * JSON read), so it works for any format — the FileSystem provider returns the
550
- * raw JSON and the Astro provider returns the parsed model serialized to JSON.
551
- *
552
- * @returns true if the cache changed (page updated or confirmed-deleted) and a
553
- * reload broadcast is warranted; false if the read failed transiently (e.g. a
554
- * partial mid-write or parse error), in which case the cache is left intact and
555
- * the next watcher event retries.
556
- */
557
- async reloadPageFromDisk(path2) {
558
- if (!this.provider) return false;
559
- let content = null;
560
- try {
561
- content = await this.provider.get(path2);
562
- } catch {
563
- return false;
564
- }
565
- if (content) {
566
- this.pageCache.set(path2, content, buildLineMap(content));
567
- return true;
568
- }
569
- try {
570
- if (!await this.provider.exists(path2)) {
571
- this.pageCache.delete(path2);
572
- return true;
573
- }
574
- } catch {
575
- }
576
- return false;
577
- }
578
- /**
579
- * Delete page from cache and optionally from storage
580
- *
581
- * Removes a page from the in-memory cache. If provider is set,
582
- * also deletes from the underlying storage.
583
- *
584
- * @param path - Page path to delete
585
- * @param deleteFromStorage - If true and provider is set, also delete from storage
586
- *
587
- * @example
588
- * ```typescript
589
- * pageService.deletePage('/old-page');
590
- * // Or with storage deletion:
591
- * await pageService.deletePage('/old-page', true);
592
- * ```
593
- */
594
- async deletePage(path2, deleteFromStorage = false) {
595
- this.pageCache.delete(path2);
596
- if (deleteFromStorage && this.provider) {
597
- await this.provider.delete(path2);
598
- }
599
- }
600
- /**
601
- * Get all page paths
602
- *
603
- * Returns an array of all page paths currently in the cache.
604
- *
605
- * @returns Array of page paths (e.g., ["/", "/about", "/contact"])
606
- *
607
- * @example
608
- * ```typescript
609
- * const paths = pageService.getAllPagePaths();
610
- * console.log(`Loaded ${paths.length} pages`);
611
- * ```
612
- */
613
- getAllPagePaths() {
614
- return this.pageCache.keys();
615
- }
616
- /**
617
- * Get all pages with their info (including draft status)
618
- *
619
- * Returns an array of page info objects including path and draft status.
620
- *
621
- * @returns Array of page info objects
622
- *
623
- * @example
624
- * ```typescript
625
- * const pages = pageService.getAllPagesWithInfo();
626
- * // [{ path: "/", isDraft: false }, { path: "/about", isDraft: true }]
627
- * ```
628
- */
629
- getAllPagesWithInfo() {
630
- const paths = this.pageCache.keys().filter((p) => !p.startsWith("/_sketch/"));
631
- return paths.map((path2) => {
632
- const pageData = this.getPageData(path2);
633
- const pathWithoutSlash = path2 === "/" ? "index" : path2.substring(1);
634
- const slashIndex = pathWithoutSlash.lastIndexOf("/");
635
- const folder = slashIndex > 0 ? pathWithoutSlash.substring(0, slashIndex) : void 0;
636
- return {
637
- path: path2,
638
- isDraft: pageData?.meta?.draft === true,
639
- ...folder ? { folder } : {}
640
- };
641
- });
642
- }
643
- /**
644
- * Get line map for a page
645
- *
646
- * Returns the line number mapping for elements in the page JSON.
647
- *
648
- * @param path - Page path (e.g., "/" or "/about")
649
- * @returns LineMap object or undefined if page not found
650
- */
651
- getLineMap(path2) {
652
- return this.pageCache.getLineMap(path2);
653
- }
654
- /**
655
- * Pages base directory — provider-aware (e.g. `src/pages` for astro projects),
656
- * falling back to `projectPaths.pages()` when the provider doesn't expose
657
- * `baseDir()` (or no provider is set). Used by the folder/move/rename ops that
658
- * touch the filesystem directly.
659
- */
660
- pagesBaseDir() {
661
- return this.provider?.baseDir?.() ?? projectPaths.pages();
662
- }
663
- /** Page file extension (e.g. `.astro` for astro projects), defaulting to `.json`. */
664
- pageExt() {
665
- return this.provider?.extension?.() ?? ".json";
666
- }
667
- /**
668
- * Get all page folders
669
- *
670
- * Scans the pages directory for subdirectories.
671
- *
672
- * @returns Sorted array of folder names
673
- */
674
- getAllFolders() {
675
- const pagesDir = this.pagesBaseDir();
676
- if (!existsSync(pagesDir)) {
677
- return [];
678
- }
679
- const collectFolders = (dir, prefix) => {
680
- try {
681
- const entries = readdirSync(dir, { withFileTypes: true });
682
- const result = [];
683
- for (const entry of entries) {
684
- if (!entry.isDirectory()) continue;
685
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
686
- result.push(relativePath);
687
- result.push(...collectFolders(join(dir, entry.name), relativePath));
688
- }
689
- return result;
690
- } catch {
691
- return [];
692
- }
693
- };
694
- return collectFolders(pagesDir, "").sort();
695
- }
696
- /**
697
- * Create a page folder
698
- *
699
- * Creates an empty folder in the pages directory.
700
- *
701
- * @param folderName - Name of the folder to create
702
- * @throws {Error} If folder name is invalid or already exists
703
- */
704
- async createFolder(folderName) {
705
- if (!folderName || folderName.trim() === "") {
706
- throw new Error("Folder name cannot be empty");
707
- }
708
- const trimmed = folderName.trim();
709
- const segments = trimmed.split("/");
710
- const segmentRegex = /^[a-z0-9][a-z0-9-_]*$/;
711
- for (const segment of segments) {
712
- if (!segment) throw new Error("Folder path cannot have empty segments");
713
- if (!segmentRegex.test(segment)) {
714
- throw new Error("Each folder segment can only contain lowercase letters, numbers, dashes, and underscores");
715
- }
716
- }
717
- const pagesDir = this.pagesBaseDir();
718
- const folderPath = join(pagesDir, trimmed);
719
- if (existsSync(folderPath)) {
720
- throw new Error("Folder already exists");
721
- }
722
- mkdirSync(folderPath, { recursive: true });
723
- }
724
- /**
725
- * Move a page to a different folder
726
- *
727
- * Moves a page file from its current location to the target folder.
728
- * If folder is empty string, moves to root pages directory.
729
- *
730
- * @param pagePath - Current page path (e.g., "/blog/post" or "/about")
731
- * @param newFolder - Target folder name (empty string for root)
732
- */
733
- async movePage(pagePath, newFolder) {
734
- const { rename } = await import("fs/promises");
735
- const pagesDir = this.pagesBaseDir();
736
- const ext = this.pageExt();
737
- const pageName = pagePath === "/" ? "index" : pagePath.substring(1);
738
- const slashIndex = pageName.lastIndexOf("/");
739
- const baseName = slashIndex >= 0 ? pageName.substring(slashIndex + 1) : pageName;
740
- const sourceFile = join(pagesDir, `${pageName}${ext}`);
741
- const targetFile = newFolder ? join(pagesDir, newFolder, `${baseName}${ext}`) : join(pagesDir, `${baseName}${ext}`);
742
- if (newFolder) {
743
- const targetDir = join(pagesDir, newFolder);
744
- if (!existsSync(targetDir)) {
745
- mkdirSync(targetDir, { recursive: true });
746
- }
747
- }
748
- if (!existsSync(sourceFile)) {
749
- throw new Error(`Page not found: ${pagePath}`);
750
- }
751
- await rename(sourceFile, targetFile);
752
- const content = this.pageCache.getContent(pagePath);
753
- if (content) {
754
- this.pageCache.delete(pagePath);
755
- const newPageName = newFolder ? `${newFolder}/${baseName}` : baseName;
756
- const newPath = newPageName === "index" ? "/" : `/${newPageName}`;
757
- const lineMap = buildLineMap(content);
758
- this.pageCache.set(newPath, content, lineMap);
759
- }
760
- const sourceDir = slashIndex >= 0 ? join(pagesDir, pageName.substring(0, slashIndex)) : null;
761
- if (sourceDir && sourceDir !== pagesDir) {
762
- try {
763
- const remaining = readdirSync(sourceDir);
764
- if (remaining.length === 0) {
765
- rmdirSync(sourceDir);
766
- }
767
- } catch {
768
- }
769
- }
770
- }
771
- /**
772
- * Rename a page (file rename + cache update).
773
- *
774
- * Renames the underlying .json file from `oldPath`'s filename to `newPath`'s
775
- * filename, keeping the same folder. Updates the in-memory cache so the
776
- * page is now keyed at `newPath`. Folder moves stay with `movePage()` —
777
- * this is purely "change the URL slug of an existing page" for
778
- * single-locale projects.
779
- *
780
- * @param oldPath - Current page path (e.g., "/about" or "/blog/post")
781
- * @param newPath - New page path with same folder (e.g., "/about-us" or "/blog/new-slug")
782
- * @throws {Error} If oldPath doesn't exist, newPath already exists, or paths
783
- * reference different folders.
784
- */
785
- async renamePage(oldPath, newPath) {
786
- if (oldPath === newPath) return;
787
- if (newPath === "/") {
788
- throw new Error("Cannot rename a page to the index path");
789
- }
790
- if (!newPath.startsWith("/")) {
791
- throw new Error('newPath must start with "/"');
792
- }
793
- const oldName = oldPath === "/" ? "index" : oldPath.substring(1);
794
- const newName = newPath.substring(1);
795
- const oldSlash = oldName.lastIndexOf("/");
796
- const newSlash = newName.lastIndexOf("/");
797
- const oldFolder = oldSlash >= 0 ? oldName.substring(0, oldSlash) : "";
798
- const newFolder = newSlash >= 0 ? newName.substring(0, newSlash) : "";
799
- if (oldFolder !== newFolder) {
800
- throw new Error("renamePage only changes the filename; use movePage to change folder");
801
- }
802
- const baseSegment = newSlash >= 0 ? newName.substring(newSlash + 1) : newName;
803
- if (!/^[a-z0-9][a-z0-9-_]*$/.test(baseSegment)) {
804
- throw new Error("Page slug can only contain lowercase letters, numbers, dashes, and underscores");
805
- }
806
- const { rename } = await import("fs/promises");
807
- const pagesDir = this.pagesBaseDir();
808
- const ext = this.pageExt();
809
- const sourceFile = join(pagesDir, `${oldName}${ext}`);
810
- const targetFile = join(pagesDir, `${newName}${ext}`);
811
- if (!existsSync(sourceFile)) {
812
- throw new Error(`Page not found: ${oldPath}`);
813
- }
814
- if (existsSync(targetFile)) {
815
- throw new Error(`Page already exists: ${newPath}`);
816
- }
817
- await rename(sourceFile, targetFile);
818
- const content = this.pageCache.getContent(oldPath);
819
- if (content) {
820
- this.pageCache.delete(oldPath);
821
- const lineMap = buildLineMap(content);
822
- this.pageCache.set(newPath, content, lineMap);
823
- }
824
- }
825
- /**
826
- * Get slug mappings for all pages
827
- *
828
- * Extracts slug translations from all pages' meta.slugs fields.
829
- * Returns a list of { pageId, slugs } objects for locale-aware routing.
830
- *
831
- * @returns Array of SlugMap objects with pageId and translated slugs
832
- *
833
- * @example
834
- * ```typescript
835
- * const mappings = pageService.getSlugMappings();
836
- * // [{ pageId: "about", slugs: { "en": "about", "pl": "o-nas" } }]
837
- * ```
838
- */
839
- getSlugMappings() {
840
- const mappings = [];
841
- const paths = this.pageCache.keys();
842
- for (const path2 of paths) {
843
- const pageData = this.getPageData(path2);
844
- if (pageData?.meta?.slugs) {
845
- const pageId = path2 === "/" ? "index" : path2.substring(1);
846
- mappings.push({
847
- pageId,
848
- slugs: pageData.meta.slugs
849
- });
850
- } else {
851
- const pageId = path2 === "/" ? "index" : path2.substring(1);
852
- const defaultSlug = path2 === "/" ? "" : path2.substring(1);
853
- mappings.push({
854
- pageId,
855
- slugs: { _default: defaultSlug }
856
- });
857
- }
858
- }
859
- return mappings;
860
- }
861
- };
862
-
863
- // lib/server/services/componentService.ts
864
- import { join as join2 } from "path";
865
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmdirSync as rmdirSync2 } from "fs";
866
- var ComponentService = class {
867
- components = /* @__PURE__ */ new Map();
868
- componentCategories = /* @__PURE__ */ new Map();
869
- fs;
870
- loader;
871
- writer;
872
- loadErrors = /* @__PURE__ */ new Map();
873
- loadWarnings = [];
874
- /**
875
- * Creates a new ComponentService instance
876
- *
877
- * @param options - Optional configuration for dependency injection
878
- * @param options.fs - Optional file system interface for testing
879
- * @param options.loader - Optional component loader interface for testing (READ)
880
- * @param options.writer - Optional component writer for non-JSON on-disk formats
881
- * (WRITE). When omitted, the service writes JSON exactly as before — legacy
882
- * projects are byte-identical. The astro bootstrap injects an AstroComponentWriter
883
- * so saves emit `.astro` files under `src/components/`.
884
- */
885
- constructor(options) {
886
- this.fs = options?.fs;
887
- this.loader = options?.loader;
888
- this.writer = options?.writer;
889
- }
890
- /**
891
- * Base components directory — `<root>/components` for JSON, or whatever the
892
- * injected writer owns (e.g. `<root>/src/components` for astro). All path
893
- * resolution (categories, folders, save/move/rename targets) routes through
894
- * here so a single switch covers every write op.
895
- * @internal
896
- */
897
- componentsBaseDir() {
898
- return this.writer ? this.writer.componentsDir() : projectPaths.components();
899
- }
900
- /**
901
- * Load all components from the components directory
902
- *
903
- * Scans the ./components directory recursively and loads all component definitions.
904
- * Subdirectories are treated as category folders. Clears existing components before loading.
905
- * Components are loaded from .json files, and associated .js and .css files are
906
- * automatically loaded if they exist.
907
- *
908
- * @returns Promise that resolves when all components are loaded
909
- *
910
- * @example
911
- * ```typescript
912
- * await componentService.loadAllComponents();
913
- * const count = Object.keys(componentService.getAllComponents()).length;
914
- * ```
915
- */
916
- async loadAllComponents() {
917
- const nextComponents = /* @__PURE__ */ new Map();
918
- const nextCategories = /* @__PURE__ */ new Map();
919
- const nextErrors = /* @__PURE__ */ new Map();
920
- let nextWarnings = [];
921
- let loadedComponents;
922
- if (this.loader) {
923
- loadedComponents = await this.loader.loadDirectory(projectPaths.components());
924
- } else {
925
- const result = await loadComponentDirectory(projectPaths.components());
926
- loadedComponents = result.components;
927
- nextWarnings = result.warnings;
928
- for (const error of result.errors) {
929
- nextErrors.set(error.componentName, error);
930
- }
931
- }
932
- loadedComponents.forEach((value, key) => {
933
- const componentWithCategory = value;
934
- const category = componentWithCategory._category;
935
- nextCategories.set(key, category);
936
- const { _category, _relativePath, ...cleanDef } = componentWithCategory;
937
- nextComponents.set(key, cleanDef);
938
- });
939
- this.components = nextComponents;
940
- this.componentCategories = nextCategories;
941
- this.loadErrors = nextErrors;
942
- this.loadWarnings = nextWarnings;
943
- }
944
- /**
945
- * Get component by name
946
- *
947
- * @param name - Component name (without .json extension)
948
- * @returns ComponentDefinition if found, undefined otherwise
949
- *
950
- * @example
951
- * ```typescript
952
- * const button = componentService.getComponent('Button');
953
- * if (button) {
954
- * console.log(button.component.interface);
955
- * }
956
- * ```
957
- */
958
- getComponent(name) {
959
- return this.components.get(name);
960
- }
961
- /**
962
- * Check if component exists
963
- *
964
- * @param name - Component name to check
965
- * @returns True if component exists in cache, false otherwise
966
- *
967
- * @example
968
- * ```typescript
969
- * if (componentService.hasComponent('Button')) {
970
- * // Component is available
971
- * }
972
- * ```
973
- */
974
- hasComponent(name) {
975
- return this.components.has(name);
976
- }
977
- /**
978
- * Get all components as a Record
979
- *
980
- * Returns all loaded components as a plain object for easy iteration
981
- * or serialization.
982
- *
983
- * @returns Record mapping component names to ComponentDefinition objects
984
- *
985
- * @example
986
- * ```typescript
987
- * const allComponents = componentService.getAllComponents();
988
- * Object.keys(allComponents).forEach(name => {
989
- * console.log(`Component: ${name}`);
990
- * });
991
- * ```
992
- */
993
- getAllComponents() {
994
- const record = {};
995
- this.components.forEach((value, key) => {
996
- record[key] = value;
997
- });
998
- return record;
999
- }
1000
- /**
1001
- * Get all components with their category information
1002
- *
1003
- * Returns all loaded components with category metadata derived from folder structure.
1004
- *
1005
- * @returns Record mapping component names to ComponentInfo objects (definition + category)
1006
- *
1007
- * @example
1008
- * ```typescript
1009
- * const components = componentService.getAllComponentsWithCategories();
1010
- * Object.entries(components).forEach(([name, info]) => {
1011
- * console.log(`${name}: category=${info.category || 'uncategorized'}`);
1012
- * });
1013
- * ```
1014
- */
1015
- getAllComponentsWithCategories() {
1016
- const record = {};
1017
- this.components.forEach((value, key) => {
1018
- record[key] = {
1019
- definition: value,
1020
- category: this.componentCategories.get(key)
1021
- };
1022
- });
1023
- return record;
1024
- }
1025
- /**
1026
- * Get category for a component
1027
- *
1028
- * @param name - Component name
1029
- * @returns Category string if component is in a category folder, undefined otherwise
1030
- */
1031
- getComponentCategory(name) {
1032
- return this.componentCategories.get(name);
1033
- }
1034
- /**
1035
- * Get load errors keyed by component name
1036
- */
1037
- getLoadErrors() {
1038
- return this.loadErrors;
1039
- }
1040
- /**
1041
- * Get load warnings
1042
- */
1043
- getLoadWarnings() {
1044
- return this.loadWarnings;
1045
- }
1046
- /**
1047
- * Get all load diagnostics
1048
- */
1049
- getLoadDiagnostics() {
1050
- return {
1051
- errors: Array.from(this.loadErrors.values()),
1052
- warnings: this.loadWarnings
1053
- };
1054
- }
1055
- /**
1056
- * Validate that a component has a valid structure
1057
- *
1058
- * @param componentDef - ComponentDefinition to validate
1059
- * @returns true if component has valid structure, false otherwise
1060
- *
1061
- * @example
1062
- * ```typescript
1063
- * const component = componentService.getComponent('Button');
1064
- * if (component && componentService.validateComponentStructure(component)) {
1065
- * // Component has valid structure
1066
- * }
1067
- * ```
1068
- */
1069
- validateComponentStructure(componentDef) {
1070
- if (!componentDef) {
1071
- return false;
1072
- }
1073
- const component = componentDef.component;
1074
- if (!component || typeof component !== "object") {
1075
- return false;
1076
- }
1077
- const structure = component.structure;
1078
- if (!structure) {
1079
- return false;
1080
- }
1081
- if (typeof structure !== "object" || structure === null || Array.isArray(structure)) {
1082
- return false;
1083
- }
1084
- if (!("type" in structure) || typeof structure.type !== "string") {
1085
- return false;
1086
- }
1087
- return true;
1088
- }
1089
- /**
1090
- * Get the directory path for a component based on its category
1091
- * @internal
1092
- */
1093
- getComponentDir(name, category) {
1094
- const componentsDir = this.componentsBaseDir();
1095
- const cat = this.resolveComponentCategory(name, category);
1096
- return cat ? join2(componentsDir, cat) : componentsDir;
1097
- }
1098
- /**
1099
- * Resolve which category a save should land in. Priority:
1100
- * 1. Explicit caller-supplied category (including '' for root)
1101
- * 2. In-memory categories cache
1102
- * 3. Existing file on disk (defense against a stale/empty cache)
1103
- * Falls back to root only when none of the above resolves. The disk
1104
- * scan in step 3 closes the race where a save fires while
1105
- * loadAllComponents has been called but hasn't rebuilt the cache yet.
1106
- * @internal
1107
- */
1108
- resolveComponentCategory(name, category) {
1109
- if (category !== void 0) {
1110
- return category || void 0;
1111
- }
1112
- if (this.componentCategories.has(name)) {
1113
- return this.componentCategories.get(name);
1114
- }
1115
- return this.findComponentCategoryOnDisk(name);
1116
- }
1117
- /**
1118
- * Look on disk for an existing `<name>.json` and return its category.
1119
- * Returns undefined for "exists at root" or "not found anywhere" — both
1120
- * map to writing at root, which is the right default.
1121
- * @internal
1122
- */
1123
- findComponentCategoryOnDisk(name) {
1124
- const componentsDir = this.componentsBaseDir();
1125
- if (!existsSync2(componentsDir)) return void 0;
1126
- const ext = this.writer ? null : ".json";
1127
- const existsAt = (dir) => {
1128
- if (ext) return existsSync2(join2(dir, `${name}${ext}`));
1129
- try {
1130
- return readdirSync2(dir, { withFileTypes: true }).some(
1131
- (e) => e.isFile() && (e.name === `${name}.json` || e.name === `${name}.astro`)
1132
- );
1133
- } catch {
1134
- return false;
1135
- }
1136
- };
1137
- if (existsAt(componentsDir)) return void 0;
1138
- try {
1139
- for (const entry of readdirSync2(componentsDir, { withFileTypes: true })) {
1140
- if (entry.isDirectory() && existsAt(join2(componentsDir, entry.name))) {
1141
- return entry.name;
1142
- }
1143
- }
1144
- } catch {
1145
- }
1146
- return void 0;
1147
- }
1148
- /**
1149
- * Get component JavaScript from .js file
1150
- *
1151
- * Loads the JavaScript code for a component from its .js file.
1152
- * Returns null if the file doesn't exist.
1153
- *
1154
- * @param name - Component name (without .js extension)
1155
- * @returns JavaScript code as string, or null if file doesn't exist
1156
- *
1157
- * @example
1158
- * ```typescript
1159
- * const js = await componentService.getComponentJavaScript('Button');
1160
- * if (js) {
1161
- * console.log('Component has JavaScript');
1162
- * }
1163
- * ```
1164
- */
1165
- async getComponentJavaScript(name) {
1166
- const componentDir = this.getComponentDir(name);
1167
- const jsFilePath = join2(componentDir, `${name}.js`);
1168
- try {
1169
- if (await fileExists(jsFilePath)) {
1170
- return await readTextFile(jsFilePath);
1171
- }
1172
- return null;
1173
- } catch (error) {
1174
- return null;
1175
- }
1176
- }
1177
- /**
1178
- * Save component definition
1179
- *
1180
- * Saves component definition to a .json file and updates the cache.
1181
- * The javascript field is automatically removed from the saved data
1182
- * (JavaScript should only be in .js files).
1183
- *
1184
- * If category is provided, the component is saved to the category subfolder.
1185
- * If category is undefined and component exists, it stays in its current location.
1186
- * If category is empty string '', component is saved to root (uncategorized).
1187
- *
1188
- * @param name - Component name (without .json extension)
1189
- * @param data - ComponentDefinition object to save
1190
- * @param category - Optional category folder name (empty string for root/uncategorized)
1191
- * @returns Promise that resolves when the component is saved
1192
- *
1193
- * @throws {Error} If file write fails
1194
- *
1195
- * @example
1196
- * ```typescript
1197
- * // Save to root (uncategorized)
1198
- * await componentService.saveComponent('Button', buttonDef);
1199
- *
1200
- * // Save to 'ui' category folder
1201
- * await componentService.saveComponent('Card', cardDef, 'ui');
1202
- * ```
1203
- */
1204
- async saveComponent(name, data, category) {
1205
- const dataWithoutJS = JSON.parse(JSON.stringify(data));
1206
- if (dataWithoutJS?.component?.javascript !== void 0) {
1207
- delete dataWithoutJS.component.javascript;
1208
- }
1209
- const targetCategory = this.resolveComponentCategory(name, category);
1210
- const componentsDir = this.componentsBaseDir();
1211
- const componentDir = targetCategory ? join2(componentsDir, targetCategory) : componentsDir;
1212
- if (this.writer) {
1213
- await this.writer.writeComponent(componentDir, name, dataWithoutJS);
1214
- } else {
1215
- const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1216
- if (targetCategory && !existsSync2(componentDir)) {
1217
- mkdirSync2(componentDir, { recursive: true });
1218
- }
1219
- const filePath = join2(componentDir, `${name}.json`);
1220
- await writeFile2(filePath, JSON.stringify(dataWithoutJS, null, 2), "utf-8");
1221
- }
1222
- this.components.set(name, dataWithoutJS);
1223
- this.componentCategories.set(name, targetCategory);
1224
- }
1225
- /**
1226
- * Save component JavaScript to .js file
1227
- *
1228
- * Saves JavaScript code to a component's .js file and reloads the component
1229
- * to update the in-memory cache with the new JavaScript.
1230
- *
1231
- * @param name - Component name (without .js extension)
1232
- * @param javascript - JavaScript code to save
1233
- * @returns Promise that resolves when the JavaScript is saved
1234
- *
1235
- * @throws {Error} If file write fails
1236
- *
1237
- * @example
1238
- * ```typescript
1239
- * await componentService.saveComponentJavaScript('Button', 'console.log("clicked");');
1240
- * ```
1241
- */
1242
- async saveComponentJavaScript(name, javascript) {
1243
- const componentDir = this.getComponentDir(name);
1244
- if (this.writer) {
1245
- const current = this.components.get(name);
1246
- await this.writer.writeJavaScript(componentDir, name, javascript || "", current);
1247
- const next = current ? { ...current, component: { ...current.component, javascript: javascript || "" } } : { component: { javascript: javascript || "" } };
1248
- this.components.set(name, next);
1249
- return;
1250
- }
1251
- const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1252
- const jsFilePath = join2(componentDir, `${name}.js`);
1253
- await writeFile2(jsFilePath, javascript || "", "utf-8");
1254
- const componentPath = join2(componentDir, `${name}.json`);
1255
- const componentData = await loadJSONFile(componentPath);
1256
- if (componentData) {
1257
- const parsed = parseJSON(componentData);
1258
- try {
1259
- if (await fileExists(jsFilePath)) {
1260
- const jsContent = await readTextFile(jsFilePath);
1261
- if (!parsed.component) {
1262
- parsed.component = {};
1263
- }
1264
- parsed.component.javascript = jsContent;
1265
- }
1266
- } catch (error) {
1267
- }
1268
- this.components.set(name, parsed);
1269
- }
1270
- }
1271
- /**
1272
- * Save component CSS to .css file
1273
- *
1274
- * Saves CSS code to a component's .css file and reloads the component
1275
- * to update the in-memory cache with the new CSS.
1276
- *
1277
- * @param name - Component name (without .css extension)
1278
- * @param css - CSS code to save
1279
- * @returns Promise that resolves when the CSS is saved
1280
- *
1281
- * @throws {Error} If file write fails
1282
- *
1283
- * @example
1284
- * ```typescript
1285
- * await componentService.saveComponentCSS('Button', '.button { color: blue; }');
1286
- * ```
1287
- */
1288
- async saveComponentCSS(name, css) {
1289
- const componentDir = this.getComponentDir(name);
1290
- if (this.writer) {
1291
- const current = this.components.get(name);
1292
- await this.writer.writeCSS(componentDir, name, css || "", current);
1293
- const next = current ? { ...current, component: { ...current.component, css: css || "" } } : { component: { css: css || "" } };
1294
- this.components.set(name, next);
1295
- return;
1296
- }
1297
- const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1298
- const cssFilePath = join2(componentDir, `${name}.css`);
1299
- await writeFile2(cssFilePath, css || "", "utf-8");
1300
- const componentPath = join2(componentDir, `${name}.json`);
1301
- const componentData = await loadJSONFile(componentPath);
1302
- if (componentData) {
1303
- const parsed = parseJSON(componentData);
1304
- try {
1305
- if (await fileExists(cssFilePath)) {
1306
- const cssContent = await readTextFile(cssFilePath);
1307
- if (!parsed.component) {
1308
- parsed.component = {};
1309
- }
1310
- parsed.component.css = cssContent;
1311
- }
1312
- } catch (error) {
1313
- }
1314
- this.components.set(name, parsed);
1315
- }
1316
- }
1317
- /**
1318
- * Get all component folders (including empty ones)
1319
- *
1320
- * Scans the components directory for subdirectories.
1321
- * Returns all folder names, whether they contain components or not.
1322
- *
1323
- * @returns Array of folder names
1324
- *
1325
- * @example
1326
- * ```typescript
1327
- * const folders = componentService.getAllFolders();
1328
- * // ['ui', 'layout', 'content']
1329
- * ```
1330
- */
1331
- getAllFolders() {
1332
- const componentsDir = this.componentsBaseDir();
1333
- if (!existsSync2(componentsDir)) {
1334
- return [];
1335
- }
1336
- try {
1337
- const entries = readdirSync2(componentsDir, { withFileTypes: true });
1338
- return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
1339
- } catch {
1340
- return [];
1341
- }
1342
- }
1343
- /**
1344
- * Create a component folder
1345
- *
1346
- * Creates an empty folder in the components directory.
1347
- * Useful for pre-creating category folders before moving components.
1348
- *
1349
- * @param folderName - Name of the folder to create
1350
- * @returns Promise that resolves when folder is created
1351
- *
1352
- * @throws {Error} If folder creation fails
1353
- *
1354
- * @example
1355
- * ```typescript
1356
- * await componentService.createFolder('ui');
1357
- * ```
1358
- */
1359
- async createFolder(folderName) {
1360
- if (!folderName || folderName.trim() === "") {
1361
- throw new Error("Folder name cannot be empty");
1362
- }
1363
- const sanitized = folderName.trim().toLowerCase().replace(/[^a-z0-9-_]/g, "-");
1364
- if (sanitized !== folderName.trim()) {
1365
- throw new Error("Folder name can only contain lowercase letters, numbers, dashes, and underscores");
1366
- }
1367
- const componentsDir = this.componentsBaseDir();
1368
- const folderPath = join2(componentsDir, folderName);
1369
- if (existsSync2(folderPath)) {
1370
- throw new Error("Folder already exists");
1371
- }
1372
- mkdirSync2(folderPath, { recursive: true });
1373
- }
1374
- /**
1375
- * Move component to a different category folder
1376
- *
1377
- * Moves all component files (.json, .js, .css) from current location to new category folder.
1378
- * If newCategory is empty string or undefined, moves to root (uncategorized).
1379
- *
1380
- * @param name - Component name
1381
- * @param newCategory - Target category folder (empty string or undefined for root)
1382
- * @returns Promise that resolves when move is complete
1383
- *
1384
- * @throws {Error} If component doesn't exist or move fails
1385
- *
1386
- * @example
1387
- * ```typescript
1388
- * // Move Button from root to 'ui' category
1389
- * await componentService.moveComponent('Button', 'ui');
1390
- *
1391
- * // Move Card from 'ui' to root (uncategorized)
1392
- * await componentService.moveComponent('Card', '');
1393
- * ```
1394
- */
1395
- async moveComponent(name, newCategory) {
1396
- const currentCategory = this.componentCategories.get(name);
1397
- const targetCategory = newCategory || void 0;
1398
- if (currentCategory === targetCategory) {
1399
- return;
1400
- }
1401
- const componentsDir = this.componentsBaseDir();
1402
- const sourceDir = currentCategory ? join2(componentsDir, currentCategory) : componentsDir;
1403
- const targetDir = targetCategory ? join2(componentsDir, targetCategory) : componentsDir;
1404
- if (this.writer) {
1405
- await this.writer.moveComponent(sourceDir, targetDir, name);
1406
- } else {
1407
- if (targetCategory && !existsSync2(targetDir)) {
1408
- mkdirSync2(targetDir, { recursive: true });
1409
- }
1410
- const extensions = [".json", ".js", ".css"];
1411
- const { rename } = await import("fs/promises");
1412
- for (const ext of extensions) {
1413
- const sourcePath = join2(sourceDir, `${name}${ext}`);
1414
- const targetPath = join2(targetDir, `${name}${ext}`);
1415
- if (existsSync2(sourcePath)) {
1416
- await rename(sourcePath, targetPath);
1417
- }
1418
- }
1419
- }
1420
- this.componentCategories.set(name, targetCategory);
1421
- if (currentCategory) {
1422
- try {
1423
- const remaining = readdirSync2(sourceDir);
1424
- if (remaining.length === 0) {
1425
- rmdirSync2(sourceDir);
1426
- }
1427
- } catch {
1428
- }
1429
- }
1430
- }
1431
- /**
1432
- * Rename a component across the project.
1433
- *
1434
- * Renames the component's `.json`/`.js`/`.css` files on disk (respecting its
1435
- * category folder), updates the in-memory caches, rewrites every
1436
- * `{ type: "component", component: "<oldName>" }` reference inside other
1437
- * component structures, and — if `pageService` is provided — also walks every
1438
- * page/template and rewrites references there, saving any pages that changed.
1439
- *
1440
- * Matching is structural (only `node.component` string equality), not
1441
- * text-based, so it never touches HTML tags, CSS classes, prop values, or
1442
- * embedded markup that happen to contain the name as a substring.
1443
- *
1444
- * @throws {Error} If old/new name is missing, names are identical, new name
1445
- * isn't a valid JS identifier, the source component doesn't exist, or the
1446
- * new name is already taken.
1447
- */
1448
- async renameComponent(oldName, newName, pageService) {
1449
- if (!oldName || typeof oldName !== "string") {
1450
- throw new Error("oldName is required");
1451
- }
1452
- if (!newName || typeof newName !== "string") {
1453
- throw new Error("newName is required");
1454
- }
1455
- const trimmedNew = newName.trim();
1456
- if (oldName === trimmedNew) {
1457
- throw new Error("newName must differ from oldName");
1458
- }
1459
- if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmedNew)) {
1460
- throw new Error("newName must be a valid JavaScript identifier");
1461
- }
1462
- if (!this.components.has(oldName)) {
1463
- throw new Error(`Component "${oldName}" not found`);
1464
- }
1465
- if (this.components.has(trimmedNew)) {
1466
- throw new Error(`Component "${trimmedNew}" already exists`);
1467
- }
1468
- const category = this.componentCategories.get(oldName);
1469
- const componentsDir = this.componentsBaseDir();
1470
- const dir = category ? join2(componentsDir, category) : componentsDir;
1471
- if (this.writer) {
1472
- if (await this.writer.componentExists(dir, trimmedNew)) {
1473
- throw new Error(`Component "${trimmedNew}" already exists in components/${category ?? ""}`);
1474
- }
1475
- await this.writer.moveComponent(dir, dir, oldName, trimmedNew);
1476
- } else {
1477
- for (const ext of [".json", ".js", ".css"]) {
1478
- if (existsSync2(join2(dir, `${trimmedNew}${ext}`))) {
1479
- throw new Error(`File "${trimmedNew}${ext}" already exists in components/${category ?? ""}`);
1480
- }
1481
- }
1482
- const { rename } = await import("fs/promises");
1483
- for (const ext of [".json", ".js", ".css"]) {
1484
- const src = join2(dir, `${oldName}${ext}`);
1485
- const dst = join2(dir, `${trimmedNew}${ext}`);
1486
- if (existsSync2(src)) {
1487
- await rename(src, dst);
1488
- }
1489
- }
1490
- }
1491
- const def = this.components.get(oldName);
1492
- this.components.delete(oldName);
1493
- this.componentCategories.delete(oldName);
1494
- this.components.set(trimmedNew, def);
1495
- this.componentCategories.set(trimmedNew, category);
1496
- let componentRefs = 0;
1497
- const writeFile2 = this.fs ? this.fs.writeFile.bind(this.fs) : (await import("fs/promises")).writeFile;
1498
- for (const [name, otherDef] of this.components.entries()) {
1499
- if (name === trimmedNew) continue;
1500
- const structure = otherDef.component?.structure;
1501
- if (!structure) continue;
1502
- if (rewriteComponentRefs(structure, oldName, trimmedNew)) {
1503
- const otherCategory = this.componentCategories.get(name);
1504
- const otherDir = otherCategory ? join2(componentsDir, otherCategory) : componentsDir;
1505
- const persisted = JSON.parse(JSON.stringify(otherDef));
1506
- if (persisted?.component?.javascript !== void 0) {
1507
- delete persisted.component.javascript;
1508
- }
1509
- if (this.writer) {
1510
- await this.writer.writeComponent(otherDir, name, persisted);
1511
- } else {
1512
- await writeFile2(join2(otherDir, `${name}.json`), JSON.stringify(persisted, null, 2), "utf-8");
1513
- }
1514
- componentRefs++;
1515
- }
1516
- }
1517
- let pageRefs = 0;
1518
- if (pageService) {
1519
- const pagePaths = pageService.getAllPagePaths();
1520
- for (const path2 of pagePaths) {
1521
- const pageData = pageService.getPageData(path2);
1522
- if (!pageData || !("root" in pageData) || !pageData.root) continue;
1523
- if (rewriteComponentRefs(pageData.root, oldName, trimmedNew)) {
1524
- await pageService.savePage(path2, pageData);
1525
- pageRefs++;
1526
- }
1527
- }
1528
- }
1529
- return {
1530
- oldName,
1531
- newName: trimmedNew,
1532
- category,
1533
- componentRefs,
1534
- pageRefs
1535
- };
1536
- }
1537
- };
1538
-
1539
- // lib/server/services/EnumService.ts
1540
- var EnumService = class extends CachedConfigLoader {
1541
- /**
1542
- * Load enums configuration from enums.json
1543
- */
1544
- async loadConfig() {
1545
- return this.load();
1546
- }
1547
- async performLoad() {
1548
- const [enumsContent, configContent] = await Promise.all([
1549
- loadJSONFile(projectPaths.enums()).catch(() => null),
1550
- loadJSONFile(projectPaths.config()).catch(() => null)
1551
- ]);
1552
- if (enumsContent) {
1553
- try {
1554
- const data = parseJSON(enumsContent);
1555
- if (data && typeof data === "object" && !Array.isArray(data)) {
1556
- return this.validateEnums(data);
1557
- }
1558
- } catch (error) {
1559
- console.warn("Failed to parse enums.json:", error);
1560
- }
1561
- }
1562
- if (configContent) {
1563
- try {
1564
- const configData = parseJSON(configContent);
1565
- if (configData && typeof configData === "object" && "enums" in configData) {
1566
- const enums = configData.enums;
1567
- if (enums && typeof enums === "object" && !Array.isArray(enums)) {
1568
- return this.validateEnums(enums);
1569
- }
1570
- }
1571
- } catch {
1572
- }
1573
- }
1574
- return this.getDefaultConfig();
1575
- }
1576
- getDefaultConfig() {
1577
- return {};
1578
- }
1579
- /**
1580
- * Validate that all values are string arrays
1581
- */
1582
- validateEnums(data) {
1583
- const result = {};
1584
- for (const [key, value] of Object.entries(data)) {
1585
- if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
1586
- result[key] = value;
1587
- }
1588
- }
1589
- return result;
1590
- }
1591
- /**
1592
- * Get config with status info (for editor UI)
1593
- */
1594
- async getConfigWithStatus() {
1595
- const filePath = projectPaths.enums();
1596
- try {
1597
- const content = await loadJSONFile(filePath);
1598
- if (!content) {
1599
- const fallbackConfig = await this.performLoad();
1600
- const hasFallback = Object.keys(fallbackConfig).length > 0;
1601
- return {
1602
- status: hasFallback ? "valid" : "missing",
1603
- config: fallbackConfig,
1604
- filePath
1605
- };
1606
- }
1607
- const data = parseJSON(content);
1608
- if (data && typeof data === "object" && !Array.isArray(data)) {
1609
- return { status: "valid", config: this.validateEnums(data), filePath };
1610
- }
1611
- return { status: "invalid", config: this.getDefaultConfig(), error: "Invalid structure: expected Record<string, string[]>", filePath };
1612
- } catch (error) {
1613
- return {
1614
- status: "invalid",
1615
- config: this.getDefaultConfig(),
1616
- error: error instanceof Error ? error.message : "Parse error",
1617
- filePath
1618
- };
1619
- }
1620
- }
1621
- /**
1622
- * Save enums configuration to enums.json
1623
- */
1624
- async saveConfig(config) {
1625
- const enumsPath = projectPaths.enums();
1626
- const content = JSON.stringify(config, null, 2);
1627
- await writeFile(enumsPath, content);
1628
- this.setCache(config);
1629
- }
1630
- };
1631
- var enumService = new EnumService();
1632
-
1633
- // lib/server/fileWatcher.ts
1634
- import { watch, existsSync as existsSync3 } from "fs";
1635
- import { basename, dirname, join as join3 } from "path";
1636
- function attachWhenDirExists(dirPath, attach, setWatcher) {
1637
- if (existsSync3(dirPath)) {
1638
- setWatcher(attach());
1639
- return;
1640
- }
1641
- const parentDir = dirname(dirPath);
1642
- const targetName = basename(dirPath);
1643
- if (!existsSync3(parentDir)) {
1644
- return;
1645
- }
1646
- let parentWatcher = null;
1647
- parentWatcher = watch(parentDir, (_event, filename) => {
1648
- if (filename !== targetName) return;
1649
- if (!existsSync3(dirPath)) return;
1650
- parentWatcher?.close();
1651
- parentWatcher = null;
1652
- setWatcher(attach());
1653
- });
1654
- setWatcher(parentWatcher);
1655
- }
1656
- var FileWatcher = class {
1657
- constructor(callbacks) {
1658
- this.callbacks = callbacks;
1659
- }
1660
- componentsWatcher = null;
1661
- pagesWatcher = null;
1662
- templatesWatcher = null;
1663
- colorsWatcher = null;
1664
- variablesWatcher = null;
1665
- enumsWatcher = null;
1666
- cmsWatcher = null;
1667
- imagesWatcher = null;
1668
- librariesWatcher = null;
1669
- projectConfigWatcher = null;
1670
- // Astro-format projects keep pages/components/CMS under src/ as .astro/.json.
1671
- astroPagesWatcher = null;
1672
- astroComponentsWatcher = null;
1673
- astroContentWatcher = null;
1674
- /**
1675
- * Start watching components directory
1676
- * Watches both .json and .js files to detect component definition and JavaScript changes
1677
- */
1678
- watchComponents(dirPath = projectPaths.components()) {
1679
- if (!existsSync3(dirPath)) {
1680
- return;
1681
- }
1682
- this.componentsWatcher = watch(
1683
- dirPath,
1684
- { recursive: true },
1685
- async (event, filename) => {
1686
- if (filename && (filename.endsWith(".json") || filename.endsWith(".js") || filename.endsWith(".css"))) {
1687
- const fileType = filename.endsWith(".js") ? "JavaScript" : filename.endsWith(".css") ? "CSS" : "component";
1688
- if (this.callbacks.onComponentChange) {
1689
- await this.callbacks.onComponentChange();
1690
- }
1691
- }
1692
- }
1693
- );
1694
- }
1695
- /**
1696
- * Start watching pages directory
1697
- */
1698
- watchPages(dirPath = projectPaths.pages()) {
1699
- if (!existsSync3(dirPath)) {
1700
- return;
1701
- }
1702
- this.pagesWatcher = watch(
1703
- dirPath,
1704
- { recursive: true },
1705
- async (event, filename) => {
1706
- if (filename && filename.endsWith(".json")) {
1707
- const pageName = filename.replace(".json", "");
1708
- const pagePath = mapPageNameToPath(pageName);
1709
- if (this.callbacks.onPageChange) {
1710
- await this.callbacks.onPageChange(pagePath);
1711
- }
1712
- }
1713
- }
1714
- );
1715
- }
1716
- /**
1717
- * Start watching root templates directory for CMS template changes.
1718
- * Falls back to a deferred-attach watcher on the project root when
1719
- * `templates/` doesn't exist yet (blank projects, projects that have never
1720
- * had a CMS collection).
1721
- */
1722
- watchTemplates(dirPath = projectPaths.templates()) {
1723
- attachWhenDirExists(
1724
- dirPath,
1725
- () => watch(
1726
- dirPath,
1727
- { recursive: true },
1728
- async (event, filename) => {
1729
- if (filename && filename.endsWith(".json")) {
1730
- const pageName = `templates/${filename.replace(".json", "")}`;
1731
- const pagePath = mapPageNameToPath(pageName);
1732
- if (this.callbacks.onPageChange) {
1733
- await this.callbacks.onPageChange(pagePath);
1734
- }
1735
- }
1736
- }
1737
- ),
1738
- (w) => {
1739
- this.templatesWatcher = w;
1740
- }
1741
- );
1742
- }
1743
- /**
1744
- * Start watching colors.json file
1745
- */
1746
- watchColors(filePath = projectPaths.colors()) {
1747
- if (!existsSync3(filePath)) {
1748
- return;
1749
- }
1750
- const dirPath = getProjectRoot();
1751
- this.colorsWatcher = watch(
1752
- dirPath,
1753
- { recursive: false },
1754
- async (event, filename) => {
1755
- if (filename === "colors.json" && this.callbacks.onColorsChange) {
1756
- await this.callbacks.onColorsChange();
1757
- }
1758
- }
1759
- );
1760
- }
1761
- /**
1762
- * Start watching variables.json file
1763
- */
1764
- watchVariables() {
1765
- const dirPath = getProjectRoot();
1766
- this.variablesWatcher = watch(
1767
- dirPath,
1768
- { recursive: false },
1769
- async (event, filename) => {
1770
- if (filename === "variables.json" && this.callbacks.onVariablesChange) {
1771
- await this.callbacks.onVariablesChange();
1772
- }
1773
- }
1774
- );
1775
- }
1776
- /**
1777
- * Start watching enums.json file
1778
- */
1779
- watchEnums() {
1780
- const dirPath = getProjectRoot();
1781
- this.enumsWatcher = watch(
1782
- dirPath,
1783
- { recursive: false },
1784
- async (event, filename) => {
1785
- if (filename === "enums.json" && this.callbacks.onEnumsChange) {
1786
- await this.callbacks.onEnumsChange();
1787
- }
1788
- }
1789
- );
1790
- }
1791
- /**
1792
- * Start watching CMS directory.
1793
- * Watches for changes in CMS content files (cms/{collection}/*.json).
1794
- * Falls back to a deferred-attach watcher when `cms/` doesn't exist yet.
1795
- */
1796
- watchCMS(dirPath = projectPaths.cms()) {
1797
- attachWhenDirExists(
1798
- dirPath,
1799
- () => watch(
1800
- dirPath,
1801
- { recursive: true },
1802
- async (event, filename) => {
1803
- if (filename && filename.endsWith(".json")) {
1804
- const collection = filename.split("/")[0];
1805
- if (this.callbacks.onCMSChange) {
1806
- await this.callbacks.onCMSChange(collection);
1807
- }
1808
- }
1809
- }
1810
- ),
1811
- (w) => {
1812
- this.cmsWatcher = w;
1813
- }
1814
- );
1815
- }
1816
- /**
1817
- * Start watching images directory for new source images
1818
- */
1819
- watchImages(dirPath = projectPaths.images()) {
1820
- if (!existsSync3(dirPath)) {
1821
- return;
1822
- }
1823
- this.imagesWatcher = watch(
1824
- dirPath,
1825
- { recursive: false },
1826
- async (event, filename) => {
1827
- if (event === "rename" && filename && this.callbacks.onImageAdded) {
1828
- await this.callbacks.onImageAdded(filename);
1829
- }
1830
- }
1831
- );
1832
- }
1833
- /**
1834
- * Start watching project.config.json for changes.
1835
- * Picks up edits to i18n locales, breakpoints, libraries, icons, etc. so the
1836
- * studio reflects external writes (e.g. an AI tool adding a new locale) without
1837
- * a dev-server restart.
1838
- */
1839
- watchProjectConfig() {
1840
- const dirPath = getProjectRoot();
1841
- this.projectConfigWatcher = watch(
1842
- dirPath,
1843
- { recursive: false },
1844
- async (_event, filename) => {
1845
- if (filename === "project.config.json" && this.callbacks.onProjectConfigChange) {
1846
- await this.callbacks.onProjectConfigChange();
1847
- }
1848
- }
1849
- );
1850
- }
1851
- /**
1852
- * Start watching libraries directory for CSS/JS changes
1853
- */
1854
- watchLibraries(dirPath = projectPaths.libraries()) {
1855
- if (!existsSync3(dirPath)) {
1856
- return;
1857
- }
1858
- this.librariesWatcher = watch(
1859
- dirPath,
1860
- { recursive: true },
1861
- async (event, filename) => {
1862
- if (filename && (filename.endsWith(".css") || filename.endsWith(".js"))) {
1863
- if (this.callbacks.onLibraryChange) {
1864
- await this.callbacks.onLibraryChange();
1865
- }
1866
- }
1867
- }
1868
- );
1869
- }
1870
- /**
1871
- * Astro-format projects keep pages/components/CMS under src/ as .astro/.json.
1872
- * The onPageChange/onComponentChange/onCMSChange callbacks are format-agnostic
1873
- * (reload goes through the active provider), so file→editor live reload works
1874
- * for `.astro` the same way it does for JSON. These are no-ops in JSON projects
1875
- * (the src/ dirs don't exist).
1876
- */
1877
- watchAstroPages(dirPath = join3(getProjectRoot(), "src", "pages")) {
1878
- if (!existsSync3(dirPath)) return;
1879
- this.astroPagesWatcher = watch(dirPath, { recursive: true }, async (_event, filename) => {
1880
- if (filename && filename.endsWith(".astro") && !filename.includes("[")) {
1881
- const pagePath = mapPageNameToPath(filename.replace(/\.astro$/, ""));
1882
- if (this.callbacks.onPageChange) await this.callbacks.onPageChange(pagePath);
1883
- }
1884
- });
1885
- }
1886
- watchAstroComponents(dirPath = join3(getProjectRoot(), "src", "components")) {
1887
- if (!existsSync3(dirPath)) return;
1888
- this.astroComponentsWatcher = watch(dirPath, { recursive: true }, async (_event, filename) => {
1889
- if (filename && filename.endsWith(".astro") && this.callbacks.onComponentChange) {
1890
- await this.callbacks.onComponentChange();
1891
- }
1892
- });
1893
- }
1894
- watchAstroContent(dirPath = join3(getProjectRoot(), "src", "content")) {
1895
- if (!existsSync3(dirPath)) return;
1896
- this.astroContentWatcher = watch(dirPath, { recursive: true }, async (_event, filename) => {
1897
- if (filename && filename.endsWith(".json") && this.callbacks.onCMSChange) {
1898
- const collection = filename.split("/")[0];
1899
- await this.callbacks.onCMSChange(collection);
1900
- }
1901
- });
1902
- }
1903
- /**
1904
- * Start watching all directories
1905
- */
1906
- watchAll() {
1907
- this.watchComponents();
1908
- this.watchPages();
1909
- this.watchTemplates();
1910
- this.watchColors();
1911
- this.watchVariables();
1912
- this.watchEnums();
1913
- this.watchCMS();
1914
- this.watchImages();
1915
- this.watchLibraries();
1916
- this.watchProjectConfig();
1917
- this.watchAstroPages();
1918
- this.watchAstroComponents();
1919
- this.watchAstroContent();
1920
- }
1921
- /**
1922
- * Stop watching all directories
1923
- */
1924
- stopAll() {
1925
- if (this.componentsWatcher) {
1926
- this.componentsWatcher.close();
1927
- this.componentsWatcher = null;
1928
- }
1929
- if (this.pagesWatcher) {
1930
- this.pagesWatcher.close();
1931
- this.pagesWatcher = null;
1932
- }
1933
- if (this.templatesWatcher) {
1934
- this.templatesWatcher.close();
1935
- this.templatesWatcher = null;
1936
- }
1937
- if (this.colorsWatcher) {
1938
- this.colorsWatcher.close();
1939
- this.colorsWatcher = null;
1940
- }
1941
- if (this.variablesWatcher) {
1942
- this.variablesWatcher.close();
1943
- this.variablesWatcher = null;
1944
- }
1945
- if (this.enumsWatcher) {
1946
- this.enumsWatcher.close();
1947
- this.enumsWatcher = null;
1948
- }
1949
- if (this.cmsWatcher) {
1950
- this.cmsWatcher.close();
1951
- this.cmsWatcher = null;
1952
- }
1953
- if (this.imagesWatcher) {
1954
- this.imagesWatcher.close();
1955
- this.imagesWatcher = null;
1956
- }
1957
- if (this.librariesWatcher) {
1958
- this.librariesWatcher.close();
1959
- this.librariesWatcher = null;
1960
- }
1961
- if (this.projectConfigWatcher) {
1962
- this.projectConfigWatcher.close();
1963
- this.projectConfigWatcher = null;
1964
- }
1965
- if (this.astroPagesWatcher) {
1966
- this.astroPagesWatcher.close();
1967
- this.astroPagesWatcher = null;
1968
- }
1969
- if (this.astroComponentsWatcher) {
1970
- this.astroComponentsWatcher.close();
1971
- this.astroComponentsWatcher = null;
1972
- }
1973
- if (this.astroContentWatcher) {
1974
- this.astroContentWatcher.close();
1975
- this.astroContentWatcher = null;
1976
- }
1977
- }
1978
- /**
1979
- * Check if watchers are active
1980
- */
1981
- isWatching() {
1982
- return this.componentsWatcher !== null || this.pagesWatcher !== null || this.cmsWatcher !== null || this.imagesWatcher !== null;
1983
- }
1984
- };
1985
-
1986
- // lib/server/services/fileWatcherService.ts
1987
- var FileWatcherService = class {
1988
- constructor(componentService, pageService, pageCache, wsManager, cmsService, onImageAdded) {
1989
- this.componentService = componentService;
1990
- this.pageService = pageService;
1991
- this.pageCache = pageCache;
1992
- this.wsManager = wsManager;
1993
- this.cmsService = cmsService;
1994
- this.onImageAdded = onImageAdded;
1995
- }
1996
- fileWatcher = null;
1997
- /** Timer for debouncing refreshSchemas calls from burst fs.watch events */
1998
- refreshSchemasTimer = null;
1999
- /**
2000
- * Initialize file watchers
2001
- */
2002
- initialize() {
2003
- this.fileWatcher = new FileWatcher({
2004
- onComponentChange: async () => {
2005
- await this.componentService.loadAllComponents();
2006
- this.wsManager.broadcastUpdate("all");
2007
- },
2008
- onPageChange: async (pagePath) => {
2009
- if (await this.pageService.reloadPageFromDisk(pagePath)) {
2010
- this.wsManager.broadcastUpdate(pagePath);
2011
- }
2012
- if (pagePath.startsWith("/templates/") && this.cmsService) {
2013
- if (this.refreshSchemasTimer !== null) {
2014
- clearTimeout(this.refreshSchemasTimer);
2015
- }
2016
- const cmsService = this.cmsService;
2017
- const wsManager = this.wsManager;
2018
- this.refreshSchemasTimer = setTimeout(async () => {
2019
- this.refreshSchemasTimer = null;
2020
- await cmsService.refreshSchemas();
2021
- wsManager.broadcastCollectionsUpdate();
2022
- }, 50);
2023
- }
2024
- },
2025
- onColorsChange: async () => {
2026
- colorService.clearCache();
2027
- this.wsManager.broadcastColorsUpdate();
2028
- },
2029
- onVariablesChange: async () => {
2030
- variableService.clearCache();
2031
- this.wsManager.broadcastVariablesUpdate();
2032
- },
2033
- onEnumsChange: async () => {
2034
- enumService.clearCache();
2035
- this.wsManager.broadcastEnumsUpdate();
2036
- },
2037
- onCMSChange: async (collection) => {
2038
- this.cmsService?.clearItemsCache(collection);
2039
- this.wsManager.broadcastCMSUpdate(collection);
2040
- },
2041
- onImageAdded: this.onImageAdded,
2042
- onLibraryChange: async () => {
2043
- this.wsManager.broadcastLibrariesUpdate();
2044
- },
2045
- onProjectConfigChange: async () => {
2046
- configService.reset();
2047
- await configService.load();
2048
- this.wsManager.broadcastConfigUpdate();
2049
- }
2050
- });
2051
- this.fileWatcher.watchAll();
2052
- }
2053
- /**
2054
- * Stop file watchers
2055
- */
2056
- stop() {
2057
- if (this.refreshSchemasTimer !== null) {
2058
- clearTimeout(this.refreshSchemasTimer);
2059
- this.refreshSchemasTimer = null;
2060
- }
2061
- if (this.fileWatcher) {
2062
- this.fileWatcher.stopAll();
2063
- this.fileWatcher = null;
2064
- }
2065
- }
2066
- };
2067
-
2068
- // lib/server/middleware/cors.ts
2069
- var DEFAULT_OPTIONS = {
2070
- origin: "*",
2071
- methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
2072
- allowedHeaders: ["Content-Type", "Authorization"],
2073
- credentials: false
2074
- };
2075
- function createCorsHeaders(options = {}) {
2076
- const opts = { ...DEFAULT_OPTIONS, ...options };
2077
- const headers = {};
2078
- if (opts.origin) {
2079
- if (typeof opts.origin === "function") {
2080
- headers["Access-Control-Allow-Origin"] = "*";
2081
- } else if (Array.isArray(opts.origin)) {
2082
- headers["Access-Control-Allow-Origin"] = opts.origin.join(", ");
2083
- } else {
2084
- headers["Access-Control-Allow-Origin"] = opts.origin;
2085
- }
2086
- }
2087
- if (opts.methods && opts.methods.length > 0) {
2088
- headers["Access-Control-Allow-Methods"] = opts.methods.join(", ");
2089
- }
2090
- if (opts.allowedHeaders && opts.allowedHeaders.length > 0) {
2091
- headers["Access-Control-Allow-Headers"] = opts.allowedHeaders.join(", ");
2092
- }
2093
- if (opts.credentials) {
2094
- headers["Access-Control-Allow-Credentials"] = "true";
2095
- }
2096
- return headers;
2097
- }
2098
- function handleCorsPreflight(req, options = {}) {
2099
- if (req.method === "OPTIONS") {
2100
- return new Response(null, {
2101
- status: 204,
2102
- headers: createCorsHeaders(options)
2103
- });
2104
- }
2105
- return null;
2106
- }
2107
-
2108
- // lib/server/middleware/errorHandler.ts
2109
- function createErrorResponse(error, status = 500, message) {
2110
- const errorMessage = error instanceof Error ? error.message : String(error);
2111
- const response = {
2112
- error: errorMessage,
2113
- status
2114
- };
2115
- if (message) {
2116
- response.message = message;
2117
- }
2118
- return new Response(JSON.stringify(response), {
2119
- status,
2120
- headers: {
2121
- "Content-Type": "application/json"
2122
- }
2123
- });
2124
- }
2125
- async function handleRouteError(handler, errorMessage = "Internal server error") {
2126
- try {
2127
- return await handler();
2128
- } catch (error) {
2129
- return createErrorResponse(error, 500, errorMessage);
2130
- }
2131
- }
2132
- function withErrorHandling(handler, errorMessage) {
2133
- return async (...args) => {
2134
- return handleRouteError(() => handler(...args), errorMessage);
2135
- };
2136
- }
2137
-
2138
- // lib/server/middleware/logger.ts
2139
- var DEFAULT_OPTIONS2 = {
2140
- logRequests: true,
2141
- logErrors: true,
2142
- logResponseTime: true
2143
- };
2144
- function logRequest(req, options = {}) {
2145
- const opts = { ...DEFAULT_OPTIONS2, ...options };
2146
- if (!opts.logRequests) {
2147
- return;
2148
- }
2149
- const url = new URL(req.url);
2150
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2151
- }
2152
- function logError(error, context, options = {}) {
2153
- const opts = { ...DEFAULT_OPTIONS2, ...options };
2154
- if (!opts.logErrors) {
2155
- return;
2156
- }
2157
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2158
- const contextStr = context ? ` [${context}]` : "";
2159
- const errorMessage = error instanceof Error ? error.message : String(error);
2160
- if (error instanceof Error && error.stack) {
2161
- }
2162
- }
2163
- function logResponseTime(startTime, req, options = {}) {
2164
- const opts = { ...DEFAULT_OPTIONS2, ...options };
2165
- if (!opts.logResponseTime) {
2166
- return;
2167
- }
2168
- const url = new URL(req.url);
2169
- const duration = Date.now() - startTime;
2170
- }
2171
- function withLogging(handler, options = {}) {
2172
- return async (...args) => {
2173
- const startTime = Date.now();
2174
- const req = args[0];
2175
- logRequest(req, options);
2176
- try {
2177
- const response = await handler(...args);
2178
- logResponseTime(startTime, req, options);
2179
- return response;
2180
- } catch (error) {
2181
- logError(error, req.method, options);
2182
- logResponseTime(startTime, req, options);
2183
- throw error;
2184
- }
2185
- };
2186
- }
2187
-
2188
- // lib/server/routes/api/shared.ts
2189
- function jsonResponse(data, options = {}) {
2190
- const corsHeaders = createCorsHeaders();
2191
- return new Response(JSON.stringify(data), {
2192
- status: options.status || 200,
2193
- headers: {
2194
- "Content-Type": "application/json",
2195
- "Cache-Control": "no-store, max-age=0",
2196
- "Pragma": "no-cache",
2197
- "Expires": "0",
2198
- ...corsHeaders
2199
- }
2200
- });
2201
- }
2202
- function errorResponse(message, status = 500) {
2203
- return jsonResponse({ error: message }, { status });
2204
- }
2205
- function fnv1a(str) {
2206
- let hash = 2166136261;
2207
- for (let i = 0; i < str.length; i++) {
2208
- hash ^= str.charCodeAt(i);
2209
- hash = Math.imul(hash, 16777619);
2210
- }
2211
- return (hash >>> 0).toString(36);
2212
- }
2213
- function cachedJsonResponse(req, body, options = {}) {
2214
- const corsHeaders = createCorsHeaders();
2215
- const contentType = options.contentType ?? "application/json";
2216
- const etag = `W/"${fnv1a(body)}"`;
2217
- const ifNoneMatch = req.headers.get("if-none-match");
2218
- if (ifNoneMatch && ifNoneMatch === etag) {
2219
- return new Response(null, {
2220
- status: 304,
2221
- headers: {
2222
- "ETag": etag,
2223
- "Cache-Control": "no-cache",
2224
- ...corsHeaders
2225
- }
2226
- });
2227
- }
2228
- return new Response(body, {
2229
- status: options.status ?? 200,
2230
- headers: {
2231
- "Content-Type": contentType,
2232
- "ETag": etag,
2233
- // `no-cache` (not `no-store`) — browser may keep the body but must
2234
- // revalidate every time. Pairs with the 304 branch above.
2235
- "Cache-Control": "no-cache",
2236
- ...corsHeaders
2237
- }
2238
- });
2239
- }
2240
-
2241
- // lib/server/routes/api/cms.ts
2242
- function handleCollectionsRoute(cmsService) {
2243
- const schemas = cmsService.getAllSchemas();
2244
- const collections = [];
2245
- for (const [, schemaInfo] of schemas) {
2246
- collections.push(schemaInfo.schema);
2247
- }
2248
- return jsonResponse({ collections });
2249
- }
2250
- function handleCollectionSchemaRoute(collection, cmsService) {
2251
- const schema = cmsService.getSchema(collection);
2252
- if (!schema) {
2253
- return errorResponse("Collection not found", 404);
2254
- }
2255
- return jsonResponse({ schema });
2256
- }
2257
- async function handleItemsRoute(collection, cmsService, searchParams) {
2258
- const limit = searchParams?.get("limit") ? parseInt(searchParams.get("limit"), 10) : void 0;
2259
- const offset = searchParams?.get("offset") ? parseInt(searchParams.get("offset"), 10) : void 0;
2260
- const search = searchParams?.get("search") || void 0;
2261
- const allItems = await cmsService.queryItems({ collection });
2262
- const total = allItems.length;
2263
- let filteredItems = allItems;
2264
- if (search) {
2265
- const searchLower = search.toLowerCase();
2266
- const schema2 = cmsService.getSchema(collection);
2267
- const slugField = schema2?.slugField || "slug";
2268
- filteredItems = allItems.filter((item) => {
2269
- const title = extractStringValue(item.title || "").toLowerCase();
2270
- const slug = extractStringValue(item[slugField] || item._slug || "").toLowerCase();
2271
- const filename = String(item._filename || "").toLowerCase();
2272
- return title.includes(searchLower) || slug.includes(searchLower) || filename.includes(searchLower);
2273
- });
2274
- }
2275
- let items = filteredItems;
2276
- if (offset !== void 0 && offset > 0) {
2277
- items = items.slice(offset);
2278
- }
2279
- if (limit !== void 0 && limit > 0) {
2280
- items = items.slice(0, limit);
2281
- }
2282
- const schema = cmsService.getSchema(collection);
2283
- const itemsWithUrl = schema ? addItemUrls(items, schema) : items;
2284
- return jsonResponse({
2285
- items: itemsWithUrl,
2286
- total: search ? filteredItems.length : total,
2287
- limit,
2288
- offset: offset || 0
2289
- });
2290
- }
2291
- async function handleItemRoute(collection, slug, cmsService) {
2292
- const match = await cmsService.matchRoute(
2293
- cmsService.getSchema(collection)?.urlPattern.replace("{{slug}}", slug) || ""
2294
- );
2295
- if (!match) {
2296
- const items = await cmsService.queryItems({ collection });
2297
- const item = items.find((i) => i._slug === slug || i[cmsService.getSchema(collection)?.slugField || "slug"] === slug);
2298
- if (!item) {
2299
- return errorResponse("Item not found", 404);
2300
- }
2301
- return jsonResponse({ item });
2302
- }
2303
- return jsonResponse({ item: match.item });
2304
- }
2305
-
2306
- // lib/server/routes/api/core-routes.ts
2307
- init_constants();
2308
-
2309
- // lib/server/routes/api/pages.ts
2310
- init_constants();
2311
- function handlePagesRoute(pageService) {
2312
- const pages = pageService.getAllPagesWithInfo();
2313
- return jsonResponse({ pages });
2314
- }
2315
- function handlePageDataRoute(url, pageService) {
2316
- const pagePath = url.pathname.replace(API_ROUTES.PAGE_DATA, "") || "/";
2317
- const content = pageService.getPage(pagePath);
2318
- if (content) {
2319
- try {
2320
- const parsedContent = parseJSON(content);
2321
- const lineMap = pageService.getLineMap(pagePath);
2322
- const lineMapObj = {};
2323
- if (lineMap) {
2324
- lineMap.forEach((value, key) => {
2325
- lineMapObj[key] = value;
2326
- });
2327
- }
2328
- return jsonResponse({ ...parsedContent, _lineMap: lineMapObj });
2329
- } catch (error) {
2330
- return jsonResponse({ error: "Invalid JSON" }, { status: 500 });
2331
- }
2332
- } else {
2333
- return jsonResponse({ error: "Page not found" }, { status: 404 });
2334
- }
2335
- }
2336
- function handlePageContentRoute(req, url, pageService) {
2337
- const page = url.searchParams.get("page") || "/";
2338
- const content = pageService.getPage(page);
2339
- if (content) {
2340
- return cachedJsonResponse(req, content);
2341
- }
2342
- const corsHeaders = createCorsHeaders();
2343
- return new Response("Page not found", {
2344
- status: 404,
2345
- headers: {
2346
- "Content-Type": "application/json",
2347
- "Cache-Control": "no-store, max-age=0",
2348
- ...corsHeaders
2349
- }
2350
- });
2351
- }
2352
- function handleSlugMappingsRoute(pageService) {
2353
- const mappings = pageService.getSlugMappings();
2354
- return jsonResponse({ mappings });
2355
- }
2356
-
2357
- // lib/server/routes/api/components.ts
2358
- function handleComponentsRoute(req, componentService) {
2359
- const componentsWithCategories = componentService.getAllComponentsWithCategories();
2360
- const result = {};
2361
- for (const [name, info] of Object.entries(componentsWithCategories)) {
2362
- result[name] = {
2363
- ...info.definition,
2364
- _category: info.category
2365
- };
2366
- }
2367
- const diagnostics = componentService.getLoadDiagnostics();
2368
- if (diagnostics.errors.length > 0 || diagnostics.warnings.length > 0) {
2369
- result._diagnostics = diagnostics;
2370
- }
2371
- return cachedJsonResponse(req, JSON.stringify(result));
2372
- }
2373
- function handleComponentDataRoute(url, componentService) {
2374
- const componentName = url.pathname.replace("/api/component-data/", "");
2375
- const componentDef = componentService.getComponent(componentName);
2376
- if (!componentDef) {
2377
- const loadErrors = componentService.getLoadErrors();
2378
- const diagnostic = loadErrors.get(componentName);
2379
- if (diagnostic) {
2380
- const message = formatJsonErrorMessage(
2381
- `${diagnostic.componentName}.json`,
2382
- { message: diagnostic.description, line: diagnostic.line, column: diagnostic.column }
2383
- );
2384
- return jsonResponse(
2385
- { error: "json_parse_error", message, errorCode: "JSON_PARSE_ERROR" },
2386
- { status: 422 }
2387
- );
2388
- }
2389
- return jsonResponse({ error: "Component not found" }, { status: 404 });
2390
- }
2391
- if (!componentService.validateComponentStructure(componentDef)) {
2392
- return jsonResponse(
2393
- {
2394
- error: "Component structure is missing",
2395
- message: `Component "${componentName}" does not have a valid structure definition. Please add a structure field to the component definition.`
2396
- },
2397
- { status: 400 }
2398
- );
2399
- }
2400
- return jsonResponse(componentDef);
2401
- }
2402
- async function handleComponentJavaScriptRoute(url, componentService) {
2403
- const componentName = url.pathname.replace("/api/component-js/", "");
2404
- const jsContent = await componentService.getComponentJavaScript(componentName);
2405
- const corsHeaders = createCorsHeaders();
2406
- return new Response(jsContent || "", {
2407
- headers: {
2408
- "Content-Type": "text/javascript",
2409
- ...corsHeaders
2410
- }
2411
- });
2412
- }
2413
- function handleComponentUsageRoute(pageService, componentService) {
2414
- const usageMap = /* @__PURE__ */ new Map();
2415
- function walkTree(node, fileName, filePath, fileType) {
2416
- if (!node || typeof node !== "object") return;
2417
- if (node.type === "component" && "component" in node && node.component) {
2418
- const compName = node.component;
2419
- if (!usageMap.has(compName)) {
2420
- usageMap.set(compName, { count: 0, files: /* @__PURE__ */ new Map() });
2421
- }
2422
- const entry = usageMap.get(compName);
2423
- entry.count++;
2424
- entry.files.set(filePath, { name: fileName, path: filePath, type: fileType });
2425
- }
2426
- if ("children" in node && Array.isArray(node.children)) {
2427
- for (const child of node.children) {
2428
- if (child && typeof child === "object") {
2429
- walkTree(child, fileName, filePath, fileType);
2430
- }
2431
- }
2432
- }
2433
- }
2434
- const pagePaths = pageService.getAllPagePaths();
2435
- for (const path2 of pagePaths) {
2436
- const pageData = pageService.getPageData(path2);
2437
- if (pageData && "root" in pageData && pageData.root) {
2438
- const name = path2 === "/" ? "index" : path2.replace(/^\//, "").replace(/\//g, "-");
2439
- walkTree(pageData.root, name, path2, "page");
2440
- }
2441
- }
2442
- const allComponents = componentService.getAllComponents();
2443
- for (const [compName, compDef] of Object.entries(allComponents)) {
2444
- if (compDef.component?.structure) {
2445
- walkTree(compDef.component.structure, compName, compName, "component");
2446
- }
2447
- }
2448
- const result = {};
2449
- for (const [name, data] of usageMap) {
2450
- result[name] = {
2451
- count: data.count,
2452
- files: Array.from(data.files.values())
2453
- };
2454
- }
2455
- return jsonResponse(result);
2456
- }
2457
-
2458
- // lib/server/routes/api/config.ts
2459
- async function handleConfigRoute() {
2460
- try {
2461
- const configContent = await loadJSONFile(projectPaths.config());
2462
- if (configContent) {
2463
- const config = parseJSON(configContent);
2464
- return jsonResponse(config);
2465
- } else {
2466
- return jsonResponse({ editor: { theme: "dark" } });
2467
- }
2468
- } catch (error) {
2469
- return jsonResponse({ editor: { theme: "dark" } });
2470
- }
2471
- }
2472
- async function handleComponentsConfigRoute() {
2473
- try {
2474
- const configContent = await loadJSONFile(projectPaths.componentsConfig());
2475
- if (configContent) {
2476
- const config = parseJSON(configContent);
2477
- return jsonResponse(config);
2478
- } else {
2479
- return jsonResponse({ componentTypes: [], componentCategories: {} });
2480
- }
2481
- } catch (error) {
2482
- return jsonResponse({ componentTypes: [], componentCategories: {} });
2483
- }
2484
- }
2485
-
2486
- // lib/server/routes/api/colors.ts
2487
- async function handleColorsRoute(url) {
2488
- const themeName = url?.searchParams.get("theme") || void 0;
2489
- const colors = await colorService.getThemeColors(themeName);
2490
- return jsonResponse(colors);
2491
- }
2492
- async function handleThemesRoute() {
2493
- const themes = await colorService.getAvailableThemes();
2494
- const defaultTheme = await colorService.getDefaultTheme();
2495
- return jsonResponse({ themes, default: defaultTheme });
2496
- }
2497
- async function handleColorsConfigRoute() {
2498
- const config = await colorService.getFullConfig();
2499
- return jsonResponse(config);
2500
- }
2501
- async function handleColorsStatusRoute() {
2502
- const result = await colorService.getConfigWithStatus();
2503
- return jsonResponse(result);
2504
- }
2505
-
2506
- // lib/server/routes/api/variables.ts
2507
- async function handleVariablesStatusRoute() {
2508
- const result = await variableService.getConfigWithStatus();
2509
- return jsonResponse(result);
2510
- }
2511
- async function handleVariablesCSSRoute() {
2512
- await configService.load();
2513
- const variablesConfig = await variableService.loadConfig();
2514
- const breakpointConfig = await loadBreakpointConfig();
2515
- const responsiveScalesConfig = configService.getResponsiveScales();
2516
- const css = generateVariablesCSS(variablesConfig, breakpointConfig, responsiveScalesConfig);
2517
- return new Response(css, {
2518
- headers: { "Content-Type": "text/css" }
2519
- });
2520
- }
2521
-
2522
- // lib/server/routes/api/enums.ts
2523
- async function handleEnumsRoute() {
2524
- const result = await enumService.getConfigWithStatus();
2525
- return jsonResponse(result);
2526
- }
2527
-
2528
- // lib/server/routes/api/functions.ts
2529
- import path from "path";
2530
- async function loadProjectEnv() {
2531
- const envPath = projectPaths.env();
2532
- const env = {};
2533
- try {
2534
- if (await fileExists(envPath)) {
2535
- const content = await readTextFile(envPath);
2536
- for (const line of content.split("\n")) {
2537
- const trimmed = line.trim();
2538
- if (trimmed && !trimmed.startsWith("#")) {
2539
- const [key, ...valueParts] = trimmed.split("=");
2540
- if (key) {
2541
- let value = valueParts.join("=");
2542
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2543
- value = value.slice(1, -1);
2544
- }
2545
- env[key.trim()] = value;
2546
- }
2547
- }
2548
- }
2549
- }
2550
- } catch (error) {
2551
- console.warn("Could not load .env file:", error);
2552
- }
2553
- return env;
2554
- }
2555
- async function handleFunctionsRoute(req, url) {
2556
- if (!url.pathname.startsWith("/api/")) {
2557
- return void 0;
2558
- }
2559
- if (url.pathname.startsWith("/api/editor/")) {
2560
- return void 0;
2561
- }
2562
- const functionPath = url.pathname.replace("/api/", "");
2563
- const functionFilePath = path.join(
2564
- projectPaths.functions(),
2565
- "api",
2566
- `${functionPath}.ts`
2567
- );
2568
- if (!await fileExists(functionFilePath)) {
2569
- const jsFilePath = functionFilePath.replace(".ts", ".js");
2570
- if (!await fileExists(jsFilePath)) {
2571
- return void 0;
2572
- }
2573
- }
2574
- try {
2575
- const moduleUrl = `${functionFilePath}?t=${Date.now()}`;
2576
- const functionModule = await import(moduleUrl);
2577
- const method = req.method.toUpperCase();
2578
- const handlerName = `onRequest${method.charAt(0)}${method.slice(1).toLowerCase()}`;
2579
- const handler = functionModule[handlerName] || functionModule.onRequest;
2580
- if (!handler || typeof handler !== "function") {
2581
- return new Response(
2582
- JSON.stringify({ error: `No handler found for ${method} request` }),
2583
- { status: 405, headers: { "Content-Type": "application/json" } }
2584
- );
2585
- }
2586
- const env = await loadProjectEnv();
2587
- const context = {
2588
- request: req,
2589
- env,
2590
- params: {},
2591
- // Could parse URL params if needed
2592
- waitUntil: () => {
2593
- },
2594
- // No-op in dev
2595
- passThroughOnException: () => {
2596
- }
2597
- // No-op in dev
2598
- };
2599
- const response = await handler(context);
2600
- const headers = new Headers(response.headers);
2601
- headers.set("Access-Control-Allow-Origin", "*");
2602
- headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2603
- headers.set("Access-Control-Allow-Headers", "Content-Type");
2604
- return new Response(response.body, {
2605
- status: response.status,
2606
- statusText: response.statusText,
2607
- headers
2608
- });
2609
- } catch (error) {
2610
- console.error(`Error executing function ${functionPath}:`, error);
2611
- return new Response(
2612
- JSON.stringify({
2613
- error: "Function execution error",
2614
- message: error instanceof Error ? error.message : "Unknown error"
2615
- }),
2616
- { status: 500, headers: { "Content-Type": "application/json" } }
2617
- );
2618
- }
2619
- }
2620
- function handleFunctionsPreflight(req, url) {
2621
- if (req.method !== "OPTIONS" || !url.pathname.startsWith("/api/")) {
2622
- return void 0;
2623
- }
2624
- return new Response(null, {
2625
- status: 204,
2626
- headers: {
2627
- "Access-Control-Allow-Origin": "*",
2628
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
2629
- "Access-Control-Allow-Headers": "Content-Type",
2630
- "Access-Control-Max-Age": "86400"
2631
- }
2632
- });
2633
- }
2634
-
2635
- // lib/server/routes/api/core-routes.ts
2636
- async function handleCoreApiRoutes(req, url, context) {
2637
- const { pageService, componentService, cmsService } = context;
2638
- const preflightResponse = handleFunctionsPreflight(req, url);
2639
- if (preflightResponse) {
2640
- return preflightResponse;
2641
- }
2642
- const functionsResponse = await handleFunctionsRoute(req, url);
2643
- if (functionsResponse) {
2644
- return functionsResponse;
2645
- }
2646
- if (url.pathname === API_ROUTES.PAGES && req.method === "GET") {
2647
- return await handleRouteError(
2648
- () => Promise.resolve(handlePagesRoute(pageService)),
2649
- "Failed to fetch pages list"
2650
- );
2651
- }
2652
- if (url.pathname.startsWith(API_ROUTES.PAGE_DATA) && req.method === "GET") {
2653
- return await handleRouteError(
2654
- () => Promise.resolve(handlePageDataRoute(url, pageService)),
2655
- "Failed to fetch page data"
2656
- );
2657
- }
2658
- if (url.pathname === API_ROUTES.PAGE_CONTENT && req.method === "GET") {
2659
- return await handleRouteError(
2660
- () => Promise.resolve(handlePageContentRoute(req, url, pageService)),
2661
- "Failed to fetch page content"
2662
- );
2663
- }
2664
- if (url.pathname === API_ROUTES.COMPONENTS && req.method === "GET") {
2665
- return await handleRouteError(
2666
- () => Promise.resolve(handleComponentsRoute(req, componentService)),
2667
- "Failed to fetch components list"
2668
- );
2669
- }
2670
- if (url.pathname.startsWith("/api/component-data/") && req.method === "GET") {
2671
- return await handleRouteError(
2672
- () => Promise.resolve(handleComponentDataRoute(url, componentService)),
2673
- "Failed to fetch component data"
2674
- );
2675
- }
2676
- if (url.pathname.startsWith("/api/component-js/") && req.method === "GET") {
2677
- return await handleRouteError(
2678
- () => handleComponentJavaScriptRoute(url, componentService),
2679
- "Failed to fetch component JavaScript"
2680
- );
2681
- }
2682
- if (url.pathname === "/api/config" && req.method === "GET") {
2683
- return await handleRouteError(
2684
- () => handleConfigRoute(),
2685
- "Failed to fetch config"
2686
- );
2687
- }
2688
- if (url.pathname === "/api/components-config" && req.method === "GET") {
2689
- return await handleRouteError(
2690
- () => handleComponentsConfigRoute(),
2691
- "Failed to fetch components config"
2692
- );
2693
- }
2694
- if (url.pathname === "/api/colors" && req.method === "GET") {
2695
- return await handleRouteError(
2696
- () => handleColorsRoute(url),
2697
- "Failed to fetch colors"
2698
- );
2699
- }
2700
- if (url.pathname === "/api/themes" && req.method === "GET") {
2701
- return await handleRouteError(
2702
- () => handleThemesRoute(),
2703
- "Failed to fetch themes"
2704
- );
2705
- }
2706
- if (url.pathname === "/api/colors-config" && req.method === "GET") {
2707
- return await handleRouteError(
2708
- () => handleColorsConfigRoute(),
2709
- "Failed to fetch colors config"
2710
- );
2711
- }
2712
- if (url.pathname === "/api/colors-status" && req.method === "GET") {
2713
- return await handleRouteError(
2714
- () => handleColorsStatusRoute(),
2715
- "Failed to fetch colors status"
2716
- );
2717
- }
2718
- if (url.pathname === "/api/variables-status" && req.method === "GET") {
2719
- return await handleRouteError(
2720
- () => handleVariablesStatusRoute(),
2721
- "Failed to fetch variables status"
2722
- );
2723
- }
2724
- if (url.pathname === "/api/variables-css" && req.method === "GET") {
2725
- return await handleRouteError(
2726
- () => handleVariablesCSSRoute(),
2727
- "Failed to generate variables CSS"
2728
- );
2729
- }
2730
- if (url.pathname === "/api/fonts-css" && req.method === "GET") {
2731
- return await handleRouteError(
2732
- async () => {
2733
- await loadProjectConfig();
2734
- const css = generateFontCSS();
2735
- return new Response(css, {
2736
- headers: { "Content-Type": "text/css" }
2737
- });
2738
- },
2739
- "Failed to generate fonts CSS"
2740
- );
2741
- }
2742
- if (url.pathname === "/api/enums" && req.method === "GET") {
2743
- return await handleRouteError(
2744
- () => handleEnumsRoute(),
2745
- "Failed to fetch enums"
2746
- );
2747
- }
2748
- if (url.pathname === "/api/usage" && req.method === "GET") {
2749
- return await handleRouteError(
2750
- () => {
2751
- const cmsCollections = cmsService ? cmsService.getAllSchemas().size : 0;
2752
- const allPages = pageService.getAllPagePaths();
2753
- const pages = allPages.filter((p) => !p.includes("/templates/")).length;
2754
- const i18n = configService.getI18n();
2755
- const locales = i18n.locales.length;
2756
- return Promise.resolve(
2757
- new Response(JSON.stringify({ cmsCollections, pages, locales }), {
2758
- headers: { "Content-Type": "application/json" }
2759
- })
2760
- );
2761
- },
2762
- "Failed to fetch usage stats"
2763
- );
2764
- }
2765
- if (url.pathname === API_ROUTES.COMPONENT_USAGE && req.method === "GET") {
2766
- return await handleRouteError(
2767
- () => Promise.resolve(handleComponentUsageRoute(pageService, componentService)),
2768
- "Failed to fetch component usage"
2769
- );
2770
- }
2771
- if (url.pathname === "/api/slug-mappings" && req.method === "GET") {
2772
- return await handleRouteError(
2773
- () => Promise.resolve(handleSlugMappingsRoute(pageService)),
2774
- "Failed to fetch slug mappings"
2775
- );
2776
- }
2777
- if (cmsService && url.pathname.startsWith("/api/cms/")) {
2778
- if (url.pathname === "/api/cms/collections" && req.method === "GET") {
2779
- return await handleRouteError(
2780
- () => Promise.resolve(handleCollectionsRoute(cmsService)),
2781
- "Failed to fetch CMS collections"
2782
- );
2783
- }
2784
- const collectionSchemaMatch = url.pathname.match(/^\/api\/cms\/collections\/([^/]+)$/);
2785
- if (collectionSchemaMatch && req.method === "GET") {
2786
- const collection = collectionSchemaMatch[1];
2787
- return await handleRouteError(
2788
- () => Promise.resolve(handleCollectionSchemaRoute(collection, cmsService)),
2789
- "Failed to fetch collection schema"
2790
- );
2791
- }
2792
- const cmsPathMatch = url.pathname.match(/^\/api\/cms\/([^/]+)(?:\/([^/]+))?$/);
2793
- if (cmsPathMatch) {
2794
- const collection = cmsPathMatch[1];
2795
- const slug = cmsPathMatch[2];
2796
- if (collection === "collections") {
2797
- return void 0;
2798
- }
2799
- if (!slug && req.method === "GET") {
2800
- return await handleRouteError(
2801
- () => handleItemsRoute(collection, cmsService, url.searchParams),
2802
- "Failed to fetch CMS items"
2803
- );
2804
- }
2805
- if (slug && req.method === "GET") {
2806
- return await handleRouteError(
2807
- () => handleItemRoute(collection, slug, cmsService),
2808
- "Failed to fetch CMS item"
2809
- );
2810
- }
2811
- }
2812
- }
2813
- return void 0;
2814
- }
2815
-
2816
- // lib/server/routes/index.ts
2817
- init_constants();
2818
-
2819
- // lib/server/routes/api/index.ts
2820
- async function handleApiRoutes(req, url, pageService, componentService, cmsContext) {
2821
- try {
2822
- const coreContext = {
2823
- pageService,
2824
- componentService,
2825
- cmsService: cmsContext?.cmsService
2826
- };
2827
- return await handleCoreApiRoutes(req, url, coreContext);
2828
- } catch (error) {
2829
- return handleRouteError(
2830
- () => Promise.reject(error),
2831
- "API route error"
2832
- );
2833
- }
2834
- }
2835
-
2836
- // lib/server/routes/pages.ts
2837
- import { randomBytes } from "crypto";
2838
-
2839
- // lib/shared/pathUtils.ts
2840
- function getStaticFilePath(pagePath, distDir = "./dist") {
2841
- if (pagePath === "/") {
2842
- return `${distDir}/index.html`;
2843
- }
2844
- const pathParts = pagePath.substring(1).split("/");
2845
- const fileName = pathParts.join("-") + ".html";
2846
- return `${distDir}/${fileName}`;
2847
- }
2848
-
2849
- // lib/server/ssr/errorOverlay.ts
2850
- function extractErrorInfo(error) {
2851
- if (error instanceof Error) {
2852
- return {
2853
- message: error.message,
2854
- stack: error.stack
2855
- };
2856
- }
2857
- if (typeof error === "string") {
2858
- return { message: error };
2859
- }
2860
- return { message: String(error) };
2861
- }
2862
- function escapeHtml(str) {
2863
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2864
- }
2865
- function safeJsonForScript(data) {
2866
- return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
2867
- }
2868
- function generateErrorPage(error, context, cspNonce) {
2869
- const errorInfo = extractErrorInfo(error);
2870
- const friendly = toFriendlyError(errorInfo.message);
2871
- const errorMessage = escapeHtml(errorInfo.message);
2872
- const errorStack = errorInfo.stack ? escapeHtml(errorInfo.stack) : "";
2873
- const renderInline = (s) => escapeHtml(s).replace(/`([^`]+)`/g, "<code>$1</code>");
2874
- const friendlyTitle = escapeHtml(friendly.title);
2875
- const friendlyMessage = renderInline(friendly.friendlyMessage);
2876
- const friendlyHint = friendly.hint ? renderInline(friendly.hint) : "";
2877
- const nonceAttr = cspNonce ? ` nonce="${cspNonce}"` : "";
2878
- const errorDataJson = safeJsonForScript({
2879
- type: "PREVIEW_ERROR",
2880
- error: {
2881
- message: friendly.friendlyMessage,
2882
- title: friendly.title,
2883
- hint: friendly.hint,
2884
- raw: errorInfo.message,
2885
- stack: errorInfo.stack,
2886
- context
2887
- }
2888
- });
2889
- const fullErrorText = safeJsonForScript(
2890
- errorInfo.message + (errorInfo.stack ? "\n\n" + errorInfo.stack : "")
2891
- );
2892
- return `<!DOCTYPE html>
2893
- <html lang="en">
2894
- <head>
2895
- <meta charset="UTF-8">
2896
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2897
- <title>Preview Error</title>
2898
- <style${nonceAttr}>
2899
- * { margin: 0; padding: 0; box-sizing: border-box; }
2900
- body {
2901
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2902
- background: #1a1a1a;
2903
- color: #fff;
2904
- min-height: 100vh;
2905
- display: flex;
2906
- align-items: center;
2907
- justify-content: center;
2908
- padding: 20px;
2909
- }
2910
- .error-container {
2911
- max-width: 800px;
2912
- width: 100%;
2913
- background: #2d2d2d;
2914
- border-radius: 12px;
2915
- overflow: hidden;
2916
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
2917
- }
2918
- .error-header {
2919
- background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
2920
- padding: 20px 24px;
2921
- display: flex;
2922
- align-items: center;
2923
- gap: 12px;
2924
- }
2925
- .error-icon { font-size: 24px; }
2926
- .error-title { font-size: 18px; font-weight: 600; }
2927
- .error-body { padding: 24px; }
2928
- .error-context {
2929
- font-size: 12px;
2930
- color: #888;
2931
- margin-bottom: 16px;
2932
- text-transform: uppercase;
2933
- letter-spacing: 0.5px;
2934
- }
2935
- .error-friendly {
2936
- font-size: 15px;
2937
- line-height: 1.6;
2938
- color: #e8e8e8;
2939
- margin-bottom: 16px;
2940
- }
2941
- .error-friendly code {
2942
- font-family: 'SF Mono', Menlo, Monaco, 'Courier New', monospace;
2943
- font-size: 13px;
2944
- background: #1a1a1a;
2945
- border: 1px solid #444;
2946
- border-radius: 4px;
2947
- padding: 1px 5px;
2948
- color: #ffb86b;
2949
- }
2950
- .error-message {
2951
- background: #1a1a1a;
2952
- border: 1px solid #444;
2953
- border-radius: 8px;
2954
- padding: 16px;
2955
- font-family: 'SF Mono', Menlo, Monaco, 'Courier New', monospace;
2956
- font-size: 14px;
2957
- line-height: 1.6;
2958
- color: #ff6b6b;
2959
- overflow-x: auto;
2960
- white-space: pre-wrap;
2961
- word-break: break-word;
2962
- }
2963
- .error-stack { margin-top: 16px; }
2964
- .stack-toggle {
2965
- background: none;
2966
- border: 1px solid #444;
2967
- color: #888;
2968
- padding: 8px 16px;
2969
- border-radius: 6px;
2970
- cursor: pointer;
2971
- font-size: 13px;
2972
- display: flex;
2973
- align-items: center;
2974
- gap: 8px;
2975
- transition: all 0.15s;
2976
- }
2977
- .stack-toggle:hover { background: #333; color: #fff; border-color: #555; }
2978
- .stack-toggle svg { width: 14px; height: 14px; transition: transform 0.2s; }
2979
- .stack-toggle.expanded svg { transform: rotate(90deg); }
2980
- .stack-content {
2981
- display: none;
2982
- margin-top: 12px;
2983
- background: #1a1a1a;
2984
- border: 1px solid #444;
2985
- border-radius: 8px;
2986
- padding: 16px;
2987
- font-family: 'SF Mono', Menlo, Monaco, 'Courier New', monospace;
2988
- font-size: 12px;
2989
- line-height: 1.8;
2990
- color: #999;
2991
- overflow-x: auto;
2992
- white-space: pre;
2993
- }
2994
- .stack-content.visible { display: block; }
2995
- .error-footer {
2996
- padding: 16px 24px;
2997
- background: #252525;
2998
- border-top: 1px solid #333;
2999
- display: flex;
3000
- justify-content: space-between;
3001
- align-items: center;
3002
- flex-wrap: wrap;
3003
- gap: 12px;
3004
- }
3005
- .error-hint { font-size: 13px; color: #888; }
3006
- .copy-btn {
3007
- background: #333;
3008
- border: 1px solid #444;
3009
- color: #fff;
3010
- padding: 8px 16px;
3011
- border-radius: 6px;
3012
- cursor: pointer;
3013
- font-size: 13px;
3014
- display: flex;
3015
- align-items: center;
3016
- gap: 6px;
3017
- transition: all 0.15s;
3018
- }
3019
- .copy-btn:hover { background: #444; border-color: #555; }
3020
- .copy-btn svg { width: 14px; height: 14px; }
3021
- </style>
3022
- </head>
3023
- <body>
3024
- <div class="error-container">
3025
- <div class="error-header">
3026
- <span class="error-icon">\u26A0\uFE0F</span>
3027
- <span class="error-title">${friendlyTitle}</span>
3028
- </div>
3029
- <div class="error-body">
3030
- ${context ? `<div class="error-context">${escapeHtml(context)}</div>` : ""}
3031
- <p class="error-friendly">${friendlyMessage}</p>
3032
- <div class="error-stack">
3033
- <button class="stack-toggle" id="stackToggle">
3034
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3035
- <polyline points="9 18 15 12 9 6"></polyline>
3036
- </svg>
3037
- <span>Show technical details</span>
3038
- </button>
3039
- <div class="stack-content" id="stackContent"><div class="error-message">${errorMessage}</div>${errorStack ? `
3040
- ${errorStack}` : ""}</div>
3041
- </div>
3042
- </div>
3043
- <div class="error-footer">
3044
- <span class="error-hint">${friendlyHint}</span>
3045
- <button class="copy-btn" id="copyBtn">
3046
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3047
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
3048
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
3049
- </svg>
3050
- <span>Copy Error</span>
3051
- </button>
3052
- </div>
3053
- </div>
3054
- <script${nonceAttr}>
3055
- (function() {
3056
- // Send error to parent editor
3057
- if (window.parent && window.parent !== window) {
3058
- try {
3059
- window.parent.postMessage(${errorDataJson}, '*');
3060
- } catch (e) {
3061
- console.error('Failed to send error to parent:', e);
3062
- }
3063
- }
3064
-
3065
- // Stack trace toggle
3066
- var stackToggle = document.getElementById('stackToggle');
3067
- var stackContent = document.getElementById('stackContent');
3068
- if (stackToggle && stackContent) {
3069
- stackToggle.addEventListener('click', function() {
3070
- var isVisible = stackContent.classList.toggle('visible');
3071
- stackToggle.classList.toggle('expanded', isVisible);
3072
- stackToggle.querySelector('span').textContent = isVisible ? 'Hide technical details' : 'Show technical details';
3073
- });
3074
- }
3075
-
3076
- // Copy button
3077
- var copyBtn = document.getElementById('copyBtn');
3078
- if (copyBtn) {
3079
- copyBtn.addEventListener('click', function() {
3080
- var errorText = ${fullErrorText};
3081
- navigator.clipboard.writeText(errorText).then(function() {
3082
- var span = copyBtn.querySelector('span');
3083
- var original = span.textContent;
3084
- span.textContent = 'Copied!';
3085
- setTimeout(function() { span.textContent = original; }, 2000);
3086
- }).catch(function(err) {
3087
- console.error('Failed to copy:', err);
3088
- });
3089
- });
3090
- }
3091
- })();
3092
- </script>
3093
- </body>
3094
- </html>`;
3095
- }
3096
-
3097
- // lib/server/scriptCache.ts
3098
- var scriptCache = /* @__PURE__ */ new Map();
3099
- function cacheScript(hash, content) {
3100
- scriptCache.set(hash, content);
3101
- }
3102
- function getScript(hash) {
3103
- return scriptCache.get(hash);
3104
- }
3105
- function hashContent2(content) {
3106
- return hashContent(content);
3107
- }
3108
-
3109
- // lib/server/routes/pages.ts
3110
- var EDITOR_HEADER = "x-meno-editor";
3111
- var CSP_NONCE_HEADER = "x-meno-csp-nonce";
3112
- function generateCspNonce() {
3113
- return randomBytes(16).toString("base64");
3114
- }
3115
- async function handlePageRoute(url, context, req) {
3116
- const { pageService, componentService, cmsService, injectLiveReload, isEditor, serverPort } = context;
3117
- const pagePath = url.pathname;
3118
- const injectEditorAttrs = injectLiveReload === true || req?.headers.get(EDITOR_HEADER) === "1";
3119
- const cspNonce = generateCspNonce();
3120
- const i18nConfig = await loadI18nConfig();
3121
- const { locale, pathWithoutLocale } = parseLocaleFromPath(pagePath, i18nConfig);
3122
- const slugMappings = pageService.getSlugMappings();
3123
- const slugIndex = buildSlugIndex(slugMappings);
3124
- if (cmsService) {
3125
- const cmsMatch = await cmsService.matchRoute(pathWithoutLocale, locale);
3126
- if (cmsMatch) {
3127
- if (injectEditorAttrs && url.searchParams.get("previewDraft") === "1" && cmsMatch.item._filename) {
3128
- const draft = await cmsService.getDraft(cmsMatch.collection, cmsMatch.item._filename);
3129
- if (draft) {
3130
- cmsMatch.item = draft;
3131
- }
3132
- }
3133
- const templatePageContent = await loadPageByFilePath(cmsMatch.pagePath);
3134
- if (templatePageContent) {
3135
- const templatesDir = projectPaths.templates();
3136
- const cmsTemplatePath = "/templates" + cmsMatch.pagePath.replace(templatesDir, "").replace(/\\/g, "/").replace(/\.json$/, "");
3137
- try {
3138
- const pageData = parseJSON(templatePageContent);
3139
- const globalComponentsRecord = componentService.getAllComponents();
3140
- const baseUrl = `${url.protocol}//${url.host}`;
3141
- const typedPageData = pageData;
3142
- if (injectLiveReload) {
3143
- const result = await generateSSRHTML({
3144
- pageData: typedPageData,
3145
- globalComponents: globalComponentsRecord,
3146
- pagePath,
3147
- baseUrl,
3148
- useBuiltBundle: true,
3149
- locale,
3150
- slugMappings,
3151
- cmsContext: { cms: cmsMatch.item },
3152
- cmsService,
3153
- cmsTemplatePath,
3154
- pageLibraries: typedPageData.meta?.libraries,
3155
- pageCustomCode: typedPageData.meta?.customCode,
3156
- injectLiveReload,
3157
- injectEditorAttrs,
3158
- isEditor,
3159
- serverPort,
3160
- returnSeparateJS: true,
3161
- cspNonce
3162
- });
3163
- let finalHtml = result.html;
3164
- if (result.javascript) {
3165
- const hash = hashContent2(result.javascript);
3166
- cacheScript(hash, result.javascript);
3167
- finalHtml = finalHtml.replace("</body>", ` <script src="/_scripts/${hash}.js" defer></script>
3168
- </body>`);
3169
- }
3170
- return new Response(finalHtml, {
3171
- headers: {
3172
- "Content-Type": "text/html; charset=utf-8",
3173
- "Cache-Control": "no-store, max-age=0",
3174
- "Pragma": "no-cache",
3175
- "Expires": "0",
3176
- [CSP_NONCE_HEADER]: cspNonce
3177
- }
3178
- });
3179
- }
3180
- const ssrHTML = await generateSSRHTML({
3181
- pageData: typedPageData,
3182
- globalComponents: globalComponentsRecord,
3183
- pagePath,
3184
- baseUrl,
3185
- useBuiltBundle: false,
3186
- locale,
3187
- slugMappings,
3188
- cmsContext: { cms: cmsMatch.item },
3189
- cmsService,
3190
- cmsTemplatePath,
3191
- pageLibraries: typedPageData.meta?.libraries,
3192
- pageCustomCode: typedPageData.meta?.customCode,
3193
- injectEditorAttrs,
3194
- isEditor,
3195
- cspNonce
3196
- });
3197
- return new Response(ssrHTML, {
3198
- headers: {
3199
- "Content-Type": "text/html; charset=utf-8",
3200
- "Cache-Control": "no-store, max-age=0",
3201
- "Pragma": "no-cache",
3202
- "Expires": "0",
3203
- [CSP_NONCE_HEADER]: cspNonce
3204
- }
3205
- });
3206
- } catch (error) {
3207
- console.error("Error rendering CMS page:", error);
3208
- return new Response(generateErrorPage(error, `Error rendering template: ${cmsTemplatePath}`, cspNonce), {
3209
- headers: {
3210
- "Content-Type": "text/html; charset=utf-8",
3211
- "Cache-Control": "no-store",
3212
- [CSP_NONCE_HEADER]: cspNonce
3213
- }
3214
- });
3215
- }
3216
- }
3217
- }
3218
- }
3219
- const slug = pathWithoutLocale === "/" ? "" : pathWithoutLocale.substring(1);
3220
- const pageId = resolveSlugToPageId(slug, locale, slugIndex);
3221
- let lookupPath;
3222
- if (pageId) {
3223
- lookupPath = pageId === "index" ? "/" : `/${pageId}`;
3224
- } else {
3225
- lookupPath = pathWithoutLocale;
3226
- }
3227
- const isDevelopment = true;
3228
- if (!isDevelopment) {
3229
- const staticFilePath = getStaticFilePath(pagePath);
3230
- if (await fileExists(staticFilePath)) {
3231
- return serveFile(staticFilePath, {
3232
- "Content-Type": "text/html; charset=utf-8",
3233
- "Cache-Control": "public, max-age=31536000"
3234
- // 1 year cache for static files
3235
- });
3236
- }
3237
- }
3238
- const pageContent = context.draftPageStore?.get(lookupPath) ?? pageService.getPage(lookupPath);
3239
- if (pageContent) {
3240
- try {
3241
- const pageData = parseJSON(pageContent);
3242
- const globalComponentsRecord = componentService.getAllComponents();
3243
- const baseUrl = `${url.protocol}//${url.host}`;
3244
- if (injectLiveReload) {
3245
- const result = await generateSSRHTML({
3246
- pageData,
3247
- globalComponents: globalComponentsRecord,
3248
- pagePath,
3249
- baseUrl,
3250
- useBuiltBundle: true,
3251
- locale,
3252
- slugMappings,
3253
- cmsService,
3254
- pageLibraries: pageData.meta?.libraries,
3255
- pageCustomCode: pageData.meta?.customCode,
3256
- injectLiveReload,
3257
- injectEditorAttrs,
3258
- isEditor,
3259
- serverPort,
3260
- returnSeparateJS: true,
3261
- cspNonce
3262
- });
3263
- let finalHtml = result.html;
3264
- if (result.javascript) {
3265
- const hash = hashContent2(result.javascript);
3266
- cacheScript(hash, result.javascript);
3267
- finalHtml = finalHtml.replace("</body>", ` <script src="/_scripts/${hash}.js" defer></script>
3268
- </body>`);
3269
- }
3270
- return new Response(finalHtml, {
3271
- headers: {
3272
- "Content-Type": "text/html; charset=utf-8",
3273
- "Cache-Control": "no-store, max-age=0",
3274
- "Pragma": "no-cache",
3275
- "Expires": "0",
3276
- [CSP_NONCE_HEADER]: cspNonce
3277
- }
3278
- });
3279
- }
3280
- const ssrHTML = await generateSSRHTML({
3281
- pageData,
3282
- globalComponents: globalComponentsRecord,
3283
- pagePath,
3284
- baseUrl,
3285
- useBuiltBundle: false,
3286
- locale,
3287
- slugMappings,
3288
- cmsService,
3289
- pageLibraries: pageData.meta?.libraries,
3290
- pageCustomCode: pageData.meta?.customCode,
3291
- injectEditorAttrs,
3292
- isEditor,
3293
- cspNonce
3294
- });
3295
- return new Response(ssrHTML, {
3296
- headers: {
3297
- "Content-Type": "text/html; charset=utf-8",
3298
- "Cache-Control": "no-store, max-age=0",
3299
- "Pragma": "no-cache",
3300
- "Expires": "0",
3301
- [CSP_NONCE_HEADER]: cspNonce
3302
- }
3303
- });
3304
- } catch (error) {
3305
- console.error("Error rendering page:", error);
3306
- return new Response(generateErrorPage(error, `Error rendering page: ${lookupPath}`, cspNonce), {
3307
- headers: {
3308
- "Content-Type": "text/html; charset=utf-8",
3309
- "Cache-Control": "no-store",
3310
- [CSP_NONCE_HEADER]: cspNonce
3311
- }
3312
- });
3313
- }
3314
- } else {
3315
- return serveFile(packagePaths.indexHtml());
3316
- }
3317
- }
3318
- async function loadPageByFilePath(filePath) {
3319
- try {
3320
- if (await fileExists(filePath)) {
3321
- return await readTextFile(filePath);
3322
- }
3323
- return null;
3324
- } catch {
3325
- return null;
3326
- }
3327
- }
3328
-
3329
- // lib/server/routes/static.ts
3330
- async function handleStaticRoute(url, req) {
3331
- if (url.pathname === "/client-router.js" || url.pathname === "/client-router.tsx") {
3332
- const preBundledPath = packagePaths.clientRouter().replace(/\.tsx$/, ".js");
3333
- if (preBundledPath !== packagePaths.clientRouter() && await fileExists(preBundledPath)) {
3334
- return serveFile(preBundledPath, {
3335
- "Content-Type": "application/javascript",
3336
- "Cache-Control": "no-cache, no-store, must-revalidate"
3337
- });
3338
- }
3339
- const result = await bundleFile(packagePaths.clientRouter(), {
3340
- target: "browser",
3341
- format: "esm",
3342
- minify: false,
3343
- sourcemap: "inline"
3344
- });
3345
- if (result.success) {
3346
- return new Response(result.code, {
3347
- headers: {
3348
- "Content-Type": "application/javascript",
3349
- "Cache-Control": "no-cache, no-store, must-revalidate"
3350
- }
3351
- });
3352
- }
3353
- }
3354
- if (url.pathname.endsWith(".tsx") || url.pathname.endsWith(".ts")) {
3355
- const entrypoint = resolvePackagePath(url.pathname.slice(1));
3356
- if (!isPathWithinRoot(entrypoint, getPackageRoot())) {
3357
- return new Response("Forbidden", { status: 403 });
3358
- }
3359
- const result = await bundleFile(entrypoint, {
3360
- target: "browser",
3361
- format: "esm",
3362
- minify: false,
3363
- sourcemap: "inline"
3364
- });
3365
- if (result.success) {
3366
- return new Response(result.code, {
3367
- headers: {
3368
- "Content-Type": "application/javascript",
3369
- "Cache-Control": "no-cache, no-store, must-revalidate"
3370
- }
3371
- });
3372
- }
3373
- }
3374
- const decodedPathname = decodeURIComponent(url.pathname);
3375
- let filePath;
3376
- let rootPath;
3377
- if (decodedPathname.startsWith("/fonts/") || decodedPathname.startsWith("/images/") || decodedPathname.startsWith("/icons/") || decodedPathname.startsWith("/assets/") || decodedPathname.startsWith("/libraries/") || decodedPathname.startsWith("/videos/")) {
3378
- rootPath = getProjectRoot();
3379
- filePath = resolveProjectPath(decodedPathname.slice(1));
3380
- } else {
3381
- rootPath = getPackageRoot();
3382
- filePath = resolvePackagePath(decodedPathname.slice(1));
3383
- }
3384
- if (!isPathWithinRoot(filePath, rootPath)) {
3385
- return new Response("Forbidden", { status: 403 });
3386
- }
3387
- if (await fileExists(filePath)) {
3388
- const cacheControl = decodedPathname.startsWith("/libraries/") ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable";
3389
- return serveFile(filePath, { "Cache-Control": cacheControl });
3390
- }
3391
- return void 0;
3392
- }
3393
-
3394
- // lib/server/routes/index.ts
3395
- async function handleRoutes(req, url, server, context) {
3396
- const startTime = Date.now();
3397
- const { pageService, componentService, cmsService, cmsProvider } = context;
3398
- logRequest(req);
3399
- const corsResponse = handleCorsPreflight(req);
3400
- if (corsResponse) {
3401
- logResponseTime(startTime, req);
3402
- return corsResponse;
3403
- }
3404
- try {
3405
- if (url.pathname === HMR_ROUTE) {
3406
- logResponseTime(startTime, req);
3407
- return void 0;
3408
- }
3409
- if (url.pathname === "/__draft-page" && req.method === "POST") {
3410
- const corsHeaders = { "Access-Control-Allow-Origin": "*" };
3411
- const store = context.draftPageStore;
3412
- const ws = context.wsManager;
3413
- if (!store || !ws || ws.getClientCount() === 0) {
3414
- logResponseTime(startTime, req);
3415
- return new Response(null, { status: 204, headers: corsHeaders });
3416
- }
3417
- try {
3418
- const body = await req.json();
3419
- if (typeof body.path === "string" && typeof body.content === "string" && body.path.startsWith("/")) {
3420
- store.set(body.path, body.content);
3421
- ws.broadcastUpdate(body.path);
3422
- }
3423
- } catch {
3424
- }
3425
- logResponseTime(startTime, req);
3426
- return new Response(null, { status: 204, headers: corsHeaders });
3427
- }
3428
- if (url.pathname === "/__draft-page/clear" && req.method === "POST") {
3429
- const corsHeaders = { "Access-Control-Allow-Origin": "*" };
3430
- const store = context.draftPageStore;
3431
- if (store) {
3432
- try {
3433
- const body = await req.json();
3434
- if (typeof body.path === "string") {
3435
- store.clear(body.path);
3436
- context.wsManager?.broadcastUpdate(body.path);
3437
- }
3438
- } catch {
3439
- }
3440
- }
3441
- logResponseTime(startTime, req);
3442
- return new Response(null, { status: 204, headers: corsHeaders });
3443
- }
3444
- if (url.pathname === "/__draft-page/clients" && req.method === "GET") {
3445
- const count = context.wsManager?.getClientCount() ?? 0;
3446
- logResponseTime(startTime, req);
3447
- return new Response(JSON.stringify({ count }), {
3448
- status: 200,
3449
- headers: {
3450
- "Content-Type": "application/json",
3451
- "Cache-Control": "no-store",
3452
- "Access-Control-Allow-Origin": "*"
3453
- }
3454
- });
3455
- }
3456
- const cmsContext = cmsService && cmsProvider ? { cmsService, cmsProvider } : void 0;
3457
- const apiResponse = await handleApiRoutes(
3458
- req,
3459
- url,
3460
- pageService,
3461
- componentService,
3462
- cmsContext
3463
- );
3464
- if (apiResponse) {
3465
- logResponseTime(startTime, req);
3466
- return apiResponse;
3467
- }
3468
- if (url.pathname.startsWith("/data/") && url.pathname.endsWith("/index.json") && cmsService) {
3469
- const match = url.pathname.match(/^\/data\/([^/]+)\/index\.json$/);
3470
- if (match) {
3471
- const collectionId = match[1];
3472
- try {
3473
- const i18nConfig = await loadI18nConfig();
3474
- let refererPath = "/";
3475
- try {
3476
- const referer = req.headers.get("referer");
3477
- if (referer) refererPath = new URL(referer).pathname;
3478
- } catch {
3479
- }
3480
- const { locale } = parseLocaleFromPath(refererPath, i18nConfig);
3481
- const items = await cmsService.queryItems({ collection: collectionId, excludeDraftLocale: locale });
3482
- logResponseTime(startTime, req);
3483
- return new Response(JSON.stringify(items), {
3484
- headers: {
3485
- "Content-Type": "application/json",
3486
- "Cache-Control": "no-cache"
3487
- }
3488
- });
3489
- } catch {
3490
- logResponseTime(startTime, req);
3491
- return new Response("[]", {
3492
- headers: { "Content-Type": "application/json" }
3493
- });
3494
- }
3495
- }
3496
- }
3497
- if (url.pathname.startsWith("/_scripts/") && url.pathname.endsWith(".js")) {
3498
- const hash = url.pathname.slice(10, -3);
3499
- const script = getScript(hash);
3500
- if (script) {
3501
- logResponseTime(startTime, req);
3502
- return new Response(script, {
3503
- headers: {
3504
- "Content-Type": "application/javascript",
3505
- "Cache-Control": "no-cache"
3506
- }
3507
- });
3508
- }
3509
- }
3510
- if (url.pathname.startsWith("/images/") || url.pathname.startsWith("/videos/") || url.pathname.startsWith("/assets/") || url.pathname.startsWith("/public/") || url.pathname.startsWith("/icons/")) {
3511
- const response2 = await handleStaticRoute(url, req);
3512
- if (response2) {
3513
- logResponseTime(startTime, req);
3514
- return response2;
3515
- }
3516
- }
3517
- if (url.pathname.startsWith("/api/")) {
3518
- logResponseTime(startTime, req);
3519
- return new Response(
3520
- JSON.stringify({ error: "Not Found", path: url.pathname }),
3521
- { status: 404, headers: { "Content-Type": "application/json" } }
3522
- );
3523
- }
3524
- if (url.pathname === "/" || url.pathname.startsWith("/") && !url.pathname.includes(".")) {
3525
- const response2 = await handlePageRoute(url, context, req);
3526
- logResponseTime(startTime, req);
3527
- return response2;
3528
- }
3529
- const response = await handleStaticRoute(url, req);
3530
- logResponseTime(startTime, req);
3531
- return response;
3532
- } catch (error) {
3533
- logResponseTime(startTime, req);
3534
- return handleRouteError(
3535
- () => Promise.reject(error),
3536
- `Error handling route: ${url.pathname}`
3537
- );
3538
- }
3539
- }
3540
-
3541
- // lib/server/createServer.ts
3542
- init_constants();
3543
- var COLLAB_ROUTE = "/__collab";
3544
- async function createServer(config) {
3545
- const {
3546
- port: requestedPort = SERVER_PORT,
3547
- pageService,
3548
- componentService,
3549
- wsManager,
3550
- cmsService,
3551
- cmsProvider,
3552
- draftPageStore,
3553
- additionalRoutes = [],
3554
- onWSMessage,
3555
- injectLiveReload,
3556
- isEditor,
3557
- onCollabWSMessage,
3558
- onCollabWSOpen,
3559
- onCollabWSClose
3560
- } = config;
3561
- const collabClients = /* @__PURE__ */ new WeakSet();
3562
- const routeContext = {
3563
- pageService,
3564
- componentService,
3565
- cmsService,
3566
- cmsProvider,
3567
- injectLiveReload,
3568
- isEditor,
3569
- wsManager,
3570
- draftPageStore
3571
- };
3572
- let boundPort;
3573
- let lastError;
3574
- for (let portAttempt = 0; portAttempt < MAX_PORT_ATTEMPTS; portAttempt++) {
3575
- const currentPort = requestedPort + portAttempt;
3576
- try {
3577
- const server = await createRuntimeServer({
3578
- port: currentPort,
3579
- hostname: "localhost",
3580
- wsPath: HMR_ROUTE,
3581
- async fetch(req, upgradeWebSocket) {
3582
- const url = new URL(req.url);
3583
- for (const handler of additionalRoutes) {
3584
- const response2 = await handler(req, url, routeContext);
3585
- if (response2) {
3586
- return response2;
3587
- }
3588
- }
3589
- if (url.pathname === HMR_ROUTE) {
3590
- const success = upgradeWebSocket(req);
3591
- return success ? void 0 : new Response("WebSocket upgrade failed", { status: 500 });
3592
- }
3593
- if (url.pathname === COLLAB_ROUTE && onCollabWSMessage) {
3594
- const success = upgradeWebSocket(req);
3595
- return success ? void 0 : new Response("WebSocket upgrade failed", { status: 500 });
3596
- }
3597
- const response = await handleRoutes(req, url, null, routeContext);
3598
- return response || new Response("Not Found", { status: 404 });
3599
- },
3600
- websocket: {
3601
- open(ws) {
3602
- wsManager.addClient(ws);
3603
- onCollabWSOpen?.(ws);
3604
- },
3605
- message(ws, message) {
3606
- try {
3607
- const data = JSON.parse(message);
3608
- if (data.type === "ping") {
3609
- ws.send("pong");
3610
- return;
3611
- }
3612
- if (onCollabWSMessage && typeof data.type === "string" && data.type.startsWith("collab:")) {
3613
- collabClients.add(ws);
3614
- onCollabWSMessage(ws, data);
3615
- return;
3616
- }
3617
- if (onWSMessage) {
3618
- onWSMessage(ws, data);
3619
- }
3620
- } catch (error) {
3621
- }
3622
- },
3623
- close(ws) {
3624
- wsManager.removeClient(ws);
3625
- if (collabClients.has(ws)) {
3626
- onCollabWSClose?.(ws);
3627
- collabClients.delete(ws);
3628
- }
3629
- }
3630
- }
3631
- });
3632
- if (portAttempt > 0) {
3633
- console.log(`Warning: Port ${requestedPort} was busy, using port ${server.port} instead`);
3634
- }
3635
- routeContext.serverPort = server.port;
3636
- return { server, port: server.port };
3637
- } catch (error) {
3638
- lastError = error;
3639
- const err = error;
3640
- if (err?.code === "EADDRINUSE") {
3641
- if (portAttempt >= MAX_PORT_ATTEMPTS - 1) {
3642
- console.error(
3643
- `Failed to start server: All ports from ${requestedPort} to ${requestedPort + MAX_PORT_ATTEMPTS - 1} are busy`
3644
- );
3645
- throw error;
3646
- }
3647
- continue;
3648
- }
3649
- throw error;
3650
- }
3651
- }
3652
- throw lastError || new Error("Failed to start server");
3653
- }
3654
-
3655
- // lib/server/providers/fileSystemPageProvider.ts
3656
- import { existsSync as existsSync4, readdirSync as readdirSync3, mkdirSync as mkdirSync3, rmdirSync as rmdirSync3 } from "fs";
3657
- import { join as join4, dirname as dirname2 } from "path";
3658
-
3659
- // lib/shared/utils/fileUtils.ts
3660
- var isJSONFile = (name) => name.endsWith(".json");
3661
- var stripExtension = (name) => {
3662
- const lastDotIndex = name.lastIndexOf(".");
3663
- return lastDotIndex > 0 ? name.substring(0, lastDotIndex) : name;
3664
- };
3665
- var mapPageNameToPath2 = (pageName) => {
3666
- return pageName === "index" ? "/" : `/${pageName}`;
3667
- };
3668
- var mapPathToPageName = (path2) => {
3669
- return path2 === "/" ? "index" : path2.substring(1);
3670
- };
3671
-
3672
- // lib/server/providers/fileSystemPageProvider.ts
3673
- async function loadJSONFile2(filePath) {
3674
- try {
3675
- if (await fileExists(filePath)) {
3676
- return await readTextFile(filePath);
3677
- }
3678
- return null;
3679
- } catch {
3680
- return null;
3681
- }
3682
- }
3683
- var FileSystemPageProvider = class {
3684
- constructor(pagesDir, templatesDir) {
3685
- this.pagesDir = pagesDir;
3686
- this.templatesDir = templatesDir;
3687
- }
3688
- async loadAll() {
3689
- const pages = /* @__PURE__ */ new Map();
3690
- if (existsSync4(this.pagesDir)) {
3691
- await this.scanDirectory(this.pagesDir, "", pages);
3692
- }
3693
- if (this.templatesDir && existsSync4(this.templatesDir)) {
3694
- await this.scanDirectory(this.templatesDir, "templates", pages);
3695
- }
3696
- return pages;
3697
- }
3698
- /**
3699
- * Recursively scan a directory for JSON page files
3700
- * @param dir - Absolute directory path to scan
3701
- * @param prefix - Relative prefix for page names (e.g., '' for root, 'blog' for blog/)
3702
- */
3703
- async scanDirectory(dir, prefix, pages) {
3704
- const entries = readdirSync3(dir, { withFileTypes: true });
3705
- const fileEntries = entries.filter((e) => e.isFile() && isJSONFile(e.name));
3706
- const dirEntries = entries.filter((e) => e.isDirectory());
3707
- await Promise.all([
3708
- ...fileEntries.map(async (entry) => {
3709
- const pageName = prefix ? `${prefix}/${stripExtension(entry.name)}` : stripExtension(entry.name);
3710
- const content = await loadJSONFile2(join4(dir, entry.name));
3711
- if (content) {
3712
- const pagePath = mapPageNameToPath2(pageName);
3713
- pages.set(pagePath, content);
3714
- }
3715
- }),
3716
- ...dirEntries.map((entry) => {
3717
- const subPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
3718
- return this.scanDirectory(join4(dir, entry.name), subPrefix, pages);
3719
- })
3720
- ]);
3721
- }
3722
- /**
3723
- * Resolve a page path to its filesystem file path
3724
- * Routes /templates/* paths to the templates directory
3725
- */
3726
- resolveFilePath(path2) {
3727
- const pageName = mapPathToPageName(path2);
3728
- if (this.templatesDir && pageName.startsWith("templates/")) {
3729
- const templateName = pageName.substring("templates/".length);
3730
- return join4(this.templatesDir, `${templateName}.json`);
3731
- }
3732
- return join4(this.pagesDir, `${pageName}.json`);
3733
- }
3734
- /**
3735
- * Resolve the root directory for cleanup operations
3736
- * Returns the appropriate root dir (templates or pages) for a given path
3737
- */
3738
- resolveRootDir(path2) {
3739
- const pageName = mapPathToPageName(path2);
3740
- if (this.templatesDir && pageName.startsWith("templates/")) {
3741
- return this.templatesDir;
3742
- }
3743
- return this.pagesDir;
3744
- }
3745
- async get(path2) {
3746
- const filePath = this.resolveFilePath(path2);
3747
- return loadJSONFile2(filePath);
3748
- }
3749
- async save(path2, content) {
3750
- const { writeFile: writeFile2 } = await import("fs/promises");
3751
- const filePath = this.resolveFilePath(path2);
3752
- const dir = dirname2(filePath);
3753
- if (!existsSync4(dir)) {
3754
- mkdirSync3(dir, { recursive: true });
3755
- }
3756
- await writeFile2(filePath, content, "utf-8");
3757
- }
3758
- async delete(path2) {
3759
- const { unlink } = await import("fs/promises");
3760
- const filePath = this.resolveFilePath(path2);
3761
- const rootDir = this.resolveRootDir(path2);
3762
- if (existsSync4(filePath)) {
3763
- await unlink(filePath);
3764
- let dir = dirname2(filePath);
3765
- while (dir !== rootDir) {
3766
- try {
3767
- const remaining = readdirSync3(dir);
3768
- if (remaining.length === 0) {
3769
- rmdirSync3(dir);
3770
- } else {
3771
- break;
3772
- }
3773
- } catch {
3774
- break;
3775
- }
3776
- dir = dirname2(dir);
3777
- }
3778
- }
3779
- }
3780
- async exists(path2) {
3781
- const filePath = this.resolveFilePath(path2);
3782
- return existsSync4(filePath);
3783
- }
3784
- baseDir() {
3785
- return this.pagesDir;
3786
- }
3787
- extension() {
3788
- return ".json";
3789
- }
3790
- };
3791
-
3792
- export {
3793
- PageCache,
3794
- WebSocketManager,
3795
- buildLineMap,
3796
- lineMapToObject,
3797
- PageService,
3798
- ComponentService,
3799
- EnumService,
3800
- enumService,
3801
- FileWatcherService,
3802
- createCorsHeaders,
3803
- handleCorsPreflight,
3804
- createErrorResponse,
3805
- handleRouteError,
3806
- withErrorHandling,
3807
- logRequest,
3808
- logError,
3809
- logResponseTime,
3810
- withLogging,
3811
- jsonResponse,
3812
- errorResponse,
3813
- handleCollectionsRoute,
3814
- handleCollectionSchemaRoute,
3815
- handleItemsRoute,
3816
- handleItemRoute,
3817
- handleCoreApiRoutes,
3818
- createServer,
3819
- FileSystemPageProvider
3820
- };
3821
- //# sourceMappingURL=chunk-R6XHAFBF.js.map