@michaelthielemann/kestrel 3.0.1 → 4.0.1

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 (341) hide show
  1. package/README.md +35 -110
  2. package/kestrel.config.ts +1 -1
  3. package/layers/access/server/middleware/00.ip-allowlist.ts +3 -18
  4. package/layers/access/server/middleware/access-guard.ts +25 -23
  5. package/layers/access/server/types.d.ts +1 -2
  6. package/layers/admin/app/components/AdminAccount.vue +1 -1
  7. package/layers/admin/app/components/BlockFields.vue +1 -2
  8. package/layers/admin/app/components/BlockPreview.vue +17 -5
  9. package/layers/admin/app/components/BlocksBody.vue +1 -1
  10. package/layers/admin/app/components/CollectionEditor.vue +35 -16
  11. package/layers/admin/app/components/CollectionList.vue +34 -3
  12. package/layers/admin/app/components/CollectionListBulkBar.vue +17 -3
  13. package/layers/admin/app/components/CollectionListFilterPanel.vue +1 -1
  14. package/layers/admin/app/components/CollectionListPager.vue +1 -1
  15. package/layers/admin/app/components/CollectionListTable.vue +41 -2
  16. package/layers/admin/app/components/EditorStatus.vue +8 -7
  17. package/layers/admin/app/components/LocaleBar.vue +3 -1
  18. package/layers/admin/app/components/PageFields.vue +2 -4
  19. package/layers/admin/app/components/SeoFields.vue +2 -4
  20. package/layers/admin/app/components/SingletonEditor.vue +3 -2
  21. package/layers/admin/app/composables/useAuth.ts +3 -3
  22. package/layers/admin/app/composables/useBlockTree.ts +1 -1
  23. package/layers/admin/app/composables/useBlocks.ts +1 -1
  24. package/layers/admin/app/composables/useCollectionOps.ts +45 -8
  25. package/layers/admin/app/composables/useCollections.ts +1 -2
  26. package/layers/admin/app/composables/useEditForm.ts +35 -17
  27. package/layers/admin/app/composables/useLinkResolver.ts +1 -1
  28. package/layers/admin/app/composables/useListBatchActions.ts +20 -3
  29. package/layers/admin/app/composables/useListColumns.ts +1 -1
  30. package/layers/admin/app/composables/useListFilterDraft.ts +3 -2
  31. package/layers/admin/app/composables/useListRows.ts +7 -2
  32. package/layers/admin/app/composables/useListUrlState.ts +1 -1
  33. package/layers/admin/app/composables/usePublishStatus.ts +2 -2
  34. package/layers/admin/app/layouts/admin.vue +0 -1
  35. package/layers/admin/app/pages/admin/[collection]/[id].vue +5 -4
  36. package/layers/admin/app/pages/admin/index.vue +1 -1
  37. package/layers/admin/app/pages/admin/references.vue +2 -2
  38. package/layers/admin/app/utils/block-tree.ts +1 -1
  39. package/layers/admin/app/utils/collection-ops.ts +28 -3
  40. package/layers/admin/app/utils/edit-form.ts +34 -2
  41. package/layers/admin/app/utils/editor-expose.ts +2 -0
  42. package/layers/admin/app/utils/editor-form-context.ts +1 -2
  43. package/layers/admin/app/utils/field-keys.ts +1 -1
  44. package/layers/admin/app/utils/list-columns.ts +3 -2
  45. package/layers/admin/app/utils/list-query.ts +2 -2
  46. package/layers/admin/app/utils/populate-blocks.ts +2 -2
  47. package/layers/admin/app/utils/reauth.ts +5 -6
  48. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +7 -0
  49. package/layers/collections/server/plugins/01.register.ts +9 -12
  50. package/layers/collections/server/plugins/02.register-relation-populate.ts +5 -7
  51. package/layers/{fields → core}/app/utils/field-factories.ts +1 -1
  52. package/layers/core/modules/auto-discovery/extract-block.ts +3 -3
  53. package/layers/core/modules/auto-discovery/index.ts +69 -11
  54. package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
  55. package/layers/core/modules/auto-discovery/scan.ts +92 -0
  56. package/layers/core/modules/auto-discovery/virtual.d.ts +5 -0
  57. package/layers/core/modules/component-namespace/index.ts +24 -0
  58. package/layers/core/modules/kestrel/index.ts +6 -1
  59. package/layers/core/modules/plugin-order/index.ts +69 -0
  60. package/layers/core/modules/plugin-order/plugin-order.ts +273 -0
  61. package/layers/core/nuxt.config.ts +1 -0
  62. package/layers/core/server/api/[...path].ts +98 -0
  63. package/layers/core/server/plugins/00.config.ts +12 -0
  64. package/layers/core/server/plugins/00.migrate.ts +1 -0
  65. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +5 -0
  66. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +5 -0
  67. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +5 -0
  68. package/layers/core/server/plugins/02.schema-sync.ts +2 -7
  69. package/layers/core/server/plugins/04.outbox-worker.ts +31 -0
  70. package/layers/core/server/plugins/05.reindex-refs.ts +5 -0
  71. package/layers/core/server/routes/__kestrel/dashboard.get.ts +29 -0
  72. package/layers/core/server/tasks/db/migrate-module.ts +83 -0
  73. package/layers/core/server/tasks/db/migrate-revisions.ts +20 -0
  74. package/layers/core/server/tasks/db/migrate.ts +2 -6
  75. package/layers/core/server/utils/server-config.ts +66 -3
  76. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +7 -0
  77. package/layers/fields/server/plugins/01.register-field-populate.ts +2 -2
  78. package/layers/media/app/components/KestrelImg.vue +3 -3
  79. package/layers/media/app/components/MediaLibrary.vue +2 -2
  80. package/layers/media/app/components/field/Media.vue +1 -1
  81. package/layers/media/app/composables/useMediaResolver.ts +1 -1
  82. package/layers/media/app/composables/useMediaUpload.ts +1 -1
  83. package/layers/media/app/composables/useMediaVariant.ts +2 -2
  84. package/layers/media/app/utils/ai-disclosure.ts +1 -1
  85. package/layers/media/app/utils/library.ts +1 -1
  86. package/layers/media/app/utils/ops.ts +1 -3
  87. package/layers/media/app/utils/picture-model.ts +2 -2
  88. package/layers/media/app/utils/variant-name.ts +1 -1
  89. package/layers/media/server/middleware/ondemand-variants.ts +6 -5
  90. package/layers/media/server/plugins/01.register-media-pipelines.ts +7 -0
  91. package/layers/media/server/plugins/02.register-media.ts +10 -7
  92. package/layers/media/server/plugins/04.variant-capture.ts +12 -8
  93. package/layers/media/server/plugins/05.media-cleanup.ts +5 -0
  94. package/layers/media/server/tasks/media/backfill.ts +3 -4
  95. package/layers/public/app/components/KestrelLink.vue +1 -1
  96. package/layers/public/app/pages/[...slug].vue +1 -1
  97. package/layers/public/app/pages/__kestrel/preview.vue +2 -2
  98. package/layers/public/app/utils/json-ld.ts +1 -1
  99. package/layers/public/app/utils/link-href.ts +1 -1
  100. package/layers/public/app/utils/page-head.ts +1 -1
  101. package/layers/public/modules/deploy-output/index.ts +4 -6
  102. package/layers/public/modules/prerender-routes/index.ts +4 -4
  103. package/layers/public/modules/prerender-routes/routes.ts +1 -1
  104. package/layers/public/modules/prune-media/index.ts +2 -2
  105. package/layers/public/server/delivery-live/serve.ts +111 -0
  106. package/layers/public/server/middleware/delivery-live-catchall.ts +7 -0
  107. package/layers/public/server/pipelines/index.ts +23 -0
  108. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +14 -0
  109. package/layers/public/server/plugins/01.register-public-pipelines.ts +7 -0
  110. package/layers/public/server/plugins/02.register-links.ts +2 -3
  111. package/layers/public/server/plugins/03.redirects.ts +41 -23
  112. package/layers/public/server/plugins/05.plan-publish.ts +5 -0
  113. package/layers/public/server/plugins/zz.publish.ts +79 -24
  114. package/layers/public/server/routes/llms-full.txt.get.ts +5 -5
  115. package/layers/public/server/routes/llms.txt.get.ts +4 -2
  116. package/layers/public/server/routes/redirects.json.get.ts +2 -2
  117. package/layers/public/server/routes/robots.txt.get.ts +2 -0
  118. package/layers/public/server/routes/sitemap.xml.get.ts +16 -6
  119. package/layers/public/server/tasks/publish/run.ts +9 -4
  120. package/layers/public/server/utils/publish/render-live.ts +26 -0
  121. package/layers/ui/app/components/field/Choice.vue +1 -1
  122. package/layers/ui/app/components/field/Datetime.vue +1 -1
  123. package/layers/ui/app/components/field/Json.vue +1 -2
  124. package/layers/ui/app/components/field/Layout.vue +2 -4
  125. package/layers/ui/app/components/field/Link.vue +1 -1
  126. package/layers/ui/app/components/field/LinkInternalPicker.vue +3 -3
  127. package/layers/ui/app/components/field/Number.vue +2 -2
  128. package/layers/ui/app/components/field/Relation.vue +1 -1
  129. package/layers/ui/app/components/field/Repeater.vue +1 -3
  130. package/layers/ui/app/components/field/Slug.vue +2 -3
  131. package/layers/ui/app/components/field/Text.vue +1 -1
  132. package/layers/ui/app/components/ui/Richtext.vue +1 -1
  133. package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
  134. package/layers/ui/app/composables/useLinkField.ts +1 -1
  135. package/layers/ui/app/composables/useRepeater.ts +1 -1
  136. package/layers/ui/app/i18n/de.ts +7 -7
  137. package/layers/ui/app/i18n/en.ts +7 -39
  138. package/layers/ui/app/utils/field-component.ts +1 -1
  139. package/layers/ui/app/utils/field-registry.ts +1 -1
  140. package/layers/ui/app/utils/icons.ts +2 -6
  141. package/layers/ui/app/utils/localized.ts +1 -1
  142. package/nuxt.config.ts +1 -1
  143. package/package.json +53 -19
  144. package/scripts/consumer-template-ci.mjs +160 -0
  145. package/scripts/coverage-all.mjs +163 -0
  146. package/scripts/dashboard.mjs +89 -0
  147. package/scripts/hash-password.mjs +1 -1
  148. package/scripts/lib/coverage-model.mjs +51 -0
  149. package/scripts/port-graph.mjs +350 -0
  150. package/scripts/typecheck.mjs +11 -0
  151. package/templates/starter/app/blocks/Prose.vue +1 -1
  152. package/layers/access/server/utils/csrf.ts +0 -39
  153. package/layers/access/server/utils/grant-registry.ts +0 -36
  154. package/layers/access/server/utils/guard.ts +0 -44
  155. package/layers/access/server/utils/ip-allowlist.ts +0 -79
  156. package/layers/access/server/utils/policy.ts +0 -98
  157. package/layers/access/server/utils/public-resources.ts +0 -11
  158. package/layers/access/server/utils/render-context.ts +0 -41
  159. package/layers/access/server/utils/require-admin.ts +0 -15
  160. package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
  161. package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
  162. package/layers/auth/server/api/auth/login.post.ts +0 -38
  163. package/layers/auth/server/api/auth/logout.post.ts +0 -9
  164. package/layers/auth/server/api/auth/session.get.ts +0 -1
  165. package/layers/auth/server/utils/admin-credential.ts +0 -42
  166. package/layers/auth/server/utils/client-ip.ts +0 -88
  167. package/layers/auth/server/utils/login-throttle.ts +0 -126
  168. package/layers/auth/server/utils/password.ts +0 -52
  169. package/layers/auth/server/utils/session-cookie.ts +0 -44
  170. package/layers/auth/server/utils/session-epoch.ts +0 -96
  171. package/layers/auth/server/utils/session.ts +0 -88
  172. package/layers/collections/server/collections/pages.ts +0 -19
  173. package/layers/collections/server/utils/populate-relations.ts +0 -79
  174. package/layers/core/app/utils/bulk-actions.ts +0 -9
  175. package/layers/core/app/utils/filter-ops.ts +0 -98
  176. package/layers/core/app/utils/list-limits.ts +0 -27
  177. package/layers/core/app/utils/locale-path.ts +0 -12
  178. package/layers/core/app/utils/slugify.ts +0 -16
  179. package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
  180. package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
  181. package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
  182. package/layers/core/server/api/[collection]/[id].get.ts +0 -11
  183. package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
  184. package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
  185. package/layers/core/server/api/[collection]/index.get.ts +0 -24
  186. package/layers/core/server/api/[collection]/index.post.ts +0 -8
  187. package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
  188. package/layers/core/server/api/[collection]/index.put.ts +0 -24
  189. package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
  190. package/layers/core/server/api/[collection]/options.get.ts +0 -18
  191. package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
  192. package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
  193. package/layers/core/server/api/[collection]/translations.get.ts +0 -25
  194. package/layers/core/server/api/collections/[name].get.ts +0 -4
  195. package/layers/core/server/api/collections/index.get.ts +0 -3
  196. package/layers/core/server/api/references/broken.get.ts +0 -9
  197. package/layers/core/server/api/references/referrers.get.ts +0 -24
  198. package/layers/core/server/database/record-refs.ts +0 -20
  199. package/layers/core/server/plugins/03.record-refs.ts +0 -12
  200. package/layers/core/server/schema/bootstrap.ts +0 -41
  201. package/layers/core/server/schema/desired.ts +0 -99
  202. package/layers/core/server/schema/dev-sync.ts +0 -23
  203. package/layers/core/server/schema/dialect.ts +0 -61
  204. package/layers/core/server/schema/diff.ts +0 -110
  205. package/layers/core/server/schema/introspect.ts +0 -68
  206. package/layers/core/server/schema/model.ts +0 -61
  207. package/layers/core/server/schema/render-sqlite.ts +0 -89
  208. package/layers/core/server/schema/sync.ts +0 -201
  209. package/layers/core/server/utils/blocks.ts +0 -6
  210. package/layers/core/server/utils/collection-types.ts +0 -28
  211. package/layers/core/server/utils/crud.ts +0 -578
  212. package/layers/core/server/utils/db.ts +0 -26
  213. package/layers/core/server/utils/defineCollection.ts +0 -213
  214. package/layers/core/server/utils/duplicate.ts +0 -162
  215. package/layers/core/server/utils/field-layout.ts +0 -125
  216. package/layers/core/server/utils/filter-predicate.ts +0 -101
  217. package/layers/core/server/utils/http.ts +0 -50
  218. package/layers/core/server/utils/if-unmodified.ts +0 -13
  219. package/layers/core/server/utils/kestrel-config.ts +0 -451
  220. package/layers/core/server/utils/key-lock.ts +0 -33
  221. package/layers/core/server/utils/locale.ts +0 -53
  222. package/layers/core/server/utils/page-route.ts +0 -58
  223. package/layers/core/server/utils/page-slug.ts +0 -127
  224. package/layers/core/server/utils/picker.ts +0 -82
  225. package/layers/core/server/utils/populate.ts +0 -50
  226. package/layers/core/server/utils/prerender-memo.ts +0 -23
  227. package/layers/core/server/utils/read-capture.ts +0 -37
  228. package/layers/core/server/utils/record-ref-index.ts +0 -200
  229. package/layers/core/server/utils/registry.ts +0 -24
  230. package/layers/core/server/utils/resolve-scope.ts +0 -79
  231. package/layers/core/server/utils/seo.ts +0 -42
  232. package/layers/core/server/utils/serialize-collection.ts +0 -147
  233. package/layers/core/server/utils/sql.ts +0 -2
  234. package/layers/core/server/utils/storage.local.ts +0 -117
  235. package/layers/core/server/utils/storage.s3.ts +0 -197
  236. package/layers/core/server/utils/storage.ts +0 -90
  237. package/layers/core/server/utils/write-effects.ts +0 -40
  238. package/layers/core/server/utils/write-events.ts +0 -31
  239. package/layers/fields/app/utils/condition.ts +0 -71
  240. package/layers/fields/app/utils/define-block.ts +0 -13
  241. package/layers/fields/app/utils/field-constraints.ts +0 -17
  242. package/layers/fields/app/utils/field-meta.ts +0 -33
  243. package/layers/fields/app/utils/field-validate.ts +0 -75
  244. package/layers/fields/app/utils/field-value.ts +0 -7
  245. package/layers/fields/app/utils/richtext-links.ts +0 -48
  246. package/layers/fields/server/api/blocks/index.get.ts +0 -9
  247. package/layers/fields/server/field-registry/index.ts +0 -249
  248. package/layers/fields/server/field-registry/naming.ts +0 -10
  249. package/layers/fields/server/field-registry/sanitize.ts +0 -43
  250. package/layers/fields/server/field-registry/types.ts +0 -19
  251. package/layers/fields/server/utils/block-ids.ts +0 -43
  252. package/layers/fields/server/utils/block-populate.ts +0 -41
  253. package/layers/fields/server/utils/buildCollection.ts +0 -90
  254. package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
  255. package/layers/fields/server/utils/buildTable.ts +0 -113
  256. package/layers/fields/server/utils/conditional-required.ts +0 -25
  257. package/layers/fields/server/utils/defineBlock.ts +0 -86
  258. package/layers/fields/server/utils/defineFieldType.ts +0 -33
  259. package/layers/fields/server/utils/extract-refs.ts +0 -152
  260. package/layers/fields/server/utils/field-populate.ts +0 -68
  261. package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
  262. package/layers/media/server/api/media/[id].delete.ts +0 -18
  263. package/layers/media/server/api/media/[id].get.ts +0 -10
  264. package/layers/media/server/api/media/[id].patch.test.ts +0 -66
  265. package/layers/media/server/api/media/[id].patch.ts +0 -65
  266. package/layers/media/server/api/media/backfill.post.ts +0 -18
  267. package/layers/media/server/api/media/copy.post.ts +0 -7
  268. package/layers/media/server/api/media/delete.post.ts +0 -13
  269. package/layers/media/server/api/media/folders.post.ts +0 -18
  270. package/layers/media/server/api/media/index.get.ts +0 -15
  271. package/layers/media/server/api/media/index.post.ts +0 -173
  272. package/layers/media/server/api/media/library.get.ts +0 -17
  273. package/layers/media/server/api/media/move.post.ts +0 -7
  274. package/layers/media/server/api/media/rename.post.ts +0 -7
  275. package/layers/media/server/api/media/resolve.get.ts +0 -13
  276. package/layers/media/server/collections/media-settings.ts +0 -25
  277. package/layers/media/server/collections/media.ts +0 -43
  278. package/layers/media/server/database/folders.ts +0 -10
  279. package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
  280. package/layers/media/server/schema-tables/folders.ts +0 -4
  281. package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
  282. package/layers/media/server/utils/ai-signal-detect.ts +0 -155
  283. package/layers/media/server/utils/backfill.ts +0 -144
  284. package/layers/media/server/utils/derive.ts +0 -59
  285. package/layers/media/server/utils/folder-paths.ts +0 -42
  286. package/layers/media/server/utils/folders.ts +0 -12
  287. package/layers/media/server/utils/library.ts +0 -91
  288. package/layers/media/server/utils/media-cleanup.ts +0 -27
  289. package/layers/media/server/utils/media-enabled.ts +0 -23
  290. package/layers/media/server/utils/media-ops.ts +0 -118
  291. package/layers/media/server/utils/media-write.ts +0 -13
  292. package/layers/media/server/utils/naming.ts +0 -52
  293. package/layers/media/server/utils/ondemand.ts +0 -69
  294. package/layers/media/server/utils/persist-upload.ts +0 -62
  295. package/layers/media/server/utils/populate.ts +0 -47
  296. package/layers/media/server/utils/record.ts +0 -71
  297. package/layers/media/server/utils/relocate-ops.ts +0 -341
  298. package/layers/media/server/utils/relocate-plan.ts +0 -63
  299. package/layers/media/server/utils/resolve.ts +0 -98
  300. package/layers/media/server/utils/sanitize-svg.ts +0 -12
  301. package/layers/media/server/utils/sniff.ts +0 -68
  302. package/layers/media/server/utils/storage-relocate.ts +0 -139
  303. package/layers/media/server/utils/translations.ts +0 -19
  304. package/layers/media/server/utils/usages.ts +0 -66
  305. package/layers/media/server/utils/variant-capture.ts +0 -70
  306. package/layers/media/server/utils/variants.ts +0 -123
  307. package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
  308. package/layers/public/server/api/links/resolve.get.ts +0 -19
  309. package/layers/public/server/api/preview.get.ts +0 -28
  310. package/layers/public/server/api/preview.post.ts +0 -93
  311. package/layers/public/server/api/publish-status.get.ts +0 -58
  312. package/layers/public/server/api/publish.post.ts +0 -84
  313. package/layers/public/server/api/route.get.ts +0 -54
  314. package/layers/public/server/collections/redirects.ts +0 -75
  315. package/layers/public/server/collections/site.ts +0 -44
  316. package/layers/public/server/database/publish-deps.ts +0 -12
  317. package/layers/public/server/database/publish-status.ts +0 -19
  318. package/layers/public/server/schema-tables/publish-deps.ts +0 -4
  319. package/layers/public/server/schema-tables/publish-status.ts +0 -4
  320. package/layers/public/server/utils/link-resolve.ts +0 -38
  321. package/layers/public/server/utils/llms-full.ts +0 -125
  322. package/layers/public/server/utils/llms.ts +0 -52
  323. package/layers/public/server/utils/page-resolve.ts +0 -208
  324. package/layers/public/server/utils/populate-links.ts +0 -66
  325. package/layers/public/server/utils/preview-token.ts +0 -109
  326. package/layers/public/server/utils/publish/coalesce.ts +0 -22
  327. package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
  328. package/layers/public/server/utils/publish/deps.ts +0 -87
  329. package/layers/public/server/utils/publish/invalidation.ts +0 -225
  330. package/layers/public/server/utils/publish/pending.ts +0 -74
  331. package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
  332. package/layers/public/server/utils/publish/publish-status.ts +0 -72
  333. package/layers/public/server/utils/publish/publisher.ts +0 -362
  334. package/layers/public/server/utils/publish/queue.ts +0 -67
  335. package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
  336. package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
  337. package/layers/public/server/utils/publish/route-for-record.ts +0 -16
  338. package/layers/public/server/utils/publish/route-keys.ts +0 -13
  339. package/layers/public/server/utils/richtext-markdown.ts +0 -260
  340. package/layers/public/server/utils/site-url.ts +0 -40
  341. package/layers/public/server/utils/sitemap.ts +0 -99
package/nuxt.config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import kestrel from './kestrel.config'
2
- import { layerKestrelOption } from './layers/core/server/utils/kestrel-config'
2
+ import { layerKestrelOption } from '@michaelthielemann/kestrel-core'
3
3
 
4
4
  // Silence known-harmless, un-actionable THIRD-PARTY build noise (output unaffected — applied to both the
5
5
  // Vite client and the Nitro server Rollup passes). Everything else still surfaces — crucially, any
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaelthielemann/kestrel",
3
- "version": "3.0.1",
3
+ "version": "4.0.1",
4
4
  "description": "A slim, collection-driven Nuxt 4 CMS meta-layer with a runtime schema engine. Add `extends: ['@michaelthielemann/kestrel']`, define collections, and the database migrates itself.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Michael Thielemann <283621694+MichaelThielemann@users.noreply.github.com>",
@@ -24,7 +24,6 @@
24
24
  "bin": {
25
25
  "kestrel": "./scripts/kestrel.mjs"
26
26
  },
27
- "packageManager": "pnpm@11.9.0",
28
27
  "publishConfig": {
29
28
  "access": "public",
30
29
  "provenance": true
@@ -45,21 +44,6 @@
45
44
  "node": ">=22.12.0"
46
45
  },
47
46
  "//lint": "The root eslint.config.mjs imports the generated playground/.nuxt/eslint.config.mjs (Nuxt-aware: auto-import globals for every layer + extension) — `nuxt prepare` must run first or that import 404s on a fresh checkout. Cheap on a warm .nuxt cache, so it's chained rather than a separate step.",
48
- "scripts": {
49
- "dev": "nuxt dev",
50
- "build": "nuxt build",
51
- "preview": "nuxt preview",
52
- "generate": "nuxt generate",
53
- "db:generate": "drizzle-kit generate",
54
- "db:migrate": "drizzle-kit migrate",
55
- "test": "vitest run",
56
- "test:watch": "vitest",
57
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
58
- "test:nuxt": "vitest run --config vitest.nuxt.config.ts",
59
- "typecheck": "node scripts/typecheck.mjs",
60
- "lint": "nuxt prepare playground && eslint .",
61
- "hash-password": "node scripts/hash-password.mjs"
62
- },
63
47
  "dependencies": {
64
48
  "@babel/parser": "^7.29.8",
65
49
  "@internationalized/date": "^3.12.3",
@@ -78,10 +62,12 @@
78
62
  "dompurify": "^3.4.13",
79
63
  "drizzle-orm": "^0.45.2",
80
64
  "drizzle-zod": "^0.8.3",
65
+ "effect": "~3.22.1",
81
66
  "exifr": "^7.1.3",
82
67
  "file-type": "^22.0.1",
83
68
  "jsdom": "^29.1.1",
84
69
  "nanoid": "^5.1.16",
70
+ "nitropack": "^2.13.4",
85
71
  "nuxt": "^4.5.2",
86
72
  "reka-ui": "^2.10.1",
87
73
  "sanitize-html": "^2.17.6",
@@ -89,7 +75,17 @@
89
75
  "sharp": "^0.35.3",
90
76
  "thumbhash": "^0.1.1",
91
77
  "typescript": "^6.0.3",
92
- "zod": "^4.4.3"
78
+ "zod": "^4.4.3",
79
+ "@michaelthielemann/kestrel-access": "0.1.0",
80
+ "@michaelthielemann/kestrel-auth": "0.1.0",
81
+ "@michaelthielemann/kestrel-contracts": "0.1.0",
82
+ "@michaelthielemann/kestrel-collections": "0.1.0",
83
+ "@michaelthielemann/kestrel-core": "0.1.0",
84
+ "@michaelthielemann/kestrel-delivery-live": "0.1.0",
85
+ "@michaelthielemann/kestrel-delivery-static": "0.1.0",
86
+ "@michaelthielemann/kestrel-media": "0.1.0",
87
+ "@michaelthielemann/kestrel-publishing": "0.1.0",
88
+ "@michaelthielemann/kestrel-fields": "0.1.0"
93
89
  },
94
90
  "//peers": "Framework singletons the layer sources import by bare specifier. Peers, not dependencies: a second copy breaks identity (two Vue instances; an h3 v1 `createError` handed to an h3 v2 error handler, which surfaces our 400/404/409 as bare 500s). All optional, because kestrel's own `nuxt` dependency already supplies them transitively — the entry pins identity where a copy exists rather than demanding one, and a required peer would ERESOLVE-fail npm installs on any Nuxt whose bundled range we did not anticipate. Each is also a devDependency so this repo resolves it without auto-install-peers writing a phantom root dependency into the lockfile.",
95
91
  "peerDependencies": {
@@ -113,20 +109,58 @@
113
109
  }
114
110
  },
115
111
  "devDependencies": {
112
+ "@arethetypeswrong/cli": "^0.18.5",
113
+ "@fast-check/vitest": "^0.4.1",
114
+ "@microsoft/api-extractor": "^7.58.13",
116
115
  "@nuxt/eslint": "^1.9.0",
117
116
  "@nuxt/kit": "^4.5.2",
118
117
  "@nuxt/test-utils": "^4.1.0",
118
+ "@stryker-mutator/core": "^10.0.0",
119
+ "@stryker-mutator/vitest-runner": "^10.0.0",
119
120
  "@vitejs/plugin-vue": "^6.0.8",
121
+ "@vitest/coverage-v8": "4.1.10",
120
122
  "@vue/test-utils": "^2.4.11",
121
123
  "drizzle-kit": "^0.31.10",
122
124
  "eslint": "^9.39.1",
125
+ "eslint-plugin-boundaries": "^7.2.0",
126
+ "eslint-plugin-jsdoc": "^64.2.1",
127
+ "eslint-plugin-tsdoc": "^0.5.2",
123
128
  "eslint-plugin-vuejs-accessibility": "^2.4.1",
129
+ "fast-check": "^4.9.0",
124
130
  "h3": "^1.15.11",
125
131
  "happy-dom": "^20.11.2",
126
132
  "playwright-core": "^1.62.1",
133
+ "publint": "^0.3.24",
134
+ "typedoc": "^0.28.20",
127
135
  "vitest": "^4.1.10",
136
+ "vitest-openapi": "^1.0.3",
128
137
  "vue": "3.5.35",
129
138
  "vue-router": "^5.2.0",
130
139
  "vue-tsc": "^3.3.9"
140
+ },
141
+ "scripts": {
142
+ "dev": "nuxt dev",
143
+ "build": "nuxt build",
144
+ "build:packages": "pnpm -F @michaelthielemann/kestrel-contracts build && pnpm -F @michaelthielemann/kestrel-core build && pnpm -F @michaelthielemann/kestrel-fields build && pnpm -F @michaelthielemann/kestrel-auth build && pnpm -F @michaelthielemann/kestrel-access build && pnpm -F @michaelthielemann/kestrel-media build && pnpm -F @michaelthielemann/kestrel-collections build && pnpm -F @michaelthielemann/kestrel-publishing build && pnpm -F @michaelthielemann/kestrel-delivery-live build && pnpm -F @michaelthielemann/kestrel-delivery-static build",
145
+ "preview": "nuxt preview",
146
+ "generate": "nuxt generate",
147
+ "db:generate": "drizzle-kit generate",
148
+ "db:migrate": "drizzle-kit migrate",
149
+ "test": "vitest run",
150
+ "test:coverage": "node scripts/coverage-all.mjs",
151
+ "test:watch": "vitest",
152
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
153
+ "test:nuxt": "vitest run --config vitest.nuxt.config.ts",
154
+ "test:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r test",
155
+ "typecheck": "node scripts/typecheck.mjs",
156
+ "typecheck:packages": "pnpm --filter \"@michaelthielemann/kestrel-*\" -r typecheck",
157
+ "lint": "nuxt prepare playground && eslint .",
158
+ "docs:api": "nuxt prepare playground && typedoc",
159
+ "api:check": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run -c packages/kestrel-core/api-extractor.json && api-extractor run -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run -c packages/kestrel-fields/api-extractor.json && api-extractor run -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run -c packages/kestrel-delivery-static/api-extractor.json",
160
+ "api:update": "pnpm -F @michaelthielemann/kestrel-contracts build && api-extractor run --local -c packages/kestrel-contracts/api-extractor.json && pnpm -F @michaelthielemann/kestrel-core build && api-extractor run --local -c packages/kestrel-core/api-extractor.json && api-extractor run --local -c packages/kestrel-core/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-fields build && api-extractor run --local -c packages/kestrel-fields/api-extractor.json && api-extractor run --local -c packages/kestrel-fields/api-extractor.client.json && pnpm -F @michaelthielemann/kestrel-auth build && api-extractor run --local -c packages/kestrel-auth/api-extractor.json && pnpm -F @michaelthielemann/kestrel-access build && api-extractor run --local -c packages/kestrel-access/api-extractor.json && pnpm -F @michaelthielemann/kestrel-media build && api-extractor run --local -c packages/kestrel-media/api-extractor.json && pnpm -F @michaelthielemann/kestrel-collections build && api-extractor run --local -c packages/kestrel-collections/api-extractor.json && pnpm -F @michaelthielemann/kestrel-publishing build && api-extractor run --local -c packages/kestrel-publishing/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-live build && api-extractor run --local -c packages/kestrel-delivery-live/api-extractor.json && pnpm -F @michaelthielemann/kestrel-delivery-static build && api-extractor run --local -c packages/kestrel-delivery-static/api-extractor.json",
161
+ "pkg:lint": "publint . && pnpm -F @michaelthielemann/kestrel-contracts build && publint packages/kestrel-contracts && attw --pack packages/kestrel-contracts --profile esm-only && pnpm -F @michaelthielemann/kestrel-core build && publint packages/kestrel-core && attw --pack packages/kestrel-core --profile esm-only && pnpm -F @michaelthielemann/kestrel-fields build && publint packages/kestrel-fields && attw --pack packages/kestrel-fields --profile esm-only && pnpm -F @michaelthielemann/kestrel-auth build && publint packages/kestrel-auth && attw --pack packages/kestrel-auth --profile esm-only && pnpm -F @michaelthielemann/kestrel-access build && publint packages/kestrel-access && attw --pack packages/kestrel-access --profile esm-only && pnpm -F @michaelthielemann/kestrel-media build && publint packages/kestrel-media && attw --pack packages/kestrel-media --profile esm-only && pnpm -F @michaelthielemann/kestrel-collections build && publint packages/kestrel-collections && attw --pack packages/kestrel-collections --profile esm-only && pnpm -F @michaelthielemann/kestrel-publishing build && publint packages/kestrel-publishing && attw --pack packages/kestrel-publishing --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-live build && publint packages/kestrel-delivery-live && attw --pack packages/kestrel-delivery-live --profile esm-only && pnpm -F @michaelthielemann/kestrel-delivery-static build && publint packages/kestrel-delivery-static && attw --pack packages/kestrel-delivery-static --profile esm-only",
162
+ "ci:consumer-template": "node scripts/consumer-template-ci.mjs",
163
+ "dashboard": "npx tsx scripts/dashboard.mjs",
164
+ "hash-password": "node scripts/hash-password.mjs"
131
165
  }
132
- }
166
+ }
@@ -0,0 +1,160 @@
1
+ // The ONLY real package-consumption test. Every earlier gate (api:check, pkg:lint) inspects one
2
+ // package's own tarball in isolation; this script is the one that scaffolds an actual consumer from
3
+ // templates/starter (via create-kestrel's real CLI), packs the engine + every @michaelthielemann/kestrel-* package the way
4
+ // `npm publish` would, installs them into that consumer with a real `npm install` (no workspace protocol,
5
+ // no symlinks back into this repo), builds it, boots the production server, and hits it over real HTTP.
6
+ //
7
+ // Local: `pnpm ci:consumer-template`. CI: the `consumer-template` job in .github/workflows/ci.yml.
8
+ import { execFileSync, spawn } from 'node:child_process'
9
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
10
+ import { tmpdir } from 'node:os'
11
+ import { join } from 'node:path'
12
+ import { setTimeout as sleep } from 'node:timers/promises'
13
+
14
+ const root = process.cwd()
15
+ const log = (msg) => console.log(`[consumer-template-ci] ${msg}`)
16
+ const run = (cmd, args, opts = {}) => execFileSync(cmd, args, { stdio: 'inherit', ...opts })
17
+
18
+ // @michaelthielemann/kestrel-* in the order `pnpm --filter "@michaelthielemann/kestrel-*" -r exec` resolves them (a real topological walk of
19
+ // the workspace graph, not a hand guess) — mirrors the publish order in .github/workflows/release.yml.
20
+ const PACKAGES = ['contracts', 'core', 'auth', 'access', 'fields', 'collections', 'media', 'publishing', 'delivery-live', 'delivery-static']
21
+
22
+ const work = mkdtempSync(join(tmpdir(), 'kestrel-consumer-ci-'))
23
+ const tarballDir = join(work, 'tarballs')
24
+ const consumerDir = join(work, 'site')
25
+ mkdirSync(tarballDir, { recursive: true })
26
+ log(`work dir: ${work}`)
27
+
28
+ let server
29
+ try {
30
+ // `pnpm --filter "@michaelthielemann/kestrel-*" -r build` does NOT reliably respect the dependency
31
+ // graph (observed: kestrel-access started before kestrel-core/kestrel-auth finished) — `build:packages`
32
+ // builds in explicit topological order instead.
33
+ log('building @michaelthielemann/kestrel-* packages (dist/ must exist before pnpm pack)')
34
+ run('pnpm', ['build:packages'], { cwd: root })
35
+
36
+ log('packing the engine + every @michaelthielemann/kestrel-* package as npm publish would')
37
+ const tarball = {}
38
+ for (const dir of ['.', ...PACKAGES.map((n) => `packages/kestrel-${n}`)]) {
39
+ const out = execFileSync('pnpm', ['pack', '--pack-destination', tarballDir], { cwd: join(root, dir) }).toString().trim()
40
+ const file = out.split('\n').pop().trim()
41
+ const name = JSON.parse(readFileSync(join(root, dir, 'package.json'), 'utf8')).name
42
+ tarball[name] = file
43
+ log(` ${name} -> ${file}`)
44
+ }
45
+
46
+ log('scaffolding a consumer from templates/starter via the real create-kestrel CLI')
47
+ const password = 'consumer-ci-password'
48
+ run('node', [join(root, 'packages/create-kestrel/index.mjs'), consumerDir, '--yes', '--password', password], { cwd: work })
49
+
50
+ log('pointing the scaffolded package.json at the packed tarballs (no registry, no workspace protocol)')
51
+ const pkgPath = join(consumerDir, 'package.json')
52
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
53
+ pkg.dependencies['@michaelthielemann/kestrel'] = `file:${tarball['@michaelthielemann/kestrel']}`
54
+ // npm's `overrides` redirects a dependency AT ANY DEPTH — the engine tarball's own manifest declares
55
+ // "@michaelthielemann/kestrel-core": "0.1.0" etc. (pnpm pack already rewrote the workspace:* protocol to that real
56
+ // version), which would otherwise 404 against the real registry since these packages are not published
57
+ // yet in this checkout's tag. This is what actually forces npm's resolver to read each package's real
58
+ // `exports` map / `main` / `types` from a standalone tarball, the same way a real end user's install
59
+ // would — not a symlink into this repo's own packages/*/src.
60
+ pkg.overrides = Object.fromEntries(PACKAGES.map((n) => [`@michaelthielemann/kestrel-${n}`, `file:${tarball[`@michaelthielemann/kestrel-${n}`]}`]))
61
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
62
+
63
+ // The runner's ambient npm (10.9.8 as of this writing) hits a real npm/arborist bug resolving nuxt's
64
+ // large optional-peer set (`TypeError: Cannot read properties of null (reading 'edgesOut')`, inside
65
+ // `#loadPeerSet`) — reproduced directly, unrelated to anything this script does. npm >= 11 does not
66
+ // have it. `npx --yes npm@11` sidesteps needing a global npm upgrade (which needs root the runner may
67
+ // not have) while still using real npm, not pnpm.
68
+ log('npm install (real npm 11 via npx — the most representative simulation of an actual end user)')
69
+ run('npx', ['--yes', 'npm@11', 'install'], { cwd: consumerDir })
70
+
71
+ log('verifying no @michaelthielemann/kestrel-* dependency resolved back into this repo (would prove nothing)')
72
+ for (const n of PACKAGES) {
73
+ let resolved = ''
74
+ try {
75
+ resolved = execFileSync('npm', ['ls', `@michaelthielemann/kestrel-${n}`, '--json'], { cwd: consumerDir }).toString()
76
+ } catch (err) {
77
+ // `npm ls` exits non-zero on a peer-dep mismatch warning even when the tree itself resolved fine;
78
+ // its stdout (on the error object) still carries the real tree, which is what matters here.
79
+ resolved = err.stdout?.toString() ?? ''
80
+ }
81
+ if (resolved.includes(root)) throw new Error(`@michaelthielemann/kestrel-${n} resolved into ${root} — the override did not take effect`)
82
+ }
83
+
84
+ log('reading the .env create-kestrel wrote (session secret + admin password hash)')
85
+ const env = Object.fromEntries(
86
+ readFileSync(join(consumerDir, '.env'), 'utf8')
87
+ .split('\n')
88
+ .map((l) => /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(l))
89
+ .filter(Boolean)
90
+ .map((m) => [m[1], m[2]]),
91
+ )
92
+ // KESTREL_SECURE_COOKIES=false is what create-kestrel writes for a brand-new site (local http dev) —
93
+ // the app itself refuses it under a production boot ("KESTREL_SECURE_COOKIES=false is not allowed in
94
+ // production"), which is the correct, secure-by-default behavior, not a bug this script should route
95
+ // around by keeping the flag. Unset it; the production default is secure.
96
+ delete env.KESTREL_SECURE_COOKIES
97
+
98
+ const devPort = 41822
99
+ log('nuxt dev (brief boot, then kill) — schema auto-syncs additively at dev boot; a production boot never touches it (by design), so this is what a real `pnpm dev` first run also does')
100
+ const dev = spawn('npx', ['nuxt', 'dev', '--port', String(devPort)], {
101
+ cwd: consumerDir,
102
+ env: { ...process.env, ...env },
103
+ stdio: 'inherit',
104
+ })
105
+ try {
106
+ await waitForServer(`http://localhost:${devPort}/robots.txt`, 60000)
107
+ } finally {
108
+ dev.kill()
109
+ await sleep(1000) // let the dev Nitro process release the sqlite file before the production build reopens it
110
+ }
111
+
112
+ log('nuxt build (production) — the real exports-map/deps resolution surface, not a dev-mode shortcut')
113
+ run('npx', ['nuxt', 'build'], { cwd: consumerDir, env: { ...process.env, ...env } })
114
+
115
+ log('booting the built server and hitting it over real HTTP')
116
+ const port = 41823
117
+ server = spawn('node', [join(consumerDir, '.output/server/index.mjs')], {
118
+ cwd: consumerDir,
119
+ env: { ...process.env, ...env, PORT: String(port), NITRO_PORT: String(port) },
120
+ stdio: 'inherit',
121
+ })
122
+ await waitForServer(`http://localhost:${port}/robots.txt`, 30000)
123
+
124
+ const checks = [
125
+ ['GET /robots.txt', `http://localhost:${port}/robots.txt`, 200],
126
+ ['GET /sitemap.xml', `http://localhost:${port}/sitemap.xml`, 200],
127
+ ['GET /admin (login page, SPA shell)', `http://localhost:${port}/admin`, 200],
128
+ ]
129
+ for (const [label, url, wantStatus] of checks) {
130
+ const res = await fetch(url)
131
+ if (res.status !== wantStatus) throw new Error(`${label} -> ${res.status}, expected ${wantStatus}`)
132
+ log(` ${label} -> ${res.status} OK`)
133
+ }
134
+
135
+ const loginRes = await fetch(`http://localhost:${port}/api/login`, {
136
+ method: 'POST',
137
+ headers: { 'content-type': 'application/json' },
138
+ body: JSON.stringify({ password }),
139
+ })
140
+ if (loginRes.status !== 200) throw new Error(`POST /api/login -> ${loginRes.status}, expected 200 (the packed admin auth stack did not resolve)`)
141
+ log(' POST /api/login -> 200 OK (the packed @michaelthielemann/kestrel-auth + @michaelthielemann/kestrel-access stack authenticates for real)')
142
+
143
+ log('PASS — a real npm install of the packed engine + packages boots and serves under production, and login round-trips through the packed auth stack.')
144
+ } finally {
145
+ server?.kill()
146
+ if (!process.env.KEEP_CONSUMER_TEMPLATE_CI_WORKDIR) rmSync(work, { recursive: true, force: true })
147
+ else log(`KEEP_CONSUMER_TEMPLATE_CI_WORKDIR set — leaving ${work} in place`)
148
+ }
149
+
150
+ async function waitForServer(url, timeoutMs) {
151
+ const deadline = Date.now() + timeoutMs
152
+ for (;;) {
153
+ try {
154
+ const res = await fetch(url)
155
+ if (res.status) return
156
+ } catch { /* not up yet */ }
157
+ if (Date.now() > deadline) throw new Error(`server did not come up at ${url} within ${timeoutMs}ms`)
158
+ await sleep(250)
159
+ }
160
+ }
@@ -0,0 +1,163 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
7
+ const VITEST_BIN = path.join(ROOT, 'node_modules/.bin/vitest')
8
+ const PARTS_DIR = path.join(ROOT, 'reports/coverage/.parts')
9
+ const OUT_DIR = path.join(ROOT, 'reports/coverage')
10
+
11
+ function sameKeys(a, b) {
12
+ const ka = Object.keys(a)
13
+ const kb = Object.keys(b)
14
+ if (ka.length !== kb.length) return false
15
+ const setB = new Set(kb)
16
+ return ka.every((k) => setB.has(k))
17
+ }
18
+
19
+ function mapsAgree(a, b) {
20
+ return sameKeys(a.statementMap, b.statementMap) && sameKeys(a.fnMap, b.fnMap) && sameKeys(a.branchMap, b.branchMap)
21
+ }
22
+
23
+ function countExecutedFunctions(entry) {
24
+ return Object.values(entry.f).filter((n) => n > 0).length
25
+ }
26
+
27
+ function cloneEntry(entry) {
28
+ return structuredClone(entry)
29
+ }
30
+
31
+ function sumCounters(a, b) {
32
+ const out = {}
33
+ for (const k of Object.keys(a)) out[k] = a[k] + (b[k] ?? 0)
34
+ return out
35
+ }
36
+
37
+ function sumBranches(a, b) {
38
+ const out = {}
39
+ for (const k of Object.keys(a)) {
40
+ const branchA = a[k]
41
+ const branchB = b[k] ?? []
42
+ out[k] = branchA.map((count, i) => count + (branchB[i] ?? 0))
43
+ }
44
+ return out
45
+ }
46
+
47
+ // Istanbul JSON is keyed by absolute file path; the same path can show up in several parts
48
+ // (a package run and the root run both touching a shared file). When their instrumentation
49
+ // maps agree, counters are additive. When they disagree, the maps describe different
50
+ // transforms of the file — summing them would misattribute counts to the wrong statements/
51
+ // functions, so the richer entry wins outright and the conflict is reported instead of hidden.
52
+ export function mergeCoverage(parts, conflicts = []) {
53
+ const merged = {}
54
+ for (const part of parts) {
55
+ for (const [filePath, entry] of Object.entries(part)) {
56
+ const existing = merged[filePath]
57
+ if (!existing) {
58
+ merged[filePath] = cloneEntry(entry)
59
+ continue
60
+ }
61
+ if (mapsAgree(existing, entry)) {
62
+ merged[filePath] = {
63
+ ...existing,
64
+ s: sumCounters(existing.s, entry.s),
65
+ f: sumCounters(existing.f, entry.f),
66
+ b: sumBranches(existing.b, entry.b),
67
+ }
68
+ } else {
69
+ if (countExecutedFunctions(entry) > countExecutedFunctions(existing)) {
70
+ merged[filePath] = cloneEntry(entry)
71
+ }
72
+ conflicts.push(filePath)
73
+ }
74
+ }
75
+ }
76
+ return merged
77
+ }
78
+
79
+ function readCoverage(dir) {
80
+ const file = path.join(dir, 'coverage-final.json')
81
+ if (!fs.existsSync(file)) return null
82
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
83
+ }
84
+
85
+ function countFiles(coverage) {
86
+ return coverage ? Object.keys(coverage).length : 0
87
+ }
88
+
89
+ function countFunctionsTotal(coverage) {
90
+ if (!coverage) return 0
91
+ let total = 0
92
+ for (const entry of Object.values(coverage)) total += countExecutedFunctions(entry)
93
+ return total
94
+ }
95
+
96
+ function discoverPackageSuites() {
97
+ const packagesDir = path.join(ROOT, 'packages')
98
+ if (!fs.existsSync(packagesDir)) return []
99
+ return fs
100
+ .readdirSync(packagesDir)
101
+ .filter((name) => fs.existsSync(path.join(packagesDir, name, 'vitest.config.ts')))
102
+ .sort()
103
+ .map((name) => ({ name, cwd: path.join(packagesDir, name) }))
104
+ }
105
+
106
+ function runSuite(name, cwd) {
107
+ const reportsDirectory = path.join(PARTS_DIR, name)
108
+ fs.mkdirSync(reportsDirectory, { recursive: true })
109
+ const args = [
110
+ 'run',
111
+ '--coverage.enabled',
112
+ '--coverage.provider=v8',
113
+ '--coverage.reporter=json',
114
+ `--coverage.reportsDirectory=${reportsDirectory}`,
115
+ '--coverage.reportOnFailure=true',
116
+ ]
117
+ console.log(`\n=== ${name} (${path.relative(ROOT, cwd) || '.'}) ===`)
118
+ const result = spawnSync(VITEST_BIN, args, { cwd, stdio: 'inherit' })
119
+ const passed = result.status === 0
120
+ const coverage = readCoverage(reportsDirectory)
121
+ return { name, passed, coverage }
122
+ }
123
+
124
+ export function main() {
125
+ fs.rmSync(PARTS_DIR, { recursive: true, force: true })
126
+ fs.mkdirSync(PARTS_DIR, { recursive: true })
127
+
128
+ const suites = [{ name: 'root', cwd: ROOT }, ...discoverPackageSuites()]
129
+ const results = suites.map(({ name, cwd }) => runSuite(name, cwd))
130
+
131
+ const conflicts = []
132
+ const merged = mergeCoverage(
133
+ results.map((r) => r.coverage).filter(Boolean),
134
+ conflicts,
135
+ )
136
+
137
+ fs.mkdirSync(OUT_DIR, { recursive: true })
138
+ fs.writeFileSync(path.join(OUT_DIR, 'coverage-final.json'), JSON.stringify(merged))
139
+
140
+ console.log('\n=== coverage-all summary ===')
141
+ for (const r of results) {
142
+ const status = r.passed ? 'PASS' : 'FAIL'
143
+ console.log(
144
+ `${r.name.padEnd(24)} ${status} files=${countFiles(r.coverage)} functionsExecuted=${countFunctionsTotal(r.coverage)}`,
145
+ )
146
+ }
147
+
148
+ if (conflicts.length > 0) {
149
+ console.log(`\n${conflicts.length} merge conflict(s) (kept the entry with more executed functions):`)
150
+ for (const filePath of conflicts) console.log(` ${filePath}`)
151
+ } else {
152
+ console.log('\nno merge conflicts')
153
+ }
154
+
155
+ console.log(
156
+ `\nmerged: files=${Object.keys(merged).length} functionsExecuted=${countFunctionsTotal(merged)} -> ${path.join(OUT_DIR, 'coverage-final.json')}`,
157
+ )
158
+
159
+ const anyFailed = results.some((r) => !r.passed)
160
+ process.exitCode = anyFailed ? 1 : 0
161
+ }
162
+
163
+ if (import.meta.url === `file://${process.argv[1]}`) main()
@@ -0,0 +1,89 @@
1
+ // Deterministic static dashboard generator — no network. Boots the pipeline/collection registries
2
+ // the same way test/architecture/pipeline-invariants.test.ts does (no Nuxt boot needed for that), then
3
+ // renders through the SAME renderDashboard() the dev route uses, so the two surfaces never drift.
4
+ //
5
+ // Run with: npx tsx scripts/dashboard.mjs (or `pnpm dashboard`)
6
+
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
8
+ import { join, dirname } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+ import Database from 'better-sqlite3'
11
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
12
+ import { fieldTypes } from '@michaelthielemann/kestrel-fields'
13
+ import {
14
+ buildIntrospectionPipelines, buildPipelineIndex, clearPipelines, clearRegistry,
15
+ registerCollection, registerPipeline, renderDashboard, setResolvedKestrelConfig, allCollections,
16
+ } from '@michaelthielemann/kestrel-core'
17
+ import { buildAuthPipelines } from '@michaelthielemann/kestrel-auth'
18
+ import { pagesCollection } from '@michaelthielemann/kestrel-collections'
19
+ import { mediaCollection, mediaSettingsCollection } from '@michaelthielemann/kestrel-media'
20
+ import { redirectsCollection, siteCollection } from '@michaelthielemann/kestrel-publishing'
21
+ import { resolveServerKestrelConfig } from '../layers/core/server/utils/server-config.ts'
22
+ import { PLUGIN_ORDER } from '../layers/core/modules/plugin-order/plugin-order.ts'
23
+ import posts from '../server/collections/posts.ts'
24
+ import settings from '../server/collections/settings.ts'
25
+
26
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
27
+
28
+ void fieldTypes
29
+ setResolvedKestrelConfig(resolveServerKestrelConfig())
30
+ Object.assign(globalThis, {
31
+ useRuntimeConfig: () => ({
32
+ kestrel: { output: { driver: 'local', dir: '', publicDir: '/kestrel-no-such-public-dir', auto: true, publishOnSave: false, reconcileMinutes: 0, verbose: false, s3: {} } },
33
+ }),
34
+ useDb: () => drizzle(new Database(':memory:')),
35
+ primaryLocale: () => 'en',
36
+ prefixPrimaryLocale: () => false,
37
+ })
38
+
39
+ clearRegistry()
40
+ for (const c of [pagesCollection, posts, settings, mediaCollection, mediaSettingsCollection, redirectsCollection, siteCollection]) registerCollection(c)
41
+
42
+ clearPipelines()
43
+ for (const def of buildAuthPipelines()) registerPipeline(def)
44
+ for (const def of buildIntrospectionPipelines()) registerPipeline(def)
45
+
46
+ const collections = allCollections().map((c) => ({
47
+ name: c.name,
48
+ mode: c.def.mode,
49
+ translatable: Boolean(c.def.translatable),
50
+ fieldCount: Object.keys(c.def.fields).length,
51
+ }))
52
+
53
+ /** Reads a repo-only enrichment file, or returns undefined — a consumer project (or a fresh checkout
54
+ * before `graphify update`/`pnpm api:check` have run) ships none of these, so every read is optional. */
55
+ function readJsonIfExists(relPath) {
56
+ const abs = join(repoRoot, relPath)
57
+ if (!existsSync(abs)) return undefined
58
+ return JSON.parse(readFileSync(abs, 'utf8'))
59
+ }
60
+
61
+ function apiSurfaceSection() {
62
+ const ceilings = readJsonIfExists('test/architecture/public-api-ceilings.json')
63
+ if (!ceilings) return undefined
64
+ const out = []
65
+ for (const [report, ceiling] of Object.entries(ceilings)) {
66
+ const abs = join(repoRoot, report)
67
+ if (!existsSync(abs)) continue
68
+ const count = readFileSync(abs, 'utf8').split('\n').filter((l) => /^\/\/ @(public|alpha|beta|internal)\b/.test(l)).length
69
+ out.push({ report, count, ceiling })
70
+ }
71
+ return out
72
+ }
73
+
74
+ const edgeAllowlist = readJsonIfExists('test/architecture/edge-allowlist.json')
75
+ const apiSurface = apiSurfaceSection()
76
+
77
+ const html = renderDashboard({
78
+ pipelines: buildPipelineIndex(),
79
+ pluginOrder: PLUGIN_ORDER.map((p) => ({ layer: p.layer, file: p.file, after: p.after })),
80
+ collections,
81
+ graph: (edgeAllowlist || apiSurface) ? { packageEdges: edgeAllowlist?.edges, apiSurface } : undefined,
82
+ generatedAt: new Date().toISOString(),
83
+ })
84
+
85
+ const outDir = join(repoRoot, 'docs')
86
+ mkdirSync(outDir, { recursive: true })
87
+ const outFile = join(outDir, 'dashboard.html')
88
+ writeFileSync(outFile, html)
89
+ console.log(`[dashboard] wrote ${outFile} (${buildPipelineIndex().length} pipelines, ${collections.length} collections)`)
@@ -3,7 +3,7 @@ import { hashPassword } from './lib/password.mjs'
3
3
  import { Cancelled, promptPassword } from './lib/cli.mjs'
4
4
 
5
5
  // Not used at runtime: this prints a value an operator pastes into KESTREL_ADMIN_PASSWORD_HASH. Kept as a
6
- // standalone entry point because docs/consuming-kestrel.md tells consumers to run it straight out of
6
+ // standalone entry point because docs/guide/getting-started.md tells consumers to run it straight out of
7
7
  // node_modules, which must keep working whether or not the `kestrel` bin is on PATH.
8
8
  const arg = process.argv[2]
9
9
  if (arg) {
@@ -0,0 +1,51 @@
1
+ // Indexed view over an Istanbul-shaped coverage report, for the audit detectors.
2
+ //
3
+ // Two questions only: which branch arms were never taken, and which functions never ran. Both are
4
+ // keyed by line so they can be joined to the graph, whose nodes carry a start line and no span.
5
+ import { readFileSync, statSync } from 'node:fs'
6
+ import { join, relative, isAbsolute } from 'node:path'
7
+
8
+ export function loadCoverage(root, file = 'reports/coverage/coverage-final.json') {
9
+ const path = isAbsolute(file) ? file : join(root, file)
10
+ let raw
11
+ let generatedAt
12
+ try {
13
+ raw = JSON.parse(readFileSync(path, 'utf8'))
14
+ generatedAt = statSync(path).mtimeMs
15
+ } catch {
16
+ return null
17
+ }
18
+
19
+ const byFile = new Map()
20
+ for (const entry of Object.values(raw)) {
21
+ const rel = relative(root, entry.path)
22
+ const missedBranchLines = new Set()
23
+ let branchMissCount = 0
24
+ for (const [key, arm] of Object.entries(entry.branchMap ?? {})) {
25
+ const counts = entry.b?.[key] ?? []
26
+ arm.locations?.forEach((location, index) => {
27
+ if (counts[index] !== 0) return
28
+ branchMissCount += 1
29
+ const line = location.start?.line ?? arm.line
30
+ if (line) missedBranchLines.add(line)
31
+ })
32
+ }
33
+ const ranFunctionLines = new Set()
34
+ for (const [key, fn] of Object.entries(entry.fnMap ?? {})) {
35
+ if (!entry.f?.[key]) continue
36
+ const line = fn.decl?.start?.line ?? fn.loc?.start?.line
37
+ if (line) ranFunctionLines.add(line)
38
+ }
39
+ byFile.set(rel, { missedBranchLines, branchMissCount, ranFunctionLines })
40
+ }
41
+
42
+ const empty = { missedBranchLines: new Set(), branchMissCount: 0, ranFunctionLines: new Set() }
43
+ return {
44
+ generatedAt,
45
+ files: [...byFile.keys()],
46
+ has: (file) => byFile.has(file),
47
+ missedBranchLines: (file) => (byFile.get(file) ?? empty).missedBranchLines,
48
+ branchMissCount: (file) => (byFile.get(file) ?? empty).branchMissCount,
49
+ ranFunctionLines: (file) => (byFile.get(file) ?? empty).ranFunctionLines,
50
+ }
51
+ }