@michaelthielemann/kestrel 3.0.0 → 4.0.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 (353) 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 +68 -11
  54. package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
  55. package/layers/core/modules/auto-discovery/scan.ts +77 -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 +6 -8
  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/Button.vue +2 -2
  133. package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
  134. package/layers/ui/app/components/ui/Checkbox.vue +2 -2
  135. package/layers/ui/app/components/ui/Combobox.vue +5 -5
  136. package/layers/ui/app/components/ui/DatePicker.vue +2 -2
  137. package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
  138. package/layers/ui/app/components/ui/NumberInput.vue +4 -4
  139. package/layers/ui/app/components/ui/Richtext.vue +1 -1
  140. package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
  141. package/layers/ui/app/components/ui/Select.vue +2 -2
  142. package/layers/ui/app/components/ui/TextInput.vue +4 -4
  143. package/layers/ui/app/components/ui/Textarea.vue +2 -2
  144. package/layers/ui/app/components/ui/TimeInput.vue +2 -2
  145. package/layers/ui/app/components/ui/Toasts.vue +3 -3
  146. package/layers/ui/app/composables/useLinkField.ts +1 -1
  147. package/layers/ui/app/composables/useRepeater.ts +1 -1
  148. package/layers/ui/app/i18n/de.ts +7 -7
  149. package/layers/ui/app/i18n/en.ts +7 -39
  150. package/layers/ui/app/utils/field-component.ts +1 -1
  151. package/layers/ui/app/utils/field-registry.ts +1 -1
  152. package/layers/ui/app/utils/icons.ts +2 -6
  153. package/layers/ui/app/utils/localized.ts +1 -1
  154. package/nuxt.config.ts +1 -1
  155. package/package.json +53 -19
  156. package/scripts/consumer-template-ci.mjs +157 -0
  157. package/scripts/coverage-all.mjs +163 -0
  158. package/scripts/dashboard.mjs +89 -0
  159. package/scripts/hash-password.mjs +1 -1
  160. package/scripts/lib/coverage-model.mjs +51 -0
  161. package/scripts/port-graph.mjs +350 -0
  162. package/scripts/typecheck.mjs +11 -0
  163. package/templates/starter/app/blocks/Prose.vue +1 -1
  164. package/layers/access/server/utils/csrf.ts +0 -39
  165. package/layers/access/server/utils/grant-registry.ts +0 -36
  166. package/layers/access/server/utils/guard.ts +0 -44
  167. package/layers/access/server/utils/ip-allowlist.ts +0 -79
  168. package/layers/access/server/utils/policy.ts +0 -98
  169. package/layers/access/server/utils/public-resources.ts +0 -11
  170. package/layers/access/server/utils/render-context.ts +0 -41
  171. package/layers/access/server/utils/require-admin.ts +0 -15
  172. package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
  173. package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
  174. package/layers/auth/server/api/auth/login.post.ts +0 -38
  175. package/layers/auth/server/api/auth/logout.post.ts +0 -9
  176. package/layers/auth/server/api/auth/session.get.ts +0 -1
  177. package/layers/auth/server/utils/admin-credential.ts +0 -42
  178. package/layers/auth/server/utils/client-ip.ts +0 -88
  179. package/layers/auth/server/utils/login-throttle.ts +0 -126
  180. package/layers/auth/server/utils/password.ts +0 -52
  181. package/layers/auth/server/utils/session-cookie.ts +0 -44
  182. package/layers/auth/server/utils/session-epoch.ts +0 -96
  183. package/layers/auth/server/utils/session.ts +0 -88
  184. package/layers/collections/server/collections/pages.ts +0 -19
  185. package/layers/collections/server/utils/populate-relations.ts +0 -79
  186. package/layers/core/app/utils/bulk-actions.ts +0 -9
  187. package/layers/core/app/utils/filter-ops.ts +0 -98
  188. package/layers/core/app/utils/list-limits.ts +0 -27
  189. package/layers/core/app/utils/locale-path.ts +0 -12
  190. package/layers/core/app/utils/slugify.ts +0 -16
  191. package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
  192. package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
  193. package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
  194. package/layers/core/server/api/[collection]/[id].get.ts +0 -11
  195. package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
  196. package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
  197. package/layers/core/server/api/[collection]/index.get.ts +0 -24
  198. package/layers/core/server/api/[collection]/index.post.ts +0 -8
  199. package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
  200. package/layers/core/server/api/[collection]/index.put.ts +0 -24
  201. package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
  202. package/layers/core/server/api/[collection]/options.get.ts +0 -18
  203. package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
  204. package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
  205. package/layers/core/server/api/[collection]/translations.get.ts +0 -25
  206. package/layers/core/server/api/collections/[name].get.ts +0 -4
  207. package/layers/core/server/api/collections/index.get.ts +0 -3
  208. package/layers/core/server/api/references/broken.get.ts +0 -9
  209. package/layers/core/server/api/references/referrers.get.ts +0 -24
  210. package/layers/core/server/database/record-refs.ts +0 -20
  211. package/layers/core/server/plugins/03.record-refs.ts +0 -12
  212. package/layers/core/server/schema/bootstrap.ts +0 -41
  213. package/layers/core/server/schema/desired.ts +0 -99
  214. package/layers/core/server/schema/dev-sync.ts +0 -23
  215. package/layers/core/server/schema/dialect.ts +0 -61
  216. package/layers/core/server/schema/diff.ts +0 -110
  217. package/layers/core/server/schema/introspect.ts +0 -68
  218. package/layers/core/server/schema/model.ts +0 -61
  219. package/layers/core/server/schema/render-sqlite.ts +0 -89
  220. package/layers/core/server/schema/sync.ts +0 -201
  221. package/layers/core/server/utils/blocks.ts +0 -6
  222. package/layers/core/server/utils/collection-types.ts +0 -28
  223. package/layers/core/server/utils/crud.ts +0 -578
  224. package/layers/core/server/utils/db.ts +0 -26
  225. package/layers/core/server/utils/defineCollection.ts +0 -213
  226. package/layers/core/server/utils/duplicate.ts +0 -162
  227. package/layers/core/server/utils/field-layout.ts +0 -125
  228. package/layers/core/server/utils/filter-predicate.ts +0 -101
  229. package/layers/core/server/utils/http.ts +0 -50
  230. package/layers/core/server/utils/if-unmodified.ts +0 -13
  231. package/layers/core/server/utils/kestrel-config.ts +0 -451
  232. package/layers/core/server/utils/key-lock.ts +0 -33
  233. package/layers/core/server/utils/locale.ts +0 -53
  234. package/layers/core/server/utils/page-route.ts +0 -58
  235. package/layers/core/server/utils/page-slug.ts +0 -127
  236. package/layers/core/server/utils/picker.ts +0 -82
  237. package/layers/core/server/utils/populate.ts +0 -50
  238. package/layers/core/server/utils/prerender-memo.ts +0 -23
  239. package/layers/core/server/utils/read-capture.ts +0 -37
  240. package/layers/core/server/utils/record-ref-index.ts +0 -200
  241. package/layers/core/server/utils/registry.ts +0 -24
  242. package/layers/core/server/utils/resolve-scope.ts +0 -79
  243. package/layers/core/server/utils/seo.ts +0 -42
  244. package/layers/core/server/utils/serialize-collection.ts +0 -147
  245. package/layers/core/server/utils/sql.ts +0 -2
  246. package/layers/core/server/utils/storage.local.ts +0 -117
  247. package/layers/core/server/utils/storage.s3.ts +0 -197
  248. package/layers/core/server/utils/storage.ts +0 -90
  249. package/layers/core/server/utils/write-effects.ts +0 -40
  250. package/layers/core/server/utils/write-events.ts +0 -31
  251. package/layers/fields/app/utils/condition.ts +0 -71
  252. package/layers/fields/app/utils/define-block.ts +0 -13
  253. package/layers/fields/app/utils/field-constraints.ts +0 -17
  254. package/layers/fields/app/utils/field-meta.ts +0 -33
  255. package/layers/fields/app/utils/field-validate.ts +0 -75
  256. package/layers/fields/app/utils/field-value.ts +0 -7
  257. package/layers/fields/app/utils/richtext-links.ts +0 -48
  258. package/layers/fields/server/api/blocks/index.get.ts +0 -9
  259. package/layers/fields/server/field-registry/index.ts +0 -249
  260. package/layers/fields/server/field-registry/naming.ts +0 -10
  261. package/layers/fields/server/field-registry/sanitize.ts +0 -43
  262. package/layers/fields/server/field-registry/types.ts +0 -19
  263. package/layers/fields/server/utils/block-ids.ts +0 -43
  264. package/layers/fields/server/utils/block-populate.ts +0 -41
  265. package/layers/fields/server/utils/buildCollection.ts +0 -90
  266. package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
  267. package/layers/fields/server/utils/buildTable.ts +0 -113
  268. package/layers/fields/server/utils/conditional-required.ts +0 -25
  269. package/layers/fields/server/utils/defineBlock.ts +0 -86
  270. package/layers/fields/server/utils/defineFieldType.ts +0 -33
  271. package/layers/fields/server/utils/extract-refs.ts +0 -152
  272. package/layers/fields/server/utils/field-populate.ts +0 -68
  273. package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
  274. package/layers/media/server/api/media/[id].delete.ts +0 -18
  275. package/layers/media/server/api/media/[id].get.ts +0 -10
  276. package/layers/media/server/api/media/[id].patch.test.ts +0 -66
  277. package/layers/media/server/api/media/[id].patch.ts +0 -65
  278. package/layers/media/server/api/media/backfill.post.ts +0 -18
  279. package/layers/media/server/api/media/copy.post.ts +0 -7
  280. package/layers/media/server/api/media/delete.post.ts +0 -13
  281. package/layers/media/server/api/media/folders.post.ts +0 -18
  282. package/layers/media/server/api/media/index.get.ts +0 -15
  283. package/layers/media/server/api/media/index.post.ts +0 -173
  284. package/layers/media/server/api/media/library.get.ts +0 -17
  285. package/layers/media/server/api/media/move.post.ts +0 -7
  286. package/layers/media/server/api/media/rename.post.ts +0 -7
  287. package/layers/media/server/api/media/resolve.get.ts +0 -13
  288. package/layers/media/server/collections/media-settings.ts +0 -25
  289. package/layers/media/server/collections/media.ts +0 -43
  290. package/layers/media/server/database/folders.ts +0 -10
  291. package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
  292. package/layers/media/server/schema-tables/folders.ts +0 -4
  293. package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
  294. package/layers/media/server/utils/ai-signal-detect.ts +0 -155
  295. package/layers/media/server/utils/backfill.ts +0 -144
  296. package/layers/media/server/utils/derive.ts +0 -59
  297. package/layers/media/server/utils/folder-paths.ts +0 -42
  298. package/layers/media/server/utils/folders.ts +0 -12
  299. package/layers/media/server/utils/library.ts +0 -91
  300. package/layers/media/server/utils/media-cleanup.ts +0 -27
  301. package/layers/media/server/utils/media-enabled.ts +0 -23
  302. package/layers/media/server/utils/media-ops.ts +0 -118
  303. package/layers/media/server/utils/media-write.ts +0 -13
  304. package/layers/media/server/utils/naming.ts +0 -52
  305. package/layers/media/server/utils/ondemand.ts +0 -69
  306. package/layers/media/server/utils/persist-upload.ts +0 -62
  307. package/layers/media/server/utils/populate.ts +0 -47
  308. package/layers/media/server/utils/record.ts +0 -71
  309. package/layers/media/server/utils/relocate-ops.ts +0 -341
  310. package/layers/media/server/utils/relocate-plan.ts +0 -63
  311. package/layers/media/server/utils/resolve.ts +0 -98
  312. package/layers/media/server/utils/sanitize-svg.ts +0 -12
  313. package/layers/media/server/utils/sniff.ts +0 -68
  314. package/layers/media/server/utils/storage-relocate.ts +0 -139
  315. package/layers/media/server/utils/translations.ts +0 -19
  316. package/layers/media/server/utils/usages.ts +0 -66
  317. package/layers/media/server/utils/variant-capture.ts +0 -70
  318. package/layers/media/server/utils/variants.ts +0 -123
  319. package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
  320. package/layers/public/server/api/links/resolve.get.ts +0 -19
  321. package/layers/public/server/api/preview.get.ts +0 -28
  322. package/layers/public/server/api/preview.post.ts +0 -93
  323. package/layers/public/server/api/publish-status.get.ts +0 -58
  324. package/layers/public/server/api/publish.post.ts +0 -84
  325. package/layers/public/server/api/route.get.ts +0 -54
  326. package/layers/public/server/collections/redirects.ts +0 -75
  327. package/layers/public/server/collections/site.ts +0 -44
  328. package/layers/public/server/database/publish-deps.ts +0 -12
  329. package/layers/public/server/database/publish-status.ts +0 -19
  330. package/layers/public/server/schema-tables/publish-deps.ts +0 -4
  331. package/layers/public/server/schema-tables/publish-status.ts +0 -4
  332. package/layers/public/server/utils/link-resolve.ts +0 -38
  333. package/layers/public/server/utils/llms-full.ts +0 -125
  334. package/layers/public/server/utils/llms.ts +0 -52
  335. package/layers/public/server/utils/page-resolve.ts +0 -208
  336. package/layers/public/server/utils/populate-links.ts +0 -66
  337. package/layers/public/server/utils/preview-token.ts +0 -109
  338. package/layers/public/server/utils/publish/coalesce.ts +0 -22
  339. package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
  340. package/layers/public/server/utils/publish/deps.ts +0 -87
  341. package/layers/public/server/utils/publish/invalidation.ts +0 -225
  342. package/layers/public/server/utils/publish/pending.ts +0 -74
  343. package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
  344. package/layers/public/server/utils/publish/publish-status.ts +0 -72
  345. package/layers/public/server/utils/publish/publisher.ts +0 -362
  346. package/layers/public/server/utils/publish/queue.ts +0 -67
  347. package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
  348. package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
  349. package/layers/public/server/utils/publish/route-for-record.ts +0 -16
  350. package/layers/public/server/utils/publish/route-keys.ts +0 -13
  351. package/layers/public/server/utils/richtext-markdown.ts +0 -260
  352. package/layers/public/server/utils/site-url.ts +0 -40
  353. package/layers/public/server/utils/sitemap.ts +0 -99
@@ -1,225 +0,0 @@
1
- import { pageRowHref } from '../../../../core/server/utils/page-route'
2
-
3
- /** The shape of the collection a write targets (the subset invalidation needs). */
4
- export interface WriteCollection {
5
- name: string
6
- pageLike?: boolean
7
- /** Whether the collection has a draft/published `status` column. */
8
- status?: boolean
9
- }
10
-
11
- type Row = Record<string, unknown> | null
12
-
13
- /** A content write reduced to what invalidation needs (computed by `classifyWrite`). */
14
- export interface WriteClassification {
15
- collection: string
16
- pageLike: boolean
17
- status: 'created' | 'updated' | 'deleted'
18
- id: number | null
19
- /** pageLike `path` changed (the old static file must be pruned, the new one written). */
20
- pathChanged: boolean
21
- /** published-ness changed (publish or unpublish — adds/removes the page + sitemap entry). */
22
- statusChanged: boolean
23
- isPublished: boolean
24
- wasPublished: boolean
25
- /** The page's own public route from the NEW row (pageLike + has a path), else null. */
26
- selfRoute: string | null
27
- /** The page's own public route from the OLD row (pageLike + had a path), else null — the route whose
28
- * static file must be pruned on a slug change / unpublish / delete (symmetric to `selfRoute`). */
29
- oldRoute: string | null
30
- groupTag: string | null
31
- /** Page-path tags for the breadcrumb edge — this record's own path before and/or after the write (both,
32
- * on a rename); empty when it is nobody's crumb step on either side. See `pagePathTag`. */
33
- crumbTags: string[]
34
- }
35
-
36
- /** The data tag naming a translation group. `#` keeps it clear of the `<coll>:<id>` record namespace. */
37
- export function translationGroupTag(coll: string, group: string): string {
38
- return `${coll}#group:${group}`
39
- }
40
-
41
- /**
42
- * The data tag naming a page-like PATH rather than a record — the edge a DESCENDANT's breadcrumb hangs on.
43
- *
44
- * It has to be the path, because Kestrel has no parent/child relation between pages: `path` is a plain
45
- * column, a slug is flat unless an editor types slashes into it, and "descendant" is nothing but a
46
- * path-prefix match. So the case that matters most — a page CREATED at `/blog` after `/blog/hello` was
47
- * already published — has no record id that anything could have captured beforehand. A path, by contrast,
48
- * is knowable before its page exists, so a descendant subscribes to the path it looked in.
49
- *
50
- * Deliberately locale-LESS: a non-translatable record has no locale to name, and a descendant looking up
51
- * `/blog` in its own locale must still be reached when the page that appears there is a locale-less one.
52
- * It therefore over-approximates across locales (an `en` `/blog` write also re-renders a `de` descendant
53
- * of the same spelling) — extra renders, never a stale page.
54
- *
55
- * The leading `#` keeps it clear of the `<coll>` and `<coll>:<id>` namespaces (a collection name never
56
- * starts with one).
57
- */
58
- export function pagePathTag(path: string): string {
59
- return `#path:${path}`
60
- }
61
-
62
- /** What to republish for a write. Routes are resolved from `tags` against the captured deps index. */
63
- export type Invalidation =
64
- | { type: 'full' }
65
- | { type: 'tags'; tags: string[]; render: string[]; prune: string[] }
66
- | { type: 'noop' }
67
-
68
- /**
69
- * The routes to actually delete from a coalesced invalidation: `prune` minus anything we just wrote live.
70
- * A render and a prune for the SAME route can coalesce into one window (e.g. a new page created at /x while
71
- * another page vacates /x) — pruning a freshly-rendered route would delete the live page, so render wins.
72
- * Filtered against the ACTUALLY-rendered set (not the render candidates), so a route whose record is now a
73
- * draft — skipped by the renderer — is still pruned.
74
- */
75
- export function routesToPrune(prune: string[], rendered: string[]): string[] {
76
- const live = new Set(rendered)
77
- return prune.filter((r) => !live.has(r))
78
- }
79
-
80
- /** A collection with no `status` column is always public; otherwise public ⇔ status === 'published'. */
81
- function isPublic(def: WriteCollection, row: Row): boolean {
82
- if (!def.status) return true
83
- return !!row && row.status === 'published'
84
- }
85
-
86
- /**
87
- * The path this record contributes as a breadcrumb step, or null when it contributes none. Mirrors
88
- * `publishedAncestors` exactly — that lookup skips anything unpublished, `noindex`ed or path-less, so a
89
- * record in one of those states is in nobody's trail and its writes owe nobody a re-render.
90
- */
91
- function crumbPath(def: WriteCollection, row: Row): string | null {
92
- if (!def.pageLike || !row) return null
93
- if (!isPublic(def, row)) return null
94
- if ((row.seo as { noindex?: boolean } | null | undefined)?.noindex) return null
95
- return typeof row.path === 'string' && row.path ? row.path : null
96
- }
97
-
98
- /** Reduce a before/after row pair (either may be null) to a `WriteClassification`. Pure (routes via the shared `pageRowHref`). */
99
- export function classifyWrite(def: WriteCollection, before: Row, after: Row, primaryLocale: string, prefixPrimary = false): WriteClassification {
100
- const status: WriteClassification['status'] = before === null ? 'created' : after === null ? 'deleted' : 'updated'
101
- const row = after ?? before
102
- const id = typeof row?.id === 'number' ? row.id : null
103
- const pageLike = !!def.pageLike
104
-
105
- const isPublished = isPublic(def, after)
106
- const wasPublished = isPublic(def, before)
107
- const statusChanged = status === 'updated' && !!def.status && isPublished !== wasPublished
108
-
109
- const routeFrom = (r: Row): string | null => (pageLike ? pageRowHref(r, primaryLocale, prefixPrimary) : null)
110
- const selfRoute = routeFrom(after)
111
- const oldRoute = routeFrom(before)
112
- // Compare the RESOLVED routes, not raw `path`: the route is localePath(path, locale), so a locale-only
113
- // change (path unchanged) still moves the route — the old-locale file must be pruned.
114
- const pathChanged = pageLike && status === 'updated' && oldRoute !== selfRoute
115
-
116
- // `update` refuses to move a row between groups, so the surviving row's group is the group either way.
117
- const group = row?.translationGroup
118
- const groupTag = typeof group === 'string' && group ? translationGroupTag(def.name, group) : null
119
-
120
- // Both sides, so a rename repairs the descendants of the OLD path as well as the new one. Deliberately
121
- // NOT narrowed to "did the crumb's label actually change": the explicit publish action classifies its
122
- // write as before === after (`publish.post.ts` — a re-render of the record's current state), so a diff
123
- // would see nothing on the very publish that makes the crumb appear.
124
- const crumbTags = [...new Set([crumbPath(def, before), crumbPath(def, after)].filter((p): p is string => p !== null))]
125
- .map(pagePathTag)
126
-
127
- return { collection: def.name, pageLike, status, id, pathChanged, statusChanged, isPublished, wasPublished, selfRoute, oldRoute, groupTag, crumbTags }
128
- }
129
-
130
- /**
131
- * What the write listener enqueues for a content write. `publishOnSave` (`output.publishOnSave`) is the
132
- * documented way back to the pre-2.0 model where a save WAS a publish: with it on, a write plans exactly
133
- * what it always did. Off (the default), only removals pass — see `planSaveInvalidation`.
134
- */
135
- export function planWrite(ev: WriteClassification, publishOnSave: boolean): Invalidation {
136
- return publishOnSave ? planInvalidation(ev) : planSaveInvalidation(ev)
137
- }
138
-
139
- /**
140
- * What a plain SAVE may do to the static output. Saving persists to the DB; writing a page's file is the
141
- * explicit publish action's job (`planInvalidation`, driven by `POST /api/publish`), so a save renders
142
- * nothing — the live site keeps serving the last published version while the editor works on the next one.
143
- *
144
- * REMOVAL is the asymmetry, and it is deliberate: an unpublished or deleted record must not keep a live
145
- * page, so those two branches act immediately. Their referrer/listing re-renders come along, because a
146
- * baked link to a page that just went offline is stale the moment it goes — the same "availability" rule
147
- * `planInvalidation` documents, minus everything that would put NEW content on the live site.
148
- */
149
- export function planSaveInvalidation(ev: WriteClassification): Invalidation {
150
- const removal = ev.status === 'deleted' || (ev.statusChanged && !ev.isPublished)
151
- return removal ? planInvalidation(ev) : { type: 'noop' }
152
- }
153
-
154
- /**
155
- * Decide what a write invalidates, per the maintainer-agreed model. Three notions of "dependent":
156
- * - LISTINGS — pages that QUERY the collection (overviews) → captured as the `<coll>` tag.
157
- * - EXPLICIT REFERRERS — pages that LINK/EMBED/relate-to a specific record → captured as `<coll>:<id>`.
158
- * - TRANSLATION SIBLINGS — every member of a group bakes the group's hreflang set → captured as the
159
- * group tag, the only edge that reaches members which rendered before this row existed.
160
- *
161
- * Two principles drive the split:
162
- * 1. FRESHENING (content or path changed) re-renders BOTH listings and explicit referrers (`[coll, coll:id]`)
163
- * so their output stays fresh.
164
- * 2. AVAILABILITY (publish / unpublish / delete) re-renders listings (membership changed) AND the explicit
165
- * referrers (`[coll, coll:id]`), because a referrer's BAKED output encodes the target's availability:
166
- * link resolution is status-gated (a draft target bakes `href="#"`) and a page's hreflang set lists only
167
- * its published translation siblings — both go dead/stale the moment availability flips, and only a
168
- * re-render can fix them. A record's OWN artifact always matches its DB state (render on publish, prune
169
- * on unpublish/delete). A draft that stays a draft is a no-op.
170
- *
171
- * `full` is reserved for the boot publish + the reconciler (enqueued directly), never produced here.
172
- */
173
- export function planInvalidation(ev: WriteClassification): Invalidation {
174
- const coll = ev.collection
175
- const recordTag = ev.id != null ? `${coll}:${ev.id}` : null
176
- // Unlike recordTag (dropped on create — no referrer can target a brand-new id), groupTag is included even
177
- // there: a new sibling still changes every existing member's hreflang set.
178
- const groupTags = ev.groupTag ? [ev.groupTag] : []
179
- // Like groupTag, the crumb tags ride EVERY branch including create — a page appearing at an ancestor
180
- // path is exactly what a descendant's breadcrumb was waiting for.
181
- const tags = recordTag ? [coll, recordTag, ...groupTags, ...ev.crumbTags] : [coll, ...groupTags, ...ev.crumbTags]
182
- const selfRender = ev.pageLike && ev.selfRoute ? [ev.selfRoute] : []
183
-
184
- // DELETE — leaves the collection. Listings re-render, referrers too (their baked link/hreflang now points
185
- // at a route that is about to disappear); the own (pageLike) route is pruned.
186
- if (ev.status === 'deleted') {
187
- const prune = ev.pageLike && ev.oldRoute ? [ev.oldRoute] : []
188
- return { type: 'tags', tags, render: [], prune }
189
- }
190
-
191
- // CREATE — joins the collection. A created draft produces no public output (noop); a created published
192
- // record re-renders listings + its own route. No referrer can point at a brand-new id, so no `coll:id`.
193
- if (ev.status === 'created') {
194
- if (!ev.isPublished) return { type: 'noop' }
195
- return { type: 'tags', tags: [coll, ...groupTags, ...ev.crumbTags], render: selfRender, prune: [] }
196
- }
197
-
198
- // UNPUBLISH — leaves the published set. Listings re-render; referrers re-render so their link falls back to
199
- // '#' and their hreflang drops this sibling; the own route is pruned (the live file sits at the OLD path if
200
- // this save also renamed).
201
- if (ev.statusChanged && !ev.isPublished) {
202
- const route = ev.oldRoute ?? ev.selfRoute
203
- const prune = ev.pageLike && route ? [route] : []
204
- return { type: 'tags', tags, render: [], prune }
205
- }
206
-
207
- // PUBLISH — joins the published set. Listings re-render; referrers re-render because a link to a draft
208
- // resolves to '#' — their baked href only becomes the real path on a re-render; the own route renders.
209
- if (ev.statusChanged && ev.isPublished) {
210
- return { type: 'tags', tags, render: selfRender, prune: [] }
211
- }
212
-
213
- // Below: a plain update, no availability change. A draft that stays a draft touches no public output.
214
- if (!ev.isPublished) return { type: 'noop' }
215
-
216
- // SLUG / PATH CHANGE — path freshening: listings + referrers re-render (their link path updates); the new
217
- // route renders, the old route is pruned.
218
- if (ev.pathChanged) {
219
- const prune = ev.oldRoute ? [ev.oldRoute] : []
220
- return { type: 'tags', tags, render: selfRender, prune }
221
- }
222
-
223
- // CONTENT EDIT — content freshening: listings + explicit referrers re-render; the own route re-renders.
224
- return { type: 'tags', tags, render: selfRender, prune: [] }
225
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * "Saved but not published": the state a deferred-publish model needs to name. Saving writes the DB,
3
- * publishing writes the static file, so the two stamps drift apart on purpose — a record edited after its
4
- * page was last published keeps serving the published version until someone publishes again.
5
- *
6
- * The tolerance is not cosmetic: `publish_status.updated_at` is stored in whole seconds while a record's
7
- * `updatedAt` is milliseconds, so the publish that directly followed a save can carry a stamp up to a
8
- * second BEHIND it. Without the slack every freshly published page would report unpublished changes.
9
- */
10
- const TOLERANCE_MS = 1000
11
-
12
- export function hasPendingChanges(savedAtMs: number | null | undefined, publishedAtMs: number | null | undefined, toleranceMs = TOLERANCE_MS): boolean {
13
- // Never published (no status row) → nothing to protect: there is no older artifact this edit could
14
- // overtake, so the route is a normal render candidate, not a pending change.
15
- if (savedAtMs == null || publishedAtMs == null) return false
16
- return savedAtMs > publishedAtMs + toleranceMs
17
- }
18
-
19
- /** The subset of `savedAt` routes whose record moved on after the route's last publish. Pure. */
20
- export function pendingRoutes(savedAt: Map<string, number>, publishedAt: Map<string, number>): string[] {
21
- const out: string[] = []
22
- for (const [route, saved] of savedAt) {
23
- if (hasPendingChanges(saved, publishedAt.get(route) ?? null)) out.push(route)
24
- }
25
- return out
26
- }
27
-
28
- export interface HeldRoutes {
29
- /** Routes to leave un-rendered: their record is serving an older published version somewhere. */
30
- hold: Set<string>
31
- /** Previously-published routes that are still the live artifact of a held record — never prune these. */
32
- keep: Set<string>
33
- }
34
-
35
- /**
36
- * Withholding by RECORD rather than by route string, which is what a rename needs. `pendingRoutes` compares
37
- * a route against its own publish stamp, so a renamed record — whose new route has no stamp at all — falls
38
- * through the "never published, nothing to protect" carve-out: the unpublished rename gets rendered and the
39
- * old route, still the live one, is left looking abandoned to the prune. The carve-out is about a FIRST
40
- * deploy having no older version; a rename has one, at the previous route.
41
- *
42
- * So a record's prior published routes are consulted too: if the record has moved on since the newest of
43
- * them, the new route is held back and those prior routes are protected from the prune. A record with no
44
- * prior published route keeps the carve-out — otherwise a first deploy would produce an empty site.
45
- *
46
- * Pure: `routesForTag` is the deps index's lookup, passed in.
47
- */
48
- export function heldRoutes(
49
- savedAt: Map<string, number>,
50
- publishedAt: Map<string, number>,
51
- recordTag: Map<string, string>,
52
- routesForTag: (tag: string) => Iterable<string>,
53
- toleranceMs = TOLERANCE_MS,
54
- ): HeldRoutes {
55
- const hold = new Set<string>()
56
- const keep = new Set<string>()
57
- for (const [route, saved] of savedAt) {
58
- const own = publishedAt.get(route) ?? null
59
- if (own != null) {
60
- if (hasPendingChanges(saved, own, toleranceMs)) hold.add(route)
61
- continue
62
- }
63
- // No stamp of its own: either genuinely never published, or published under a previous route.
64
- const tag = recordTag.get(route)
65
- if (!tag) continue
66
- const priors = [...routesForTag(tag)].filter((r) => r !== route && publishedAt.has(r))
67
- if (!priors.length) continue // first publish of this record — nothing to protect
68
- const newest = Math.max(...priors.map((r) => publishedAt.get(r)!))
69
- if (!hasPendingChanges(saved, newest, toleranceMs)) continue
70
- hold.add(route)
71
- for (const prior of priors) keep.add(prior)
72
- }
73
- return { hold, keep }
74
- }
@@ -1,28 +0,0 @@
1
- import type { PublishQueue } from './queue'
2
- import type { DepsStore } from './deps'
3
-
4
- /**
5
- * The process-wide handle on the running publish machinery. The `zz.publish` Nitro plugin owns the queue
6
- * and the deps index (it builds the output driver and does the logging), but the explicit publish action —
7
- * `POST /api/publish` — has to reach both from inside a request: the queue to enqueue the run, the deps
8
- * index to find the routes this record was baked into. Mirrors core's `write-events` registry: module
9
- * state, set once at boot.
10
- *
11
- * `null` wherever the runtime publisher does not run at all (dev, or `output.auto` off). That is not an
12
- * error — it is what the endpoint reports back so the editor can say "nothing is generated here" instead
13
- * of pretending a publish happened.
14
- */
15
- export interface PublishRuntime {
16
- queue: PublishQueue
17
- deps: DepsStore
18
- }
19
-
20
- let runtime: PublishRuntime | null = null
21
-
22
- export function setPublishRuntime(next: PublishRuntime | null): void {
23
- runtime = next
24
- }
25
-
26
- export function usePublishRuntime(): PublishRuntime | null {
27
- return runtime
28
- }
@@ -1,72 +0,0 @@
1
- import { eq } from 'drizzle-orm'
2
- import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
3
- import { publishStatus } from '../../database/publish-status'
4
-
5
- export interface PublishOutcome {
6
- status: 'success' | 'error'
7
- /** The failure message (render / write / S3) on an 'error' outcome; omitted/null on success. */
8
- error?: string | null
9
- /** Which output the attempt wrote to — `outputConfig().driver` ('local' | 's3'). */
10
- target: string
11
- }
12
-
13
- /**
14
- * Upsert the LATEST publish outcome for a route — one row per route (latest-state, not a history). Called
15
- * by the runtime publisher after each render+put (success) or when one throws (error). Resilient by design:
16
- * a missing `publish_status` table (a not-yet-migrated deploy — prod never auto-DDLs) degrades to a warn
17
- * rather than breaking the publish loop or masking the real render/write error in the caller's catch. The
18
- * timestamp is an explicit `new Date()` so the conflict-update branch (which does not fire `$defaultFn`)
19
- * also refreshes it; drizzle maps it to UNIX seconds (mode:'timestamp'), never raw milliseconds.
20
- */
21
- export function recordPublishStatus(db: BetterSQLite3Database, route: string, outcome: PublishOutcome): void {
22
- try {
23
- const set = { status: outcome.status, error: outcome.error ?? null, target: outcome.target, updatedAt: new Date() }
24
- db.insert(publishStatus).values({ route, ...set }).onConflictDoUpdate({ target: publishStatus.route, set }).run()
25
- } catch (error) {
26
- console.warn('[kestrel] could not record publish status (is `publish_status` migrated? run `db:migrate`):', (error as Error).message)
27
- }
28
- }
29
-
30
- /**
31
- * Classify a route's render result into a publish outcome:
32
- * - `success` — a 200 with a rendered body (write it + record success).
33
- * - `error` — a 5xx: the page itself rendered to a server error (a bad block / relation / template).
34
- * Crucially this surfaces as a non-200 RESPONSE, not a thrown exception, so without this
35
- * branch it would be a silent skip leaving a stale `success` row — the failure mode the
36
- * editor ampel most needs to show.
37
- * - `skip` — any other non-200 (a draft / unpublish race resolving to a 404 / redirect): not an error,
38
- * leave the route's status untouched (the prune path handles a real unpublish).
39
- */
40
- export function renderOutcome(status: number, hasBody: boolean): 'success' | 'error' | 'skip' {
41
- if (status === 200 && hasBody) return 'success'
42
- if (status >= 500) return 'error'
43
- return 'skip'
44
- }
45
-
46
- /**
47
- * Every route's last successful-or-failed publish time, in ms — the "last published" half of the
48
- * saved-vs-published comparison a deferred publish needs. Same missing-table resilience as the writers:
49
- * an unmigrated deploy yields an empty map, which reads as "nothing was ever published here" and so
50
- * holds nothing back.
51
- */
52
- export function lastPublishedAt(db: BetterSQLite3Database): Map<string, number> {
53
- const out = new Map<string, number>()
54
- try {
55
- for (const row of db.select({ route: publishStatus.route, updatedAt: publishStatus.updatedAt }).from(publishStatus).all()) {
56
- if (row.updatedAt instanceof Date) out.set(row.route, row.updatedAt.getTime())
57
- }
58
- } catch (error) {
59
- console.warn('[kestrel] could not read publish status:', (error as Error).message)
60
- }
61
- return out
62
- }
63
-
64
- /** Clear a route's status row — its static file was pruned (unpublish / delete / slug change), so it is no
65
- * longer live. Idempotent; same missing-table resilience as `recordPublishStatus`. */
66
- export function clearPublishStatus(db: BetterSQLite3Database, route: string): void {
67
- try {
68
- db.delete(publishStatus).where(eq(publishStatus.route, route)).run()
69
- } catch (error) {
70
- console.warn('[kestrel] could not clear publish status:', (error as Error).message)
71
- }
72
- }