meno-core 1.0.53 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +66 -261
  3. package/build-astro.ts +150 -128
  4. package/dist/bin/cli.js +42 -185
  5. package/dist/bin/cli.js.map +2 -2
  6. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  7. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  8. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
  9. package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
  10. package/dist/chunks/chunk-4ZRU52J2.js +169 -0
  11. package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
  12. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
  13. package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
  14. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
  15. package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
  16. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
  17. package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
  18. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
  19. package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
  20. package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
  21. package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
  22. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
  23. package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
  24. package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
  25. package/dist/lib/client/index.js +1003 -475
  26. package/dist/lib/client/index.js.map +3 -3
  27. package/dist/lib/server/index.js +14393 -6371
  28. package/dist/lib/server/index.js.map +4 -4
  29. package/dist/lib/shared/index.js +342 -101
  30. package/dist/lib/shared/index.js.map +4 -4
  31. package/dist/lib/shared/richtext/index.js +1 -1
  32. package/dist/lib/test-utils/index.js +64 -80
  33. package/dist/lib/test-utils/index.js.map +2 -2
  34. package/entries/client-router.tsx +10 -8
  35. package/lib/client/ClientInitializer.ts +8 -8
  36. package/lib/client/ErrorBoundary.test.tsx +246 -186
  37. package/lib/client/ErrorBoundary.tsx +183 -132
  38. package/lib/client/componentRegistry.test.ts +96 -108
  39. package/lib/client/componentRegistry.ts +1 -3
  40. package/lib/client/contexts/ThemeContext.tsx +3 -2
  41. package/lib/client/core/ComponentBuilder.test.ts +663 -560
  42. package/lib/client/core/ComponentBuilder.ts +427 -155
  43. package/lib/client/core/ComponentRenderer.test.tsx +2 -4
  44. package/lib/client/core/ComponentRenderer.tsx +46 -33
  45. package/lib/client/core/builders/embedBuilder.ts +242 -54
  46. package/lib/client/core/builders/linkBuilder.ts +72 -45
  47. package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
  48. package/lib/client/core/builders/listBuilder.ts +155 -89
  49. package/lib/client/core/builders/localeListBuilder.ts +97 -62
  50. package/lib/client/core/builders/types.ts +5 -5
  51. package/lib/client/core/cmsTemplateProcessor.ts +8 -9
  52. package/lib/client/elementRegistry.ts +3 -3
  53. package/lib/client/fontFamiliesService.test.ts +2 -10
  54. package/lib/client/fontFamiliesService.ts +3 -3
  55. package/lib/client/hmr/HMRManager.tsx +47 -30
  56. package/lib/client/hmrCssReload.ts +49 -14
  57. package/lib/client/hmrWebSocket.ts +14 -19
  58. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  59. package/lib/client/hooks/useColorVariables.ts +23 -18
  60. package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
  61. package/lib/client/hooks/useVariables.ts +6 -4
  62. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  63. package/lib/client/hydration/HydrationUtils.ts +3 -4
  64. package/lib/client/i18nConfigService.test.ts +2 -7
  65. package/lib/client/i18nConfigService.ts +2 -2
  66. package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
  67. package/lib/client/meno-filter/MenoFilter.ts +14 -15
  68. package/lib/client/meno-filter/bindings.ts +18 -43
  69. package/lib/client/meno-filter/init.ts +4 -4
  70. package/lib/client/meno-filter/renderer.ts +30 -32
  71. package/lib/client/meno-filter/script.generated.ts +1 -3
  72. package/lib/client/meno-filter/ui.ts +4 -6
  73. package/lib/client/meno-filter/updates.ts +18 -23
  74. package/lib/client/meno-filter/utils.ts +1 -1
  75. package/lib/client/navigation.test.ts +174 -178
  76. package/lib/client/navigation.ts +0 -1
  77. package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
  78. package/lib/client/responsiveStyleResolver.test.ts +234 -232
  79. package/lib/client/responsiveStyleResolver.ts +74 -20
  80. package/lib/client/routing/RouteLoader.test.ts +24 -25
  81. package/lib/client/routing/RouteLoader.ts +27 -35
  82. package/lib/client/routing/Router.tsx +210 -153
  83. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  84. package/lib/client/scripts/ScriptExecutor.ts +18 -36
  85. package/lib/client/services/PrefetchService.test.ts +3 -3
  86. package/lib/client/services/PrefetchService.ts +23 -29
  87. package/lib/client/styleProcessor.test.ts +9 -9
  88. package/lib/client/styleProcessor.ts +24 -15
  89. package/lib/client/styles/StyleInjector.test.ts +122 -115
  90. package/lib/client/styles/StyleInjector.ts +10 -7
  91. package/lib/client/styles/UtilityClassCollector.ts +62 -28
  92. package/lib/client/styles/cspNonce.test.ts +2 -5
  93. package/lib/client/templateEngine.test.ts +554 -488
  94. package/lib/client/templateEngine.ts +203 -73
  95. package/lib/client/theme.test.ts +1 -1
  96. package/lib/client/theme.ts +0 -1
  97. package/lib/client/utils/toast.ts +0 -1
  98. package/lib/server/__integration__/api-routes.test.ts +8 -4
  99. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  100. package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
  101. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  102. package/lib/server/__integration__/static-assets.test.ts +3 -3
  103. package/lib/server/__integration__/test-helpers.ts +87 -74
  104. package/lib/server/ab/generateFunctions.ts +12 -10
  105. package/lib/server/astro/cmsPageEmitter.ts +26 -42
  106. package/lib/server/astro/componentEmitter.ts +35 -43
  107. package/lib/server/astro/cssCollector.ts +10 -26
  108. package/lib/server/astro/nodeToAstro.test.ts +1754 -34
  109. package/lib/server/astro/nodeToAstro.ts +232 -216
  110. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
  111. package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
  112. package/lib/server/astro/pageEmitter.ts +9 -13
  113. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  114. package/lib/server/astro/tailwindMapper.ts +33 -40
  115. package/lib/server/astro/templateTransformer.ts +17 -20
  116. package/lib/server/createServer.ts +7 -19
  117. package/lib/server/cssGenerator.test.ts +35 -44
  118. package/lib/server/cssGenerator.ts +8 -19
  119. package/lib/server/fileWatcher.test.ts +164 -13
  120. package/lib/server/fileWatcher.ts +209 -124
  121. package/lib/server/index.ts +14 -17
  122. package/lib/server/jsonLoader.test.ts +39 -2
  123. package/lib/server/jsonLoader.ts +39 -34
  124. package/lib/server/middleware/cors.test.ts +21 -21
  125. package/lib/server/middleware/cors.ts +28 -4
  126. package/lib/server/middleware/errorHandler.test.ts +7 -5
  127. package/lib/server/middleware/errorHandler.ts +3 -8
  128. package/lib/server/middleware/index.ts +0 -1
  129. package/lib/server/middleware/logger.test.ts +10 -8
  130. package/lib/server/middleware/logger.ts +17 -29
  131. package/lib/server/migrateTemplates.ts +3 -3
  132. package/lib/server/pageCache.test.ts +76 -77
  133. package/lib/server/pageCache.ts +0 -1
  134. package/lib/server/projectContext.ts +5 -4
  135. package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
  136. package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
  137. package/lib/server/routes/api/cms.test.ts +26 -14
  138. package/lib/server/routes/api/cms.ts +9 -14
  139. package/lib/server/routes/api/colors.test.ts +2 -2
  140. package/lib/server/routes/api/components.ts +31 -35
  141. package/lib/server/routes/api/config.ts +3 -4
  142. package/lib/server/routes/api/core-routes.ts +49 -76
  143. package/lib/server/routes/api/enums.test.ts +2 -2
  144. package/lib/server/routes/api/functions.ts +9 -17
  145. package/lib/server/routes/api/index.ts +3 -6
  146. package/lib/server/routes/api/pages.ts +8 -11
  147. package/lib/server/routes/api/shared.test.ts +1 -1
  148. package/lib/server/routes/api/shared.ts +12 -9
  149. package/lib/server/routes/api/variables.test.ts +3 -5
  150. package/lib/server/routes/api/variables.ts +1 -1
  151. package/lib/server/routes/index.ts +25 -28
  152. package/lib/server/routes/pages.ts +35 -31
  153. package/lib/server/routes/static.ts +17 -6
  154. package/lib/server/runtime/bundler.ts +98 -61
  155. package/lib/server/runtime/fs.ts +16 -26
  156. package/lib/server/runtime/httpServer.ts +8 -12
  157. package/lib/server/services/ColorService.test.ts +3 -3
  158. package/lib/server/services/ColorService.ts +37 -29
  159. package/lib/server/services/EnumService.test.ts +4 -8
  160. package/lib/server/services/EnumService.ts +11 -3
  161. package/lib/server/services/VariableService.test.ts +8 -12
  162. package/lib/server/services/VariableService.ts +11 -3
  163. package/lib/server/services/cmsService.test.ts +158 -120
  164. package/lib/server/services/cmsService.ts +36 -146
  165. package/lib/server/services/componentService.test.ts +125 -45
  166. package/lib/server/services/componentService.ts +70 -46
  167. package/lib/server/services/configService.test.ts +22 -45
  168. package/lib/server/services/configService.ts +50 -27
  169. package/lib/server/services/fileWatcherService.ts +42 -8
  170. package/lib/server/services/index.ts +0 -1
  171. package/lib/server/services/pageService.test.ts +8 -10
  172. package/lib/server/services/pageService.ts +32 -18
  173. package/lib/server/ssr/attributeBuilder.ts +18 -8
  174. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  175. package/lib/server/ssr/clientDataInjector.ts +7 -21
  176. package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
  177. package/lib/server/ssr/cssCollector.ts +1 -1
  178. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  179. package/lib/server/ssr/errorOverlay.ts +3 -9
  180. package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
  181. package/lib/server/ssr/htmlGenerator.test.ts +120 -44
  182. package/lib/server/ssr/htmlGenerator.ts +126 -91
  183. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  184. package/lib/server/ssr/imageMetadata.ts +27 -21
  185. package/lib/server/ssr/jsCollector.test.ts +8 -18
  186. package/lib/server/ssr/jsCollector.ts +3 -8
  187. package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
  188. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  189. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  190. package/lib/server/ssr/ssrRenderer.test.ts +316 -256
  191. package/lib/server/ssr/ssrRenderer.ts +795 -243
  192. package/lib/server/ssrRenderer.test.ts +1048 -954
  193. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  194. package/lib/server/utils/jsonLineMapper.ts +7 -7
  195. package/lib/server/validateStyleCoverage.ts +18 -21
  196. package/lib/server/websocketManager.ts +28 -19
  197. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  198. package/lib/shared/attributeNodeUtils.ts +5 -12
  199. package/lib/shared/breakpoints.test.ts +5 -5
  200. package/lib/shared/breakpoints.ts +4 -11
  201. package/lib/shared/cmsQuery.test.ts +81 -0
  202. package/lib/shared/cmsQuery.ts +92 -0
  203. package/lib/shared/cmsQueryParser.test.ts +50 -42
  204. package/lib/shared/cmsQueryParser.ts +60 -38
  205. package/lib/shared/colorConversions.ts +9 -9
  206. package/lib/shared/colorVariableUtils.test.ts +27 -5
  207. package/lib/shared/colorVariableUtils.ts +21 -12
  208. package/lib/shared/componentRefs.ts +1 -5
  209. package/lib/shared/constants.test.ts +3 -3
  210. package/lib/shared/constants.ts +8 -8
  211. package/lib/shared/cssGeneration.test.ts +285 -144
  212. package/lib/shared/cssGeneration.ts +217 -531
  213. package/lib/shared/cssNamedColors.ts +152 -30
  214. package/lib/shared/cssProperties.test.ts +69 -70
  215. package/lib/shared/cssProperties.ts +298 -114
  216. package/lib/shared/elementClassName.test.ts +109 -109
  217. package/lib/shared/elementClassName.ts +3 -3
  218. package/lib/shared/elementUtils.ts +12 -16
  219. package/lib/shared/errorLogger.test.ts +12 -12
  220. package/lib/shared/errorLogger.ts +2 -10
  221. package/lib/shared/errors.test.ts +2 -13
  222. package/lib/shared/errors.ts +2 -8
  223. package/lib/shared/expressionEvaluator.test.ts +145 -0
  224. package/lib/shared/expressionEvaluator.ts +52 -24
  225. package/lib/shared/fontLoader.test.ts +32 -18
  226. package/lib/shared/fontLoader.ts +2 -2
  227. package/lib/shared/friendlyError.ts +2 -3
  228. package/lib/shared/gradientUtils.test.ts +3 -7
  229. package/lib/shared/gradientUtils.ts +17 -14
  230. package/lib/shared/hrefRefs.test.ts +2 -2
  231. package/lib/shared/hrefRefs.ts +2 -10
  232. package/lib/shared/i18n.test.ts +4 -4
  233. package/lib/shared/i18n.ts +18 -38
  234. package/lib/shared/index.ts +27 -0
  235. package/lib/shared/inlineSvgStyleRules.ts +14 -13
  236. package/lib/shared/interactiveStyleMappings.test.ts +32 -54
  237. package/lib/shared/interactiveStyleMappings.ts +11 -24
  238. package/lib/shared/interactiveStyles.test.ts +165 -188
  239. package/lib/shared/interfaces/contentProvider.ts +1 -1
  240. package/lib/shared/itemTemplateUtils.test.ts +11 -17
  241. package/lib/shared/itemTemplateUtils.ts +27 -39
  242. package/lib/shared/jsonRepair.ts +16 -10
  243. package/lib/shared/libraryLoader.test.ts +21 -55
  244. package/lib/shared/libraryLoader.ts +7 -22
  245. package/lib/shared/linkUtils.ts +1 -1
  246. package/lib/shared/logger.test.ts +66 -0
  247. package/lib/shared/logger.ts +94 -0
  248. package/lib/shared/markdown.ts +25 -0
  249. package/lib/shared/netlifyLocale404.test.ts +179 -0
  250. package/lib/shared/netlifyLocale404.ts +110 -0
  251. package/lib/shared/nodeUtils.test.ts +28 -16
  252. package/lib/shared/nodeUtils.ts +76 -23
  253. package/lib/shared/pathArrayUtils.test.ts +1 -2
  254. package/lib/shared/pathArrayUtils.ts +1 -1
  255. package/lib/shared/pathSecurity.ts +2 -2
  256. package/lib/shared/pathUtils.test.ts +4 -6
  257. package/lib/shared/pathUtils.ts +42 -49
  258. package/lib/shared/paths/Path.test.ts +2 -2
  259. package/lib/shared/paths/Path.ts +0 -1
  260. package/lib/shared/paths/PathConverter.test.ts +1 -1
  261. package/lib/shared/paths/PathConverter.ts +17 -20
  262. package/lib/shared/paths/PathUtils.ts +13 -13
  263. package/lib/shared/paths/PathValidator.test.ts +2 -15
  264. package/lib/shared/paths/PathValidator.ts +12 -10
  265. package/lib/shared/paths/index.ts +1 -2
  266. package/lib/shared/permissions.test.ts +168 -0
  267. package/lib/shared/permissions.ts +162 -0
  268. package/lib/shared/propResolver.test.ts +240 -244
  269. package/lib/shared/propResolver.ts +15 -26
  270. package/lib/shared/pxToRem.test.ts +7 -6
  271. package/lib/shared/pxToRem.ts +2 -5
  272. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
  273. package/lib/shared/registry/ClientRegistry.ts +0 -2
  274. package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
  275. package/lib/shared/registry/ComponentRegistry.ts +12 -9
  276. package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
  277. package/lib/shared/registry/RegistryManager.ts +1 -2
  278. package/lib/shared/registry/SSRRegistry.ts +0 -2
  279. package/lib/shared/registry/createNodeType.ts +7 -9
  280. package/lib/shared/registry/defineNodeType.ts +2 -6
  281. package/lib/shared/registry/fieldPresets.ts +3 -1
  282. package/lib/shared/registry/index.ts +0 -1
  283. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  284. package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
  285. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  286. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
  287. package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
  288. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  289. package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
  290. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  291. package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
  292. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
  293. package/lib/shared/registry/nodeTypes/index.ts +26 -3
  294. package/lib/shared/responsiveScaling.test.ts +16 -34
  295. package/lib/shared/responsiveScaling.ts +61 -42
  296. package/lib/shared/responsiveStyleUtils.test.ts +0 -1
  297. package/lib/shared/responsiveStyleUtils.ts +11 -13
  298. package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
  299. package/lib/shared/richtext/htmlToTiptap.ts +2 -4
  300. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  301. package/lib/shared/richtext/types.ts +1 -8
  302. package/lib/shared/slugTranslator.test.ts +13 -13
  303. package/lib/shared/slugTranslator.ts +12 -16
  304. package/lib/shared/slugify.ts +9 -15
  305. package/lib/shared/styleNodeUtils.test.ts +8 -8
  306. package/lib/shared/styleNodeUtils.ts +18 -20
  307. package/lib/shared/styleUtils.test.ts +87 -61
  308. package/lib/shared/styleUtils.ts +5 -6
  309. package/lib/shared/themeDefaults.test.ts +11 -14
  310. package/lib/shared/themeDefaults.ts +3 -4
  311. package/lib/shared/tree/PathBuilder.test.ts +63 -145
  312. package/lib/shared/tree/PathBuilder.ts +59 -47
  313. package/lib/shared/treePathUtils.test.ts +51 -12
  314. package/lib/shared/treePathUtils.ts +91 -60
  315. package/lib/shared/types/api.ts +105 -8
  316. package/lib/shared/types/cms.test.ts +34 -0
  317. package/lib/shared/types/cms.ts +90 -34
  318. package/lib/shared/types/comment.ts +49 -16
  319. package/lib/shared/types/components.ts +51 -25
  320. package/lib/shared/types/errors.test.ts +1 -6
  321. package/lib/shared/types/errors.ts +3 -7
  322. package/lib/shared/types/experiments.ts +28 -28
  323. package/lib/shared/types/index.ts +25 -2
  324. package/lib/shared/types/permissions.ts +139 -0
  325. package/lib/shared/types/styles.ts +0 -1
  326. package/lib/shared/types/variables.test.ts +4 -13
  327. package/lib/shared/types/variables.ts +51 -30
  328. package/lib/shared/types.ts +1 -2
  329. package/lib/shared/utilityClassConfig.ts +751 -321
  330. package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
  331. package/lib/shared/utilityClassMapper.test.ts +433 -95
  332. package/lib/shared/utilityClassMapper.ts +322 -249
  333. package/lib/shared/utilityClassNames.test.ts +38 -0
  334. package/lib/shared/utilityClassNames.ts +458 -0
  335. package/lib/shared/utils.test.ts +2 -10
  336. package/lib/shared/utils.ts +19 -10
  337. package/lib/shared/validation/cmsValidators.ts +2 -1
  338. package/lib/shared/validation/commentValidators.test.ts +53 -0
  339. package/lib/shared/validation/commentValidators.ts +12 -1
  340. package/lib/shared/validation/index.ts +1 -0
  341. package/lib/shared/validation/permissionsValidators.test.ts +49 -0
  342. package/lib/shared/validation/permissionsValidators.ts +49 -0
  343. package/lib/shared/validation/propValidator.test.ts +19 -21
  344. package/lib/shared/validation/propValidator.ts +13 -18
  345. package/lib/shared/validation/schemas.test.ts +64 -32
  346. package/lib/shared/validation/schemas.ts +616 -345
  347. package/lib/shared/validation/validators.test.ts +3 -8
  348. package/lib/shared/validation/validators.ts +89 -68
  349. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  350. package/lib/shared/viewportUnits.test.ts +24 -23
  351. package/lib/shared/viewportUnits.ts +12 -5
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
  354. package/lib/test-utils/factories/DomMockFactory.ts +17 -23
  355. package/lib/test-utils/factories/EventMockFactory.ts +7 -13
  356. package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
  357. package/lib/test-utils/factories/ServerMockFactory.ts +6 -10
  358. package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
  359. package/lib/test-utils/fixtures.ts +45 -45
  360. package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
  361. package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
  362. package/lib/test-utils/helpers.ts +1 -5
  363. package/lib/test-utils/index.ts +0 -4
  364. package/lib/test-utils/mockFactories.ts +13 -19
  365. package/lib/test-utils/mocks.ts +6 -4
  366. package/package.json +3 -5
  367. package/scripts/build-for-publish.mjs +2 -4
  368. package/scripts/build-meno-filter.ts +4 -7
  369. package/tsconfig.json +1 -0
  370. package/vite.config.ts +4 -4
  371. package/build-next.ts +0 -1361
  372. package/build-static.test.ts +0 -424
  373. package/build-static.ts +0 -1074
  374. package/dist/build-static.js +0 -36
  375. package/dist/chunks/chunk-2MHDV5BF.js +0 -251
  376. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  377. package/dist/chunks/chunk-AZQYF6KE.js +0 -559
  378. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  379. package/dist/chunks/chunk-GZHGVVW3.js +0 -322
  380. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  381. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  382. package/dist/chunks/chunk-I7YIGZXT.js +0 -467
  383. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  384. package/dist/chunks/chunk-IGYR22T6.js +0 -6192
  385. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  386. package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
  387. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  388. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  389. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  390. package/dist/chunks/chunk-QB2LNO4W.js +0 -77
  391. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  392. package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
  393. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  394. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  395. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  396. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  397. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  398. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  399. package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
  400. package/dist/chunks/constants-STK2YBIW.js +0 -46
  401. package/dist/chunks/constants-STK2YBIW.js.map +0 -7
  402. package/dist/chunks/fs-JGINUXGL.js.map +0 -7
  403. package/dist/entries/server-router.js +0 -67
  404. package/dist/entries/server-router.js.map +0 -7
  405. package/entries/server-router.tsx +0 -81
  406. package/lib/server/providers/fileSystemPageProvider.ts +0 -160
  407. package/lib/server/webflow/buildWebflow.ts +0 -635
  408. package/lib/server/webflow/index.ts +0 -23
  409. package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
  410. package/lib/server/webflow/nodeToWebflow.ts +0 -2435
  411. package/lib/server/webflow/styleMapper.test.ts +0 -389
  412. package/lib/server/webflow/styleMapper.ts +0 -695
  413. package/lib/server/webflow/templateWrapper.ts +0 -49
  414. package/lib/server/webflow/types.ts +0 -396
  415. /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Leveled logger — the single console abstraction for Meno runtime/server code.
3
+ *
4
+ * Why: the codebase had hundreds of raw `console.*` calls with no way to gate
5
+ * verbosity in the field (a packaged desktop app emits everything, with no level
6
+ * control and no telemetry path). This module adds:
7
+ * - levels (silent < error < warn < info < debug), gated by `MENO_LOG_LEVEL`
8
+ * (or `NODE_ENV`: `production` → warn, otherwise → debug);
9
+ * - per-scope loggers (`createLogger('AstroProvider')`) that prefix messages;
10
+ * - a pluggable sink (`setLogSink`) so the Electron main process / telemetry can
11
+ * capture logs and crashes instead of (or in addition to) the console.
12
+ *
13
+ * Isomorphic: works in Node and the browser. Error reporting still flows through
14
+ * `errorLogger` (`logRuntimeError`/`logNetworkError`); this is for general
15
+ * debug/info/warn/error console output.
16
+ */
17
+
18
+ export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
19
+
20
+ const LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
21
+
22
+ /** A record forwarded to the sink for every emitted log line. */
23
+ export interface LogRecord {
24
+ level: Exclude<LogLevel, 'silent'>;
25
+ scope: string;
26
+ message: string;
27
+ args: unknown[];
28
+ timestamp: number;
29
+ }
30
+
31
+ type LogSink = (record: LogRecord) => void;
32
+
33
+ let sink: LogSink | null = null;
34
+
35
+ /** Install a sink (telemetry / Electron capture). Pass `null` to remove. */
36
+ export function setLogSink(next: LogSink | null): void {
37
+ sink = next;
38
+ }
39
+
40
+ function defaultLevel(): LogLevel {
41
+ const env = typeof process !== 'undefined' ? process.env : undefined;
42
+ const explicit = env?.MENO_LOG_LEVEL?.toLowerCase();
43
+ if (explicit && explicit in LEVEL_RANK) return explicit as LogLevel;
44
+ return env?.NODE_ENV === 'production' ? 'warn' : 'debug';
45
+ }
46
+
47
+ let currentLevel: LogLevel = defaultLevel();
48
+
49
+ /** Override the active level at runtime (e.g. a debug toggle in the app). */
50
+ export function setLogLevel(level: LogLevel): void {
51
+ currentLevel = level;
52
+ }
53
+
54
+ /** The active level (resolved from env at module load unless overridden). */
55
+ export function getLogLevel(): LogLevel {
56
+ return currentLevel;
57
+ }
58
+
59
+ const CONSOLE: Record<Exclude<LogLevel, 'silent'>, (...a: unknown[]) => void> = {
60
+ error: (...a) => console.error(...a),
61
+ warn: (...a) => console.warn(...a),
62
+ info: (...a) => console.info(...a),
63
+ debug: (...a) => console.debug(...a),
64
+ };
65
+
66
+ function emit(level: Exclude<LogLevel, 'silent'>, scope: string, message: string, args: unknown[]): void {
67
+ if (LEVEL_RANK[level] > LEVEL_RANK[currentLevel]) return;
68
+ const prefix = scope ? `[${scope}]` : '';
69
+ CONSOLE[level](`${prefix} ${message}`.trimStart(), ...args);
70
+ sink?.({ level, scope, message, args, timestamp: Date.now() });
71
+ }
72
+
73
+ export interface Logger {
74
+ debug(message: string, ...args: unknown[]): void;
75
+ info(message: string, ...args: unknown[]): void;
76
+ warn(message: string, ...args: unknown[]): void;
77
+ error(message: string, ...args: unknown[]): void;
78
+ /** A child logger nested under this scope (`parent:child`). */
79
+ child(scope: string): Logger;
80
+ }
81
+
82
+ /** Create a scoped logger. `scope` is prefixed to every line and forwarded to the sink. */
83
+ export function createLogger(scope = ''): Logger {
84
+ return {
85
+ debug: (message, ...args) => emit('debug', scope, message, args),
86
+ info: (message, ...args) => emit('info', scope, message, args),
87
+ warn: (message, ...args) => emit('warn', scope, message, args),
88
+ error: (message, ...args) => emit('error', scope, message, args),
89
+ child: (childScope) => createLogger(scope ? `${scope}:${childScope}` : childScope),
90
+ };
91
+ }
92
+
93
+ /** A default unscoped logger for ad-hoc use. Prefer a scoped `createLogger('Area')`. */
94
+ export const logger = createLogger();
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Markdown → HTML for the `markdown` node type.
3
+ *
4
+ * A Meno `markdown` node stores its body as a verbatim Markdown string (`node.source`).
5
+ * meno-core renders it to HTML here for both the editor canvas (client renderer) and SSR
6
+ * (design-canvas preview / JSON-format SSR). The meno-astro build runtime has its OWN
7
+ * `renderMarkdown` (packages/astro/lib/runtime/markdown.ts) — kept separate so a new
8
+ * meno-core export never has to reach the published Astro play runtime (see the
9
+ * runtime-uses-npm-meno-core constraint). Both wrap the same `markdown-it` config, so the
10
+ * canvas preview and the real Astro build render identically.
11
+ *
12
+ * `html: true` lets authors embed raw HTML in Markdown (parity with typical Markdown +
13
+ * the embed node, which also passes author HTML through). Content is author-trusted (the
14
+ * editor is the source of truth), like the `customCode` / `embed` paths.
15
+ */
16
+
17
+ import MarkdownIt from 'markdown-it';
18
+
19
+ const md = new MarkdownIt({ html: true, linkify: true, breaks: false });
20
+
21
+ /** Render a Markdown source string to an HTML string. Empty/non-string → "". */
22
+ export function renderMarkdown(source: unknown): string {
23
+ if (typeof source !== 'string' || source.trim() === '') return '';
24
+ return md.render(source);
25
+ }
@@ -0,0 +1,179 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ netlifyLocale404Redirects,
4
+ syncNetlifyLocale404Block,
5
+ NETLIFY_LOCALE_404_BEGIN,
6
+ NETLIFY_LOCALE_404_END,
7
+ } from './netlifyLocale404';
8
+ import type { I18nConfig } from './types';
9
+
10
+ const MULTI: I18nConfig = {
11
+ defaultLocale: 'en',
12
+ locales: [
13
+ { code: 'en', name: 'EN', nativeName: 'EN', langTag: 'en-US' },
14
+ { code: 'pl', name: 'PL', nativeName: 'PL', langTag: 'pl-PL' },
15
+ { code: 'de', name: 'DE', nativeName: 'DE', langTag: 'de-DE' },
16
+ ],
17
+ };
18
+
19
+ const EN_PL: I18nConfig = {
20
+ defaultLocale: 'en',
21
+ locales: [MULTI.locales[0]!, MULTI.locales[1]!],
22
+ };
23
+
24
+ const SINGLE: I18nConfig = { defaultLocale: 'en', locales: [MULTI.locales[0]!] };
25
+
26
+ const BASE_TOML = `# Scaffold generated by Meno.
27
+ [build]
28
+ command = "npm run build"
29
+ publish = "dist"
30
+
31
+ [build.environment]
32
+ NODE_VERSION = "22"
33
+ `;
34
+
35
+ describe('netlifyLocale404Redirects (pure generator)', () => {
36
+ test('one non-shadowing 404 rewrite per non-default locale', () => {
37
+ const toml = netlifyLocale404Redirects(MULTI);
38
+ for (const code of ['pl', 'de']) {
39
+ expect(toml).toContain(`from = "/${code}/*"`);
40
+ expect(toml).toContain(`to = "/${code}/404/index.html"`);
41
+ }
42
+ // status 404 (a rewrite that keeps the URL and the error status — Netlify's
43
+ // documented localized-404 pattern), never a shadowing force rule.
44
+ expect(toml.match(/status = 404/g)).toHaveLength(2);
45
+ expect(toml).not.toContain('force');
46
+ // The default locale is the root catch-all 404.html — no rule.
47
+ expect(toml).not.toContain('from = "/en/*"');
48
+ });
49
+
50
+ test('single-locale config → "" (no redirects block at all)', () => {
51
+ expect(netlifyLocale404Redirects(SINGLE)).toBe('');
52
+ });
53
+
54
+ test('a malformed locale code is skipped (cannot corrupt the TOML)', () => {
55
+ const sketchy: I18nConfig = {
56
+ defaultLocale: 'en',
57
+ locales: [
58
+ MULTI.locales[0]!,
59
+ { code: 'pl', name: 'PL', nativeName: 'PL', langTag: 'pl-PL' },
60
+ { code: 'we"ird/../x', name: 'X', nativeName: 'X', langTag: 'x' },
61
+ ],
62
+ };
63
+ const toml = netlifyLocale404Redirects(sketchy);
64
+ expect(toml).toContain('from = "/pl/*"');
65
+ expect(toml).not.toContain('we"ird');
66
+ });
67
+
68
+ test('only-malformed non-default codes → "" (no orphan header)', () => {
69
+ const allBad: I18nConfig = {
70
+ defaultLocale: 'en',
71
+ locales: [MULTI.locales[0]!, { code: 'bad code', name: 'X', nativeName: 'X', langTag: 'x' }],
72
+ };
73
+ expect(netlifyLocale404Redirects(allBad)).toBe('');
74
+ });
75
+ });
76
+
77
+ describe('syncNetlifyLocale404Block', () => {
78
+ test('inserts the managed block (multi-locale + 404 page)', () => {
79
+ const out = syncNetlifyLocale404Block(BASE_TOML, EN_PL, true);
80
+ // The original content is preserved byte-identically as a prefix.
81
+ expect(out.startsWith(BASE_TOML)).toBe(true);
82
+ expect(out).toContain(NETLIFY_LOCALE_404_BEGIN);
83
+ expect(out).toContain(NETLIFY_LOCALE_404_END);
84
+ expect(out).toContain('from = "/pl/*"');
85
+ expect(out).toContain('to = "/pl/404/index.html"');
86
+ expect(out).toContain('status = 404');
87
+ expect(out).not.toContain('from = "/en/*"');
88
+ // The block is line-shaped: BEGIN at line start, END line last, trailing newline.
89
+ expect(out).toContain(`\n${NETLIFY_LOCALE_404_BEGIN}\n`);
90
+ expect(out.endsWith(`${NETLIFY_LOCALE_404_END}\n`)).toBe(true);
91
+ });
92
+
93
+ test('adds a trailing newline before appending when the content lacks one', () => {
94
+ const noTrailing = '[build]\n publish = "dist"';
95
+ const out = syncNetlifyLocale404Block(noTrailing, EN_PL, true);
96
+ expect(out.startsWith(`${noTrailing}\n${NETLIFY_LOCALE_404_BEGIN}\n`)).toBe(true);
97
+ });
98
+
99
+ test('empty content + applicable rules → just the block', () => {
100
+ const out = syncNetlifyLocale404Block('', EN_PL, true);
101
+ expect(out.startsWith(NETLIFY_LOCALE_404_BEGIN)).toBe(true);
102
+ expect(out.endsWith(`${NETLIFY_LOCALE_404_END}\n`)).toBe(true);
103
+ });
104
+
105
+ test('no markers + nothing to add → returned unchanged (single-locale)', () => {
106
+ expect(syncNetlifyLocale404Block(BASE_TOML, SINGLE, true)).toBe(BASE_TOML);
107
+ });
108
+
109
+ test('no markers + nothing to add → returned unchanged (no 404 page)', () => {
110
+ expect(syncNetlifyLocale404Block(BASE_TOML, EN_PL, false)).toBe(BASE_TOML);
111
+ });
112
+
113
+ test('idempotent: re-syncing the same state is a no-op', () => {
114
+ const once = syncNetlifyLocale404Block(BASE_TOML, EN_PL, true);
115
+ const twice = syncNetlifyLocale404Block(once, EN_PL, true);
116
+ expect(twice).toBe(once);
117
+ });
118
+
119
+ test('replaces an existing block in place when the locale set changes', () => {
120
+ const withPl = syncNetlifyLocale404Block(BASE_TOML, EN_PL, true);
121
+ const withPlDe = syncNetlifyLocale404Block(withPl, MULTI, true);
122
+ expect(withPlDe).toContain('from = "/pl/*"');
123
+ expect(withPlDe).toContain('from = "/de/*"');
124
+ // Exactly one block (replaced, not appended).
125
+ expect(withPlDe.split(NETLIFY_LOCALE_404_BEGIN)).toHaveLength(2);
126
+ // Content outside the markers is byte-identical.
127
+ expect(withPlDe.startsWith(BASE_TOML)).toBe(true);
128
+ // Replacing pl+de straight away gives the same result (position-stable).
129
+ expect(withPlDe).toBe(syncNetlifyLocale404Block(BASE_TOML, MULTI, true));
130
+ });
131
+
132
+ test('removes the whole block when the project goes single-locale', () => {
133
+ const withBlock = syncNetlifyLocale404Block(BASE_TOML, EN_PL, true);
134
+ const removed = syncNetlifyLocale404Block(withBlock, SINGLE, true);
135
+ expect(removed).toBe(BASE_TOML); // perfect round-trip
136
+ expect(removed).not.toContain('[[redirects]]');
137
+ });
138
+
139
+ test('removes the whole block when the 404 page disappears', () => {
140
+ const withBlock = syncNetlifyLocale404Block(BASE_TOML, EN_PL, true);
141
+ const removed = syncNetlifyLocale404Block(withBlock, EN_PL, false);
142
+ expect(removed).toBe(BASE_TOML);
143
+ });
144
+
145
+ test('user content around the block is preserved byte-identically', () => {
146
+ const userAbove = '# my deploy config\n[build]\n command = "my build"\n';
147
+ const userBelow =
148
+ '\n# user redirect kept verbatim\n[[redirects]]\n from = "/old"\n to = "/new"\n status = 301\n';
149
+ const withBlock = syncNetlifyLocale404Block(userAbove, EN_PL, true) + userBelow;
150
+
151
+ // Replace: outside-the-markers bytes survive.
152
+ const replaced = syncNetlifyLocale404Block(withBlock, MULTI, true);
153
+ expect(replaced.startsWith(userAbove)).toBe(true);
154
+ expect(replaced.endsWith(userBelow)).toBe(true);
155
+ expect(replaced).toContain('from = "/de/*"');
156
+
157
+ // Remove: only the managed lines disappear.
158
+ const removed = syncNetlifyLocale404Block(withBlock, SINGLE, true);
159
+ expect(removed).toBe(userAbove + userBelow);
160
+ });
161
+
162
+ test('a corrupted block (BEGIN without END) is left untouched', () => {
163
+ const corrupt = `${BASE_TOML}${NETLIFY_LOCALE_404_BEGIN}\n# half a block, END deleted by hand\n`;
164
+ expect(syncNetlifyLocale404Block(corrupt, MULTI, true)).toBe(corrupt);
165
+ expect(syncNetlifyLocale404Block(corrupt, SINGLE, true)).toBe(corrupt);
166
+ });
167
+
168
+ test('weird locale codes cannot reach the managed block', () => {
169
+ const sketchy: I18nConfig = {
170
+ defaultLocale: 'en',
171
+ locales: [MULTI.locales[0]!, { code: '../escape', name: 'X', nativeName: 'X', langTag: 'x' }],
172
+ };
173
+ // Only-malformed codes → no rules → nothing inserted.
174
+ expect(syncNetlifyLocale404Block(BASE_TOML, sketchy, true)).toBe(BASE_TOML);
175
+ // And an existing block is removed rather than filled with garbage.
176
+ const withBlock = syncNetlifyLocale404Block(BASE_TOML, EN_PL, true);
177
+ expect(syncNetlifyLocale404Block(withBlock, sketchy, true)).toBe(BASE_TOML);
178
+ });
179
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Locale-scoped Netlify 404 rules + the managed netlify.toml block that carries them.
3
+ *
4
+ * Why: a multi-locale static build emits a localized 404 per non-default locale
5
+ * (`dist/pl/404/index.html`), but a static host serves exactly ONE catch-all 404
6
+ * page unless told otherwise — so `/pl/<missing>` would get the default-locale
7
+ * `404.html`. These rules scope the 404 per locale prefix.
8
+ *
9
+ * Shared by every netlify.toml producer — meno-astro's `convertProject` scaffold,
10
+ * the JSON→Astro export (`build-astro.ts`), the CLI new-project scaffold — and by
11
+ * the studio save-config route, which re-syncs the managed block when the locale
12
+ * set changes AFTER the file was first written (so a project that adds a locale
13
+ * later still gets its rules). It lives in meno-core because the dependency
14
+ * direction is astro→core: meno-core must never import from meno-astro.
15
+ */
16
+
17
+ import type { I18nConfig } from './types';
18
+
19
+ /**
20
+ * First line of the managed block. Everything between the markers (inclusive) is
21
+ * generated and may be rewritten wholesale by `syncNetlifyLocale404Block`; user
22
+ * content outside the markers is never touched.
23
+ */
24
+ export const NETLIFY_LOCALE_404_BEGIN = '# BEGIN meno locale-404 redirects (generated — do not edit between markers)';
25
+
26
+ /** Last line of the managed block. */
27
+ export const NETLIFY_LOCALE_404_END = '# END meno locale-404 redirects';
28
+
29
+ /**
30
+ * Locale-scoped 404 rules for a netlify.toml — one `[[redirects]]` block per
31
+ * non-default locale (`''` for single-locale configs):
32
+ *
33
+ * [[redirects]]
34
+ * from = "/pl/*"
35
+ * to = "/pl/404/index.html"
36
+ * status = 404
37
+ *
38
+ * Netlify semantics (verified against Netlify's "redirect options" docs, the
39
+ * documented "localized custom 404" pattern): a redirect with `status = 404` and no
40
+ * `force` is a NON-SHADOWING rewrite — a request that matches an existing file is
41
+ * served normally (real `/pl/...` pages always win), and only an unmatched `/pl/*`
42
+ * URL serves the `to` file's CONTENT with an HTTP 404 status (the URL stays put; no
43
+ * 3xx). Everything the rules don't cover falls through to Netlify's root convention:
44
+ * a top-level `404.html` (which Astro builds from `src/pages/404.astro`) is served
45
+ * automatically for any other unmatched URL — no explicit catch-all rule needed.
46
+ *
47
+ * Locale codes are sanity-checked (`/^[A-Za-z0-9_-]+$/`): a malformed code would
48
+ * corrupt the TOML / redirect syntax, and such a locale cannot produce a route anyway.
49
+ *
50
+ * Pure (config in, TOML fragment out) so it is directly unit-testable; callers own
51
+ * the existence gates (404 page present, netlify.toml ownership). Most callers
52
+ * should go through `syncNetlifyLocale404Block`, which wraps the rules in the
53
+ * managed marker block so later config saves can update them in place.
54
+ */
55
+ export function netlifyLocale404Redirects(config: I18nConfig): string {
56
+ if (!config.locales || config.locales.length <= 1) return '';
57
+ const codes = config.locales
58
+ .map((l) => l.code)
59
+ .filter((code) => code !== config.defaultLocale && /^[A-Za-z0-9_-]+$/.test(code));
60
+ if (codes.length === 0) return '';
61
+ const header =
62
+ "\n# Locale-scoped 404s — an unmatched /<locale>/* URL serves that locale's own\n" +
63
+ '# prerendered 404 page WITH a 404 status (non-shadowing: real pages still win).\n' +
64
+ '# Unmatched URLs outside these prefixes fall through to the default /404.html.\n';
65
+ const rules = codes
66
+ .map((code) => `[[redirects]]\n from = "/${code}/*"\n to = "/${code}/404/index.html"\n status = 404\n`)
67
+ .join('\n');
68
+ return header + rules;
69
+ }
70
+
71
+ /**
72
+ * Insert / refresh / remove the managed locale-404 block in netlify.toml content.
73
+ * Pure (string in, string out) — callers own reading/writing the file.
74
+ *
75
+ * - Rules applicable (multi-locale config AND `has404`) + no block yet → the block
76
+ * is APPENDED (a trailing newline is added to the existing content first if it
77
+ * lacks one).
78
+ * - Rules applicable + block present → the block's contents are REPLACED in place
79
+ * (idempotent: re-syncing identical state is a no-op).
80
+ * - Rules no longer applicable (single-locale, or no 404 page) + block present →
81
+ * the whole block is REMOVED; everything outside the markers stays byte-identical.
82
+ * - Nothing applicable + no block → the content is returned unchanged.
83
+ *
84
+ * A corrupted block (BEGIN marker without its END) is left untouched — rewriting up
85
+ * to an unknown boundary could eat user content, and a hand-damaged block is the
86
+ * user taking ownership.
87
+ */
88
+ export function syncNetlifyLocale404Block(content: string, config: I18nConfig, has404: boolean): string {
89
+ const rules = has404 ? netlifyLocale404Redirects(config) : '';
90
+ // `rules` starts with '\n' (terminating the BEGIN line) and ends with '\n'
91
+ // (putting END at line start), so the block is exactly BEGIN…rules…END lines.
92
+ const block = rules ? `${NETLIFY_LOCALE_404_BEGIN}${rules}${NETLIFY_LOCALE_404_END}\n` : '';
93
+
94
+ const beginIdx = content.indexOf(NETLIFY_LOCALE_404_BEGIN);
95
+ if (beginIdx === -1) {
96
+ if (!block) return content;
97
+ const base = content === '' || content.endsWith('\n') ? content : `${content}\n`;
98
+ return base + block;
99
+ }
100
+
101
+ const endMarkerIdx = content.indexOf(NETLIFY_LOCALE_404_END, beginIdx);
102
+ if (endMarkerIdx === -1) return content; // corrupted block — refuse to guess its extent
103
+
104
+ let endIdx = endMarkerIdx + NETLIFY_LOCALE_404_END.length;
105
+ if (content[endIdx] === '\n') endIdx += 1; // the END line's own newline belongs to the block
106
+
107
+ const before = content.slice(0, beginIdx);
108
+ const after = content.slice(endIdx);
109
+ return before + block + after;
110
+ }
@@ -24,7 +24,7 @@ import {
24
24
  isEmptyContainer,
25
25
  } from './nodeUtils';
26
26
  import { NODE_TYPE } from './constants';
27
- import type { ComponentNode, ComponentInstanceNode, HtmlNode, SlotMarker, EmbedNode, LinkNode, LocaleListNode } from './types';
27
+ import type { ComponentInstanceNode, HtmlNode, SlotMarker, EmbedNode, LinkNode, LocaleListNode } from './types';
28
28
  import { registerBuiltInNodeTypes } from './registry';
29
29
 
30
30
  describe('nodeUtils', () => {
@@ -126,11 +126,11 @@ describe('nodeUtils', () => {
126
126
  const node: ComponentInstanceNode = {
127
127
  type: NODE_TYPE.COMPONENT,
128
128
  component: 'Button',
129
- props: { text: 'Click me' }
129
+ props: { text: 'Click me' },
130
130
  };
131
131
  expect(extractNodeProperties(node)).toEqual({
132
132
  componentName: 'Button',
133
- props: { text: 'Click me' }
133
+ props: { text: 'Click me' },
134
134
  });
135
135
  });
136
136
 
@@ -138,7 +138,7 @@ describe('nodeUtils', () => {
138
138
  const node: HtmlNode = { type: NODE_TYPE.NODE, tag: 'div' };
139
139
  expect(extractNodeProperties(node)).toEqual({
140
140
  tag: 'div',
141
- props: {}
141
+ props: {},
142
142
  });
143
143
  });
144
144
 
@@ -227,6 +227,10 @@ describe('nodeUtils', () => {
227
227
  expect(isValidNodeType(NODE_TYPE.EMBED)).toBe(true);
228
228
  expect(isValidNodeType(NODE_TYPE.LINK)).toBe(true);
229
229
  expect(isValidNodeType(NODE_TYPE.LOCALE_LIST)).toBe(true);
230
+ expect(isValidNodeType(NODE_TYPE.LIST)).toBe(true);
231
+ expect(isValidNodeType(NODE_TYPE.ISLAND)).toBe(true);
232
+ expect(isValidNodeType(NODE_TYPE.CUSTOM)).toBe(true);
233
+ expect(isValidNodeType(NODE_TYPE.MARKDOWN)).toBe(true);
230
234
  });
231
235
 
232
236
  test('returns false for invalid node types', () => {
@@ -253,6 +257,14 @@ describe('nodeUtils', () => {
253
257
  expect(canHaveChildren(null)).toBe(false);
254
258
  expect(canHaveChildren(undefined)).toBe(false);
255
259
  });
260
+
261
+ test('returns false for a verbatim {_code} marker (the emitter ignores children → silent loss)', () => {
262
+ expect(
263
+ canHaveChildren({ _code: true, expr: 'items.map((i) => i)' } as unknown as Parameters<
264
+ typeof canHaveChildren
265
+ >[0]),
266
+ ).toBe(false);
267
+ });
256
268
  });
257
269
 
258
270
  describe('canHaveStyle', () => {
@@ -328,7 +340,7 @@ describe('nodeUtils', () => {
328
340
  const mapping = {
329
341
  _mapping: true,
330
342
  prop: 'visible',
331
- values: { 'true': true, 'false': false }
343
+ values: { true: true, false: false },
332
344
  };
333
345
  expect(isBooleanMapping(mapping)).toBe(true);
334
346
  });
@@ -368,8 +380,8 @@ describe('nodeUtils', () => {
368
380
  if: {
369
381
  _mapping: true,
370
382
  prop: 'showBanner',
371
- values: { 'true': true, 'false': false }
372
- }
383
+ values: { true: true, false: false },
384
+ },
373
385
  } as any;
374
386
 
375
387
  expect(evaluateNodeIf(node, { showBanner: 'true' })).toBe(true);
@@ -383,8 +395,8 @@ describe('nodeUtils', () => {
383
395
  if: {
384
396
  _mapping: true,
385
397
  prop: 'status',
386
- values: { 'active': true, 'inactive': false }
387
- }
398
+ values: { active: true, inactive: false },
399
+ },
388
400
  } as any;
389
401
 
390
402
  // 'unknown' is not in the values map
@@ -398,8 +410,8 @@ describe('nodeUtils', () => {
398
410
  if: {
399
411
  _mapping: true,
400
412
  prop: 'visible',
401
- values: { 'true': true, 'false': false }
402
- }
413
+ values: { true: true, false: false },
414
+ },
403
415
  } as any;
404
416
 
405
417
  expect(evaluateNodeIf(node)).toBe(true);
@@ -409,7 +421,7 @@ describe('nodeUtils', () => {
409
421
  const node = {
410
422
  type: NODE_TYPE.NODE,
411
423
  tag: 'div',
412
- if: '{{showBanner}}'
424
+ if: '{{showBanner}}',
413
425
  } as any;
414
426
 
415
427
  // String templates can't be resolved without context, default to true
@@ -420,7 +432,7 @@ describe('nodeUtils', () => {
420
432
  const node: ComponentInstanceNode = {
421
433
  type: NODE_TYPE.COMPONENT,
422
434
  component: 'Banner',
423
- if: false
435
+ if: false,
424
436
  } as any;
425
437
  expect(evaluateNodeIf(node)).toBe(false);
426
438
  });
@@ -429,7 +441,7 @@ describe('nodeUtils', () => {
429
441
  const node: EmbedNode = {
430
442
  type: NODE_TYPE.EMBED,
431
443
  html: '<svg></svg>',
432
- if: false
444
+ if: false,
433
445
  } as any;
434
446
  expect(evaluateNodeIf(node)).toBe(false);
435
447
  });
@@ -438,7 +450,7 @@ describe('nodeUtils', () => {
438
450
  const node: LinkNode = {
439
451
  type: NODE_TYPE.LINK,
440
452
  href: '/page',
441
- if: true
453
+ if: true,
442
454
  } as any;
443
455
  expect(evaluateNodeIf(node)).toBe(true);
444
456
  });
@@ -515,7 +527,7 @@ describe('nodeUtils', () => {
515
527
  const node: HtmlNode = {
516
528
  type: NODE_TYPE.NODE,
517
529
  tag: 'div',
518
- children: [{ type: NODE_TYPE.NODE, tag: 'span' }]
530
+ children: [{ type: NODE_TYPE.NODE, tag: 'span' }],
519
531
  };
520
532
  expect(isEmptyContainer(node)).toBe(false);
521
533
  });