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
@@ -32,14 +32,14 @@ import { isSlotMarker } from '../nodeUtils';
32
32
  function calculateChildrenExpansionOffset(
33
33
  componentPath: Path,
34
34
  componentStructure: ComponentNode | undefined,
35
- instanceChildren: ComponentNode['children'] | undefined
35
+ instanceChildren: ComponentNode['children'] | undefined,
36
36
  ): number {
37
37
  if (!componentStructure || !Array.isArray(instanceChildren)) {
38
38
  return 0;
39
39
  }
40
40
 
41
41
  // Count actual children (accounting for marker expansion)
42
- const childrenCount = Array.isArray(instanceChildren) ? instanceChildren.length : (instanceChildren ? 1 : 0);
42
+ const childrenCount = Array.isArray(instanceChildren) ? instanceChildren.length : instanceChildren ? 1 : 0;
43
43
 
44
44
  let offset = 0;
45
45
  let currentNode = componentStructure;
@@ -47,6 +47,7 @@ function calculateChildrenExpansionOffset(
47
47
  // Walk the component structure following the path
48
48
  for (let i = 1; i < componentPath.length; i++) {
49
49
  const index = componentPath[i];
50
+ if (index === undefined) break;
50
51
 
51
52
  // Get children of current node
52
53
  if (!currentNode || !Array.isArray((currentNode as any).children)) {
@@ -72,7 +73,7 @@ function calculateChildrenExpansionOffset(
72
73
 
73
74
  // Move to next node in the path
74
75
  if (index < children.length) {
75
- currentNode = (children[index] as any) as ComponentNode;
76
+ currentNode = children[index] as any as ComponentNode;
76
77
  } else {
77
78
  break;
78
79
  }
@@ -128,12 +129,12 @@ export function buildParentPaths(path: Path): Path[] {
128
129
  if (isRootPath(path)) {
129
130
  return [];
130
131
  }
131
-
132
+
132
133
  const parents: Path[] = [];
133
134
  for (let i = 1; i < path.length; i++) {
134
135
  parents.push(path.slice(0, i));
135
136
  }
136
-
137
+
137
138
  return parents;
138
139
  }
139
140
 
@@ -186,12 +187,12 @@ export function convertPagePathToComponentPath(
186
187
  pagePath: Path,
187
188
  componentInstancePath: Path,
188
189
  componentStructure?: ComponentNode,
189
- instanceChildren?: ComponentNode['children']
190
+ instanceChildren?: ComponentNode['children'],
190
191
  ): Path | null {
191
192
  // Check if pagePath starts with componentInstancePath
192
193
  // Path must be within or equal to component instance
193
- const isWithinComponent = isAncestorPath(componentInstancePath, pagePath) ||
194
- pathsEqual(pagePath, componentInstancePath);
194
+ const isWithinComponent =
195
+ isAncestorPath(componentInstancePath, pagePath) || pathsEqual(pagePath, componentInstancePath);
195
196
 
196
197
  if (!isWithinComponent) {
197
198
  return null; // Path is not within the component instance
@@ -212,7 +213,7 @@ export function convertPagePathToComponentPath(
212
213
  isSlotMarker(componentStructure.children[0])
213
214
  ) {
214
215
  // Children marker is at index 0, so indices 0 to (childrenCount - 1) map to children
215
- const pageFirstIndex = suffix[0];
216
+ const pageFirstIndex = suffix[0] ?? 0;
216
217
 
217
218
  // If page index is within the children range, it's a child (no component path)
218
219
  if (pageFirstIndex < childrenCount) {
@@ -224,7 +225,7 @@ export function convertPagePathToComponentPath(
224
225
  // Page index is after slot expansion
225
226
  // Subtract the offset (childrenCount - 1) to get component index
226
227
  const offset = childrenCount - 1;
227
- adjustedSuffix = [suffix[0] - offset, ...suffix.slice(1)];
228
+ adjustedSuffix = [pageFirstIndex - offset, ...suffix.slice(1)];
228
229
  }
229
230
  }
230
231
  }
@@ -264,7 +265,7 @@ export function convertComponentPathToPagePath(
264
265
  componentPath: Path,
265
266
  componentInstancePath: Path,
266
267
  componentStructure?: ComponentNode,
267
- instanceChildren?: ComponentNode['children']
268
+ instanceChildren?: ComponentNode['children'],
268
269
  ): Path {
269
270
  // Component paths start with [0] (component root)
270
271
  if (componentPath.length === 1 && componentPath[0] === 0) {
@@ -281,10 +282,9 @@ export function convertComponentPathToPagePath(
281
282
  // Apply offset to first level index if slot expansion occurred
282
283
  let adjustedSuffix = suffix;
283
284
  if (offset > 0 && suffix.length > 0) {
284
- adjustedSuffix = [suffix[0] + offset, ...suffix.slice(1)];
285
+ adjustedSuffix = [(suffix[0] ?? 0) + offset, ...suffix.slice(1)];
285
286
  }
286
287
 
287
288
  // Append adjusted suffix to component instance path
288
289
  return [...componentInstancePath, ...adjustedSuffix];
289
290
  }
290
-
@@ -1,10 +1,5 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
- import {
3
- validatePath,
4
- isRootPath,
5
- InvalidPathError,
6
- PathConversionError
7
- } from './PathValidator';
2
+ import { validatePath, isRootPath, InvalidPathError, PathConversionError } from './PathValidator';
8
3
 
9
4
  describe('PathValidator', () => {
10
5
  describe('validatePath', () => {
@@ -175,15 +170,7 @@ describe('PathValidator', () => {
175
170
  });
176
171
 
177
172
  test('stores any input type', () => {
178
- const inputs = [
179
- 'string',
180
- 123,
181
- null,
182
- undefined,
183
- {},
184
- [],
185
- true
186
- ];
173
+ const inputs = ['string', 123, null, undefined, {}, [], true];
187
174
  for (const input of inputs) {
188
175
  const error = new PathConversionError('Test', input);
189
176
  expect(error.input).toBe(input);
@@ -9,14 +9,21 @@ import type { Path } from './Path';
9
9
  * Custom error classes for path operations
10
10
  */
11
11
  export class InvalidPathError extends Error {
12
- constructor(message: string, public readonly path: unknown, public readonly index?: number) {
12
+ constructor(
13
+ message: string,
14
+ public readonly path: unknown,
15
+ public readonly index?: number,
16
+ ) {
13
17
  super(message);
14
18
  this.name = 'InvalidPathError';
15
19
  }
16
20
  }
17
21
 
18
22
  export class PathConversionError extends Error {
19
- constructor(message: string, public readonly input: unknown) {
23
+ constructor(
24
+ message: string,
25
+ public readonly input: unknown,
26
+ ) {
20
27
  super(message);
21
28
  this.name = 'PathConversionError';
22
29
  }
@@ -30,15 +37,11 @@ export function validatePath(path: Path): void {
30
37
  if (!Array.isArray(path)) {
31
38
  throw new InvalidPathError('Path must be an array', path);
32
39
  }
33
-
40
+
34
41
  for (let i = 0; i < path.length; i++) {
35
42
  const value = path[i];
36
- if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
37
- throw new InvalidPathError(
38
- `Invalid path value at index ${i}: expected number, got ${typeof value}`,
39
- path,
40
- i
41
- );
43
+ if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value)) {
44
+ throw new InvalidPathError(`Invalid path value at index ${i}: expected number, got ${typeof value}`, path, i);
42
45
  }
43
46
  }
44
47
  }
@@ -50,4 +53,3 @@ export function validatePath(path: Path): void {
50
53
  export function isRootPath(path: Path): boolean {
51
54
  return path.length === 0 || (path.length === 1 && path[0] === 0);
52
55
  }
53
-
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Path Utilities
3
3
  * Unified path handling system
4
- *
4
+ *
5
5
  * This module provides a single source of truth for path operations.
6
6
  * All path-related functionality is consolidated here.
7
7
  */
@@ -45,4 +45,3 @@ export {
45
45
  convertPagePathToComponentPath,
46
46
  convertComponentPathToPagePath,
47
47
  } from './PathUtils';
48
-
@@ -0,0 +1,168 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { resolvePermissions, canEdit, matchesPermissionPattern, matchesAnyPermissionPattern } from './permissions';
3
+ import type { PermissionsPolicy, ResourceRef } from './types/permissions';
4
+
5
+ const page = (id: string): ResourceRef => ({ kind: 'page', id });
6
+ const component = (id: string): ResourceRef => ({ kind: 'component', id });
7
+ const cms = (id: string): ResourceRef => ({ kind: 'cms', id });
8
+ const config = (id: string): ResourceRef => ({ kind: 'config', id });
9
+
10
+ describe('matchesPermissionPattern', () => {
11
+ test('exact match', () => {
12
+ expect(matchesPermissionPattern('about', 'about')).toBe(true);
13
+ expect(matchesPermissionPattern('about', 'contact')).toBe(false);
14
+ });
15
+
16
+ test('bare "*" matches anything', () => {
17
+ expect(matchesPermissionPattern('*', 'anything')).toBe(true);
18
+ expect(matchesPermissionPattern('*', '')).toBe(true);
19
+ });
20
+
21
+ test('"blog/*" matches under the prefix, including deeper paths (wildcard crosses "/")', () => {
22
+ expect(matchesPermissionPattern('blog/*', 'blog/post')).toBe(true);
23
+ expect(matchesPermissionPattern('blog/*', 'blog/2026/post')).toBe(true);
24
+ expect(matchesPermissionPattern('blog/*', 'marketing/post')).toBe(false);
25
+ });
26
+
27
+ test('"*Hero" suffix match', () => {
28
+ expect(matchesPermissionPattern('*Hero', 'HomeHero')).toBe(true);
29
+ expect(matchesPermissionPattern('*Hero', 'HeroBanner')).toBe(false);
30
+ });
31
+
32
+ test('regex special chars in the pattern are treated literally', () => {
33
+ expect(matchesPermissionPattern('a.b', 'a.b')).toBe(true);
34
+ expect(matchesPermissionPattern('a.b', 'axb')).toBe(false);
35
+ });
36
+
37
+ test('matchesAnyPermissionPattern handles undefined / empty lists', () => {
38
+ expect(matchesAnyPermissionPattern(undefined, 'x')).toBe(false);
39
+ expect(matchesAnyPermissionPattern([], 'x')).toBe(false);
40
+ expect(matchesAnyPermissionPattern(['a', 'b*'], 'bar')).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe('resolvePermissions — no policy (unmanaged project)', () => {
45
+ test('everyone is admin and can edit everything', () => {
46
+ const r = resolvePermissions(null, { login: 'alice' });
47
+ expect(r.isAdmin).toBe(true);
48
+ expect(r.role).toBe('admin');
49
+ expect(canEdit(r, page('anything'))).toBe(true);
50
+ expect(canEdit(r, config('colors'))).toBe(true);
51
+ });
52
+
53
+ test('canManagePolicy: repo admins and local/unauthenticated runs only', () => {
54
+ expect(resolvePermissions(null, { login: 'bob' }).canManagePolicy).toBe(false);
55
+ expect(resolvePermissions(null, { login: 'bob', isRepoAdmin: true }).canManagePolicy).toBe(true);
56
+ expect(resolvePermissions(null, { login: null }).canManagePolicy).toBe(true);
57
+ });
58
+ });
59
+
60
+ describe('resolvePermissions — present policy', () => {
61
+ const policy: PermissionsPolicy = {
62
+ version: 1,
63
+ defaultRole: 'viewer',
64
+ members: {
65
+ alice: { allow: { cms: ['posts'], pages: ['blog/*'] } },
66
+ Bob: { allow: { pages: ['marketing/*'], components: ['*'] } },
67
+ carol: { role: 'admin' },
68
+ dave: { role: 'viewer' },
69
+ },
70
+ teams: {
71
+ content: { allow: { cms: ['*'] } },
72
+ leads: { role: 'admin' },
73
+ },
74
+ };
75
+
76
+ test('unlisted authenticated user gets defaultRole (viewer → edits nothing)', () => {
77
+ const r = resolvePermissions(policy, { login: 'stranger' });
78
+ expect(r.role).toBe('viewer');
79
+ expect(r.isAdmin).toBe(false);
80
+ expect(canEdit(r, page('blog/post'))).toBe(false);
81
+ });
82
+
83
+ test('editor edits only allow-listed resources', () => {
84
+ const r = resolvePermissions(policy, { login: 'alice' });
85
+ expect(r.role).toBe('editor');
86
+ expect(canEdit(r, page('blog/post'))).toBe(true);
87
+ expect(canEdit(r, page('marketing/home'))).toBe(false);
88
+ expect(canEdit(r, cms('posts'))).toBe(true);
89
+ expect(canEdit(r, cms('products'))).toBe(false);
90
+ expect(canEdit(r, component('Card'))).toBe(false);
91
+ expect(canEdit(r, config('colors'))).toBe(false);
92
+ });
93
+
94
+ test('login lookup is case-insensitive', () => {
95
+ const r = resolvePermissions(policy, { login: 'BOB' });
96
+ expect(r.role).toBe('editor');
97
+ expect(canEdit(r, page('marketing/home'))).toBe(true);
98
+ expect(canEdit(r, component('AnythingGoes'))).toBe(true);
99
+ });
100
+
101
+ test('explicit admin role edits everything regardless of allow-lists', () => {
102
+ const r = resolvePermissions(policy, { login: 'carol' });
103
+ expect(r.isAdmin).toBe(true);
104
+ expect(r.canManagePolicy).toBe(true);
105
+ expect(canEdit(r, page('anything'))).toBe(true);
106
+ expect(canEdit(r, config('variables'))).toBe(true);
107
+ });
108
+
109
+ test('explicit viewer role edits nothing', () => {
110
+ const r = resolvePermissions(policy, { login: 'dave' });
111
+ expect(r.role).toBe('viewer');
112
+ expect(canEdit(r, page('anything'))).toBe(false);
113
+ expect(r.canManagePolicy).toBe(false);
114
+ });
115
+
116
+ test('repo admin is always admin, even when the policy lists them lower', () => {
117
+ const r = resolvePermissions(policy, { login: 'dave', isRepoAdmin: true });
118
+ expect(r.isAdmin).toBe(true);
119
+ expect(r.role).toBe('admin');
120
+ expect(canEdit(r, page('anything'))).toBe(true);
121
+ expect(r.canManagePolicy).toBe(true);
122
+ });
123
+
124
+ test('team grants apply and unite with member grants', () => {
125
+ const r = resolvePermissions(policy, { login: 'alice', teams: ['content'] });
126
+ expect(r.role).toBe('editor');
127
+ // member grant (blog) + team grant (all cms)
128
+ expect(canEdit(r, page('blog/post'))).toBe(true);
129
+ expect(canEdit(r, cms('products'))).toBe(true);
130
+ });
131
+
132
+ test('highest role across member + teams wins (team admin promotes)', () => {
133
+ const r = resolvePermissions(policy, { login: 'alice', teams: ['leads'] });
134
+ expect(r.isAdmin).toBe(true);
135
+ expect(canEdit(r, config('project'))).toBe(true);
136
+ });
137
+
138
+ test('team lookup is case-insensitive', () => {
139
+ const r = resolvePermissions(policy, { login: 'nobody', teams: ['Content'] });
140
+ expect(canEdit(r, cms('posts'))).toBe(true);
141
+ });
142
+
143
+ test('config area allow-lists support wildcards', () => {
144
+ const p: PermissionsPolicy = {
145
+ version: 1,
146
+ members: { ed: { allow: { config: ['colors', 'variables'] } }, all: { allow: { config: ['*'] } } },
147
+ };
148
+ const ed = resolvePermissions(p, { login: 'ed' });
149
+ expect(canEdit(ed, config('colors'))).toBe(true);
150
+ expect(canEdit(ed, config('i18n'))).toBe(false);
151
+ const all = resolvePermissions(p, { login: 'all' });
152
+ expect(canEdit(all, config('i18n'))).toBe(true);
153
+ });
154
+
155
+ test('defaultRole can widen the baseline (editor default)', () => {
156
+ const p: PermissionsPolicy = { version: 1, defaultRole: 'editor', members: { ed: { allow: { pages: ['x'] } } } };
157
+ // An unlisted user is an editor with NO allow-list → edits nothing.
158
+ const unlisted = resolvePermissions(p, { login: 'stranger' });
159
+ expect(unlisted.role).toBe('editor');
160
+ expect(canEdit(unlisted, page('x'))).toBe(false);
161
+ });
162
+
163
+ test('null login with a present policy falls to defaultRole', () => {
164
+ const r = resolvePermissions(policy, { login: null });
165
+ expect(r.role).toBe('viewer');
166
+ expect(canEdit(r, page('blog/post'))).toBe(false);
167
+ });
168
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Permission resolver
3
+ *
4
+ * Pure functions that turn a {@link PermissionsPolicy} + a
5
+ * {@link PermissionIdentity} into a queryable {@link ResolvedPermissions}, and
6
+ * answer "can this identity edit this resource?". No IO — the policy is loaded
7
+ * and the identity gathered by the caller. Shared by the studio client (UI
8
+ * gating) and any future server-side mirror.
9
+ *
10
+ * See `./types/permissions.ts` for the data model and the SOFT-enforcement
11
+ * caveat.
12
+ */
13
+
14
+ import type {
15
+ PermissionGrant,
16
+ PermissionIdentity,
17
+ PermissionRole,
18
+ PermissionRule,
19
+ PermissionsPolicy,
20
+ ResolvedPermissions,
21
+ ResourceKind,
22
+ ResourceRef,
23
+ } from './types/permissions';
24
+
25
+ /** Role capability ordering, low → high. */
26
+ const ROLE_RANK: Record<PermissionRole, number> = { viewer: 0, editor: 1, admin: 2 };
27
+
28
+ /** Maps a {@link ResourceKind} to its {@link PermissionRule} key. */
29
+ const RULE_KEY: Record<ResourceKind, keyof PermissionRule> = {
30
+ page: 'pages',
31
+ component: 'components',
32
+ cms: 'cms',
33
+ config: 'config',
34
+ };
35
+
36
+ /** The empty (all-kinds-present) allow rule used as a merge seed. */
37
+ function emptyAllow(): Required<PermissionRule> {
38
+ return { pages: [], components: [], cms: [], config: [] };
39
+ }
40
+
41
+ function higherRole(a: PermissionRole, b: PermissionRole): PermissionRole {
42
+ return ROLE_RANK[b] > ROLE_RANK[a] ? b : a;
43
+ }
44
+
45
+ /** A grant's role: explicit, else `editor` when it carries an allow-list, else `viewer`. */
46
+ function grantRole(grant: PermissionGrant): PermissionRole {
47
+ if (grant.role) return grant.role;
48
+ return grant.allow ? 'editor' : 'viewer';
49
+ }
50
+
51
+ function mergeAllow(grants: PermissionGrant[]): Required<PermissionRule> {
52
+ const merged = emptyAllow();
53
+ for (const grant of grants) {
54
+ const allow = grant.allow;
55
+ if (!allow) continue;
56
+ for (const kind of Object.keys(merged) as (keyof PermissionRule)[]) {
57
+ const patterns = allow[kind];
58
+ if (patterns) merged[kind].push(...patterns);
59
+ }
60
+ }
61
+ // Dedupe each list while preserving order.
62
+ for (const kind of Object.keys(merged) as (keyof PermissionRule)[]) {
63
+ merged[kind] = [...new Set(merged[kind])];
64
+ }
65
+ return merged;
66
+ }
67
+
68
+ const REGEX_SPECIALS = /[.+?^${}()|[\]\\]/g;
69
+
70
+ /**
71
+ * Glob match for a single permission pattern against a value. `*` matches any
72
+ * run of characters (including `/`); every other character is literal. An exact
73
+ * (wildcard-free) pattern must equal the value.
74
+ */
75
+ export function matchesPermissionPattern(pattern: string, value: string): boolean {
76
+ if (pattern === value) return true;
77
+ if (!pattern.includes('*')) return false;
78
+ const source = pattern
79
+ .split('*')
80
+ .map((segment) => segment.replace(REGEX_SPECIALS, '\\$&'))
81
+ .join('.*');
82
+ return new RegExp(`^${source}$`).test(value);
83
+ }
84
+
85
+ /** True if `value` matches any pattern in `patterns`. */
86
+ export function matchesAnyPermissionPattern(patterns: string[] | undefined, value: string): boolean {
87
+ return !!patterns && patterns.some((pattern) => matchesPermissionPattern(pattern, value));
88
+ }
89
+
90
+ /**
91
+ * Resolve a policy + identity into a capability set.
92
+ *
93
+ * - `policy == null` → an UNMANAGED project: everyone edits everything (status
94
+ * quo). `canManagePolicy` (who may create the first policy) is gated to repo
95
+ * admins, or anyone on a local/unauthenticated run.
96
+ * - With a policy present, an identity not listed in `members`/`teams` gets
97
+ * `defaultRole` (default `viewer`). `login: null` is treated the same way —
98
+ * callers that want to bypass gating for local runs should skip resolution
99
+ * rather than pass a null login.
100
+ * - A GitHub repo admin (`identity.isRepoAdmin`) is ALWAYS an admin here,
101
+ * regardless of the policy (bootstrap safety against lockout).
102
+ */
103
+ export function resolvePermissions(
104
+ policy: PermissionsPolicy | null | undefined,
105
+ identity: PermissionIdentity,
106
+ ): ResolvedPermissions {
107
+ const login = identity.login ?? null;
108
+ const isRepoAdmin = identity.isRepoAdmin === true;
109
+
110
+ if (!policy) {
111
+ return {
112
+ login,
113
+ role: 'admin',
114
+ isAdmin: true,
115
+ canManagePolicy: login == null || isRepoAdmin,
116
+ allow: emptyAllow(),
117
+ };
118
+ }
119
+
120
+ const grants: PermissionGrant[] = [];
121
+
122
+ const lookupLogin = login?.toLowerCase() ?? null;
123
+ if (lookupLogin && policy.members) {
124
+ for (const [key, grant] of Object.entries(policy.members)) {
125
+ if (key.toLowerCase() === lookupLogin) grants.push(grant);
126
+ }
127
+ }
128
+
129
+ if (policy.teams && identity.teams && identity.teams.length > 0) {
130
+ const teamSet = new Set(identity.teams.map((team) => team.toLowerCase()));
131
+ for (const [key, grant] of Object.entries(policy.teams)) {
132
+ if (teamSet.has(key.toLowerCase())) grants.push(grant);
133
+ }
134
+ }
135
+
136
+ const defaultRole: PermissionRole = policy.defaultRole ?? 'viewer';
137
+ const baseRole =
138
+ grants.length === 0
139
+ ? defaultRole
140
+ : grants.reduce<PermissionRole>((acc, g) => higherRole(acc, grantRole(g)), 'viewer');
141
+
142
+ const isAdmin = isRepoAdmin || baseRole === 'admin';
143
+
144
+ return {
145
+ login,
146
+ role: isAdmin ? 'admin' : baseRole,
147
+ isAdmin,
148
+ canManagePolicy: isAdmin,
149
+ allow: mergeAllow(grants),
150
+ };
151
+ }
152
+
153
+ /**
154
+ * May this identity edit this resource? Admins edit everything; viewers edit
155
+ * nothing; editors edit only resources whose id matches the merged allow-list
156
+ * for the resource's kind.
157
+ */
158
+ export function canEdit(resolved: ResolvedPermissions, ref: ResourceRef): boolean {
159
+ if (resolved.isAdmin) return true;
160
+ if (resolved.role === 'viewer') return false;
161
+ return matchesAnyPermissionPattern(resolved.allow[RULE_KEY[ref.kind]], ref.id);
162
+ }