@michaelthielemann/kestrel 4.1.2 → 5.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 (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. package/templates/starter/tsconfig.json +0 -3
@@ -1,37 +0,0 @@
1
- import type Database from 'better-sqlite3'
2
- import { pagesToRoutes, type PageRouteRow } from './routes'
3
-
4
- /** Tables the schema engine marked routable, by their partial `path` index (see `collectPageRoutes`).
5
- * Empty means this file holds no page-like collection at all — a structural fact about the schema, not a
6
- * statement about how many pages exist: a CMS with one page still has its table and that index. */
7
- export function pageLikeTables(db: Database.Database): string[] {
8
- return (db
9
- .prepare("SELECT DISTINCT tbl_name AS name FROM sqlite_master WHERE type = 'index' AND sql LIKE '%path is not null%'")
10
- .all() as { name: string }[]).map((t) => t.name)
11
- }
12
-
13
- /**
14
- * Page routes to seed for `nuxt generate`, discovered straight from the build-time DB (no registry is
15
- * available at build). Every page-like collection carries a **partial unique index on `path`**
16
- * (`… WHERE path is not null`) — the schema engine's marker for a routable table — so we enumerate those.
17
- * That is reliable where "has a `path` column" is NOT: the media `folders` registry has a `path` column +
18
- * a plain unique index, but no such partial clause, so it is correctly excluded. Per pageLike table:
19
- * published rows (only where a `status` column exists) with a non-null path, keyed by their own `locale`
20
- * (or the primary locale for a non-translatable collection). Returns the deduped, sorted route list
21
- * (always including the root).
22
- */
23
- export function collectPageRoutes(db: Database.Database, primaryLocale: string, prefixPrimary = false): string[] {
24
- const rows: PageRouteRow[] = []
25
- for (const name of pageLikeTables(db)) {
26
- const q = `"${name.replace(/"/g, '""')}"`
27
- const cols = (db.prepare(`PRAGMA table_info(${q})`).all() as { name: string }[]).map((c) => c.name)
28
- const statusClause = cols.includes('status') ? "status = 'published' AND " : ''
29
- if (cols.includes('locale')) {
30
- rows.push(...(db.prepare(`SELECT path, locale FROM ${q} WHERE ${statusClause}path IS NOT NULL`).all() as PageRouteRow[]))
31
- } else {
32
- const bare = db.prepare(`SELECT path FROM ${q} WHERE ${statusClause}path IS NOT NULL`).all() as { path: string | null }[]
33
- for (const r of bare) rows.push({ path: r.path, locale: primaryLocale })
34
- }
35
- }
36
- return pagesToRoutes(rows, primaryLocale, prefixPrimary)
37
- }
@@ -1,88 +0,0 @@
1
- import { cpus } from 'node:os'
2
- import { existsSync } from 'node:fs'
3
- import { defineNuxtModule } from '@nuxt/kit'
4
- import Database from 'better-sqlite3'
5
- import { collectPageRoutes, pageLikeTables } from './discover'
6
- import { localePath, resolveKestrel } from '@michaelthielemann/kestrel-core'
7
- import type { KestrelConfig } from '@michaelthielemann/kestrel-core'
8
- import { META_KEYS } from '@michaelthielemann/kestrel-core'
9
- import { recordRouteDiscovery, type RouteDiscovery } from '@michaelthielemann/kestrel-delivery-static'
10
- // Read published paths from EVERY page-like collection straight from the DB at build time so
11
- // `nuxt generate` prerenders one HTML file per page (see `collectPageRoutes` for how pageLike tables
12
- // are identified). The server's migrate-on-boot hasn't run yet at this point, so a DB that isn't there
13
- // yet degrades to the root alone — reported as `incomplete`, because a deploy that reconciles must be
14
- // able to tell that degrade apart from a CMS that genuinely holds one page.
15
- // `reconciles` (an S3 deploy follows this build) makes an unreadable-but-present DB fail the build
16
- // instead: there the degraded tree would also be uploaded over a real live site. Without that step there
17
- // is nothing destructive downstream, so a degrade stays a warning.
18
- export function discoverRoutes(path: string, primary: string, prefixPrimary: boolean, reconciles = false): RouteDiscovery {
19
- const root = localePath('/', primary, primary, prefixPrimary) // `/` or `/<primary>` when the primary is prefixed
20
- if (path === ':memory:') return { routes: [root], incomplete: 'the build ran against an in-memory database — only the site root was prerendered' }
21
- if (!existsSync(path)) {
22
- console.warn(`[kestrel] prerender: no database at ${path} — prerendering the site root only`)
23
- return { routes: [root], incomplete: `no database at ${path} — only the site root was prerendered` }
24
- }
25
- let db: Database.Database | undefined
26
- try {
27
- db = new Database(path, { readonly: true, fileMustExist: true })
28
- // Opening the file is not enumerating it: an unmigrated, zero-byte or simply wrong `KESTREL_DB` file
29
- // opens as a valid EMPTY database, and the root-only route list it yields is byte-identical to a
30
- // complete enumeration. Report the missing schema so a reconcile downstream can tell the two apart.
31
- if (!pageLikeTables(db).length) {
32
- const why = `the database at ${path} holds no page-like table — only the site root was prerendered`
33
- console.warn(`[kestrel] prerender: ${why}`)
34
- return { routes: [root], incomplete: why }
35
- }
36
- return { routes: collectPageRoutes(db, primary, prefixPrimary) }
37
- } catch (error) {
38
- const why = `cannot read the database at ${path}: ${(error as Error)?.message ?? error}`
39
- if (reconciles) throw new Error(`[kestrel] prerender: ${why} — refusing to ship a site with no pages to an S3 target`)
40
- console.warn(`[kestrel] prerender: ${why} — prerendering the site root only`)
41
- return { routes: [root], incomplete: why }
42
- } finally {
43
- db?.close()
44
- }
45
- }
46
-
47
- export default defineNuxtModule({
48
- meta: { name: 'kestrel-prerender-routes' },
49
- setup(_options, nuxt) {
50
- // Build-time: resolve directly from the consumer's config (runtimeConfig/useRuntimeConfig aren't
51
- // available here), rather than the request-time locale.ts globals.
52
- const c = resolveKestrel(nuxt.options.kestrel as KestrelConfig, process.env, nuxt.options.rootDir)
53
- // The editor-preview fallback page must NEVER be prerendered: the classic `nuxt generate` (static
54
- // preset) auto-seeds every static page path, and this page 404s without an admin session — a
55
- // guaranteed prerender error on every SSG build. Registered unconditionally (before the output.auto
56
- // early-return) so both publishing models exclude it.
57
- nuxt.hook('nitro:config', (nitro) => {
58
- nitro.prerender ||= {}
59
- nitro.prerender.ignore = [...(nitro.prerender.ignore ?? []), '/__kestrel/preview']
60
- })
61
- // The runtime publisher (output.auto) and build-time prerendering are mutually exclusive: a prerendered
62
- // `.output/public/<route>/index.html` SHADOWS the live SSR route, so the publisher's localFetch would
63
- // read STALE build-time HTML instead of rendering fresh. With auto on, the publisher (boot + on-write)
64
- // owns publishing — so skip prerender seeding entirely. (auto off = the classic `nuxt generate` SSG model.)
65
- if (c.output.auto) return
66
- nuxt.hook('nitro:config', (nitro) => {
67
- // Only an S3 target reconciles (deletes what this build didn't produce); a local build leaves the
68
- // tree on disk, so an incomplete enumeration destroys nothing there. `nitro.static` is the same flag
69
- // the deploy itself gates on: this hook also runs for `nuxt dev` and a plain `nuxt build`, and
70
- // neither ships anything, so there a degrade must stay a warning instead of failing the command.
71
- const reconciles = c.output.driver === 's3' && nitro.static === true
72
- const discovery = discoverRoutes(c.dbPath, c.primaryLocale, c.prefixPrimary, reconciles)
73
- // Hand the deploy module the completeness of THIS enumeration — the only step that knows it.
74
- recordRouteDiscovery(nuxt, discovery)
75
- nitro.prerender ||= {}
76
- // Every meta artifact EXCEPT `llms-full.txt`, which is seeded only when the consumer opted in — the
77
- // route 404s otherwise, and a prerender error fails the whole `nuxt generate`.
78
- const meta = META_KEYS.filter((k) => k !== 'llms-full.txt' || c.seo.llmsFull).map((k) => `/${k}`)
79
- nitro.prerender.routes = [...new Set([...(nitro.prerender.routes ?? []), ...discovery.routes, ...meta])]
80
- // Render pages in parallel — the dominant lever on `nuxt generate` wall-clock as page count grows.
81
- // Safe: page reads are synchronous better-sqlite3 (WAL + busy_timeout, can't interleave mid-statement);
82
- // the one WRITE on this path — the variant-registry capture — is an IMMEDIATE transaction, so
83
- // concurrent prerender writers (and a co-running server publisher) serialize instead of losing updates.
84
- // Capped to avoid memory over-subscription; overridable.
85
- nitro.prerender.concurrency ??= Math.min(Math.max(cpus().length, 2), 8)
86
- })
87
- },
88
- })
@@ -1,16 +0,0 @@
1
- import { localePath } from '@michaelthielemann/kestrel-core'
2
-
3
- export interface PageRouteRow {
4
- path: string | null
5
- locale: string
6
- }
7
-
8
- export function pagesToRoutes(rows: PageRouteRow[], primaryLocale: string, prefixPrimary = false): string[] {
9
- // The site root is the primary-locale home — `/` normally, `/<primary>` when the primary is prefixed.
10
- const routes = new Set<string>([localePath('/', primaryLocale, primaryLocale, prefixPrimary)])
11
- for (const { path, locale } of rows) {
12
- if (!path) continue
13
- routes.add(localePath(path, locale, primaryLocale, prefixPrimary))
14
- }
15
- return [...routes].sort()
16
- }
@@ -1,48 +0,0 @@
1
- import { defineNuxtModule } from '@nuxt/kit'
2
- import Database from 'better-sqlite3'
3
- import { resolveKestrel, type KestrelConfig } from '@michaelthielemann/kestrel-core'
4
- import { isEnvTrue } from '@michaelthielemann/kestrel-delivery-static'
5
- import { pruneUnreferencedMedia, mediaOwnedKeys } from './prune-media'
6
-
7
- // The set of storage keys the media library owns (originals + derivatives), read straight from the DB at
8
- // build time. Returns null when the registry can't be read (no DB, no media table) — the caller then SKIPS
9
- // the prune entirely rather than risk deleting extension/consumer blobs (e.g. gallery ciphertext).
10
- function readOwnedKeys(dbPath: string): Set<string> | null {
11
- if (dbPath === ':memory:') return null
12
- try {
13
- const db = new Database(dbPath, { readonly: true, fileMustExist: true })
14
- try {
15
- const rows = db.prepare('SELECT storage_key AS storageKey, derivatives FROM media').all() as { storageKey: string; derivatives: string | null }[]
16
- return mediaOwnedKeys(rows.map((r) => ({ storageKey: r.storageKey, derivatives: r.derivatives ? JSON.parse(r.derivatives) : null })))
17
- } finally {
18
- db.close()
19
- }
20
- } catch {
21
- return null
22
- }
23
- }
24
-
25
- // `nuxt generate` bakes the WHOLE upload dir into .output/public/<baseUrl>; without this, images of
26
- // deleted/unpublished pages ship to prod. At `compiled` (full output on disk, post prerender-abort) delete
27
- // every baked media file no generated page references — prunes the BAKE only, so a wrong prune costs a
28
- // re-generate, not data. Local-driver only (only it registers the /uploads publicAssets that gets baked).
29
- // If deploy-output is ever added to modules[], it must come AFTER this so the prune runs before the ship.
30
- export default defineNuxtModule({
31
- meta: { name: 'kestrel-prune-media' },
32
- setup(_options, nuxt) {
33
- const c = resolveKestrel(nuxt.options.kestrel as KestrelConfig, process.env, nuxt.options.rootDir)
34
- if (c.media.driver !== 'local') return
35
- const dryRun = isEnvTrue(process.env.KESTREL_OUTPUT_DRY_RUN)
36
- nuxt.hook('nitro:init', (nitro) => {
37
- nitro.hooks.hook('compiled', () => {
38
- if (nitro.options.static !== true) return
39
- const ownedKeys = readOwnedKeys(c.dbPath)
40
- if (!ownedKeys) {
41
- console.log('[kestrel] media prune skipped: media registry unavailable (would risk deleting extension/consumer blobs)')
42
- return
43
- }
44
- pruneUnreferencedMedia(nitro.options.output.publicDir, c.media.baseUrl, { dryRun, ownedKeys, log: (m) => console.log(`[kestrel] ${m}`) })
45
- })
46
- })
47
- },
48
- })
@@ -1,81 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs'
2
- import { join, relative, sep } from 'node:path'
3
-
4
- function escapeRe(s: string): string {
5
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
6
- }
7
-
8
- // Prefix-relative keys referenced in `text`. The charset stops at ?/#/quote/space, so query strings,
9
- // fragments and srcset descriptors (320w, 2x) fall outside the key; the leading `/` keeps `/uploads` from
10
- // matching `/myuploads`.
11
- export function referencedKeys(text: string, prefix: string): Set<string> {
12
- const out = new Set<string>()
13
- const re = new RegExp(`${escapeRe(prefix)}/([A-Za-z0-9._/-]+)`, 'g')
14
- for (const m of text.matchAll(re)) out.add(m[1])
15
- return out
16
- }
17
-
18
- // The prunable candidates: unreferenced files that the media library OWNS. When `ownedKeys` is given, a
19
- // file not in it (gallery ciphertext / index, or any consumer blob written via useStorageDriver) is NEVER
20
- // a candidate — those are baked deliberately and their URLs are built at runtime, so they never appear as
21
- // literal keys in the output. Without `ownedKeys` the legacy "prune any unreferenced key" behaviour holds.
22
- export function planMediaPrune(files: string[], referenced: Set<string>, ownedKeys?: Set<string>): string[] {
23
- return files.filter((f) => !referenced.has(f) && (!ownedKeys || ownedKeys.has(f)))
24
- }
25
-
26
- // The storage keys the media library owns: every original + every derivative object it tracks. Used to
27
- // scope the bake prune so it can only ever delete media-library artifacts, never extension/consumer blobs.
28
- export function mediaOwnedKeys(rows: { storageKey?: string | null; derivatives?: Record<string, { key?: string }> | null }[]): Set<string> {
29
- const out = new Set<string>()
30
- for (const r of rows) {
31
- if (r.storageKey) out.add(r.storageKey)
32
- for (const e of Object.values(r.derivatives ?? {})) if (e?.key) out.add(e.key)
33
- }
34
- return out
35
- }
36
-
37
- function walkFiles(dir: string): string[] {
38
- return readdirSync(dir, { recursive: true, withFileTypes: true })
39
- .filter((e) => e.isFile())
40
- .map((e) => join(e.parentPath, e.name))
41
- }
42
-
43
- export interface PruneResult {
44
- kept: number
45
- pruned: number
46
- }
47
-
48
- export interface PruneOptions {
49
- /** Report what would be deleted without touching the filesystem. */
50
- dryRun?: boolean
51
- /** Restrict deletion to these media-library-owned keys; anything else baked under the root is kept. */
52
- ownedKeys?: Set<string>
53
- log?: (msg: string) => void
54
- }
55
-
56
- // Delete every baked upload under `publicDir/<baseUrl>` that no generated .html/.css/.json references.
57
- // Over-scanning those files is keep-on-doubt (a key seen anywhere is kept). No-op when the dir is absent
58
- // (S3 media / nothing baked). Touches only the bake — the library upload dir is elsewhere.
59
- export function pruneUnreferencedMedia(publicDir: string, baseUrl: string, opts: PruneOptions = {}): PruneResult {
60
- const prefix = baseUrl.replace(/\/+$/, '')
61
- if (!prefix) return { kept: 0, pruned: 0 } // an empty baseUrl would target the whole output — refuse
62
- const uploadsDir = join(publicDir, prefix)
63
- if (!existsSync(uploadsDir)) return { kept: 0, pruned: 0 }
64
-
65
- const referenced = new Set<string>()
66
- for (const abs of walkFiles(publicDir)) {
67
- // Also scan JS chunks: a media URL can be emitted only from client JS (a background image, a config
68
- // object), never as literal HTML/CSS — pruning it would 404 the live reference.
69
- if (!/\.(?:html|css|json|m?js)$/i.test(abs)) continue
70
- for (const k of referencedKeys(readFileSync(abs, 'utf8'), prefix)) referenced.add(k)
71
- }
72
-
73
- const files = walkFiles(uploadsDir).map((abs) => ({ abs, key: relative(uploadsDir, abs).split(sep).join('/') }))
74
- const prune = new Set(planMediaPrune(files.map((f) => f.key), referenced, opts.ownedKeys))
75
- const toDelete = files.filter((f) => prune.has(f.key))
76
- if (!opts.dryRun) for (const f of toDelete) rmSync(f.abs, { force: true })
77
-
78
- const kept = files.length - toDelete.length
79
- opts.log?.(`media prune${opts.dryRun ? ' (dry-run)' : ''}: kept ${kept}, ${opts.dryRun ? 'would prune' : 'pruned'} ${toDelete.length} unreferenced file(s)`)
80
- return { kept, pruned: toDelete.length }
81
- }
@@ -1,14 +0,0 @@
1
- import { fileURLToPath } from 'node:url'
2
-
3
- import { kestrelComponents } from '../core/modules/component-namespace/shared'
4
-
5
- export default defineNuxtConfig({
6
- components: [kestrelComponents(import.meta.url)],
7
- modules: [
8
- fileURLToPath(new URL('./modules/prerender-routes/index.ts', import.meta.url)),
9
- fileURLToPath(new URL('./modules/prune-media/index.ts', import.meta.url)),
10
- // AFTER prune-media so the bake is pruned before it ships. Self-gates to output.driver:'s3' + a real
11
- // `nuxt generate`; a no-op otherwise. Without it, output.driver:'s3' + output.auto:false shipped nothing.
12
- fileURLToPath(new URL('./modules/deploy-output/index.ts', import.meta.url)),
13
- ],
14
- })
@@ -1,111 +0,0 @@
1
- import { getMethod, getRequestHeader, sendRedirect, send, setHeader, setResponseStatus, type H3Event } from 'h3'
2
- import { isRendererContext } from '@michaelthielemann/kestrel-access'
3
- import { cacheControlFor, isDeliveryExemptPath, isDeliveryLive } from '@michaelthielemann/kestrel-core'
4
- import { usePublishingDb, currentSnapshot, htmlKeyForRoute } from '@michaelthielemann/kestrel-publishing'
5
- import { liveRedirectFor } from '@michaelthielemann/kestrel-delivery-live'
6
-
7
- // Requests this adapter must never answer, whatever `currentSnapshot` says: the admin app, the pipeline
8
- // API, Nitro's own internals (tasks — e.g. the manual `publish:run` trigger), built assets, and the
9
- // editor's preview ticket flow. `META_ROUTES` mirrors `@michaelthielemann/kestrel-core`'s own `META_KEYS`
10
- // (sitemap/robots/llms/redirects) — those are rendered by their own routes, not snapshots. Every prefix
11
- // is slash-terminated (`normalizeExemptPrefix`'s own rule): a bare `/admin` would also match `/admin-guide`
12
- // — a DIFFERENT, publishable route this adapter must still serve from its own snapshot.
13
- const EXEMPT_PREFIXES = ['/api/', '/admin/', '/_nuxt/', '/_nitro/', '/__kestrel/']
14
- const META_ROUTES = new Set(['/sitemap.xml', '/robots.txt', '/llms.txt', '/llms-full.txt', '/redirects.json'])
15
-
16
- // EXEMPT_PREFIXES is slash-terminated the same way (see the const's own comment), so the bare form gets
17
- // the same exact-path treatment `isDeliveryExemptPath` applies to the configured entries — matching only
18
- // `startsWith(prefix)` left it unmatched, contrary to `deliveryExempt`'s own documented examples
19
- // (`/health`, `/feed.xml`).
20
- function isExempt(path: string): boolean {
21
- if (META_ROUTES.has(path)) return true
22
- if (path === '/admin') return true // the bare admin root has no trailing segment to anchor a '/admin/' prefix match
23
- if (EXEMPT_PREFIXES.some((prefix) => path === prefix.slice(0, -1) || path.startsWith(prefix))) return true
24
- return isDeliveryExemptPath(path)
25
- }
26
-
27
- // A route's own trailing slash is not a distinct route: '/route/' must resolve the same snapshot as
28
- // '/route'. Only the FINAL slash is stripped (one normalization pass), so a prefix match like '/admin/'
29
- // against '/admin/foo/' still sees '/admin/foo' and still starts with '/admin/'. The root path is its
30
- // own fixed point and is never touched.
31
- function normalizeTrailingSlash(path: string): string {
32
- return path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path
33
- }
34
-
35
- // RFC 7232 §3.2: If-None-Match may carry a comma-separated list of ETags, and a bare `*` matches any
36
- // current representation (always a match, as long as the resource exists — checked by the caller).
37
- // This route's ETag is always strong (`"<fingerprint>"`), so a weak validator (`W/"..."`) never matches
38
- // under strong comparison, per the same section.
39
- function ifNoneMatchHits(header: string | undefined, etag: string): boolean {
40
- if (!header) return false
41
- const trimmed = header.trim()
42
- if (trimmed === '*') return true
43
- return trimmed.split(',').some((candidate) => candidate.trim() === etag)
44
- }
45
-
46
- /**
47
- * The delivery-live catch-all: under `delivery: 'live'`, answers public content traffic exclusively from
48
- * `published_snapshots` — a configured redirect, 200 with the snapshot's own html, or 404. Wired as a
49
- * Nitro MIDDLEWARE (see `middleware/delivery-live-catchall.ts`) rather than a route, since a middleware
50
- * runs ahead of Nuxt's page router; middleware return values are ignored by h3, so this writes the
51
- * response itself and returns whether it did.
52
- *
53
- * Returns `false` (unhandled — the caller falls through to ordinary page rendering) for everything the
54
- * adapter must not touch: `delivery: 'static'` (the default), a response already sent, a non-GET/HEAD
55
- * request, a non-content path (admin/api/assets/meta/consumer-exempted routes), or the producer's OWN
56
- * internal render fetch. That last one is `isRendererContext()`/`import.meta.prerender` — the SAME
57
- * unforgeable AsyncLocalStorage marker `pipelines/route.ts` and `00.ip-allowlist.ts` already trust for
58
- * exactly this "is this our own in-process render, not an external request" question (`render-live.ts`'s
59
- * `renderRouteLive` runs its `localFetch` inside `runAsRenderer`, which sets it) — NOT a request header,
60
- * which any external client could forge. Without this bypass, a publish's own render would loop back
61
- * into this catch-all and get served its route's CURRENT (about-to-be-superseded) snapshot instead of
62
- * ever reaching a real page render, so a
63
- * publish under `'live'` could never record a fresh one.
64
- */
65
- export async function serveLiveRoute(event: H3Event): Promise<boolean> {
66
- if (event.node?.res?.writableEnded) return false
67
- if (!isDeliveryLive()) return false
68
- if (import.meta.prerender === true || isRendererContext()) return false
69
- const method = getMethod(event)
70
- if (method !== 'GET' && method !== 'HEAD') return false
71
- const path = normalizeTrailingSlash(event.path.split('?')[0] || '/')
72
- if (isExempt(path)) return false
73
-
74
- // Live mode is the app's own edge — there is no separate njs/edge script in front of it the way the
75
- // static topology has, so a configured redirect has to be honoured here too, before the 404.
76
- const redirect = liveRedirectFor(path)
77
- if (redirect) {
78
- await sendRedirect(event, redirect.target, redirect.status)
79
- return true
80
- }
81
-
82
- const snapshot = currentSnapshot(usePublishingDb().db, path)
83
- if (!snapshot) {
84
- setResponseStatus(event, 404)
85
- await send(event, 'Not Found', 'text/plain')
86
- return true
87
- }
88
-
89
- // The fingerprint is a sha256 of the exact rendered bytes (publisher.ts), so a STRONG ETag is exact:
90
- // any two responses sharing it are byte-identical, never merely semantically equivalent. Cache-Control
91
- // mirrors delivery-static's own policy for an html key (`cacheControlFor`/`REVALIDATE_CACHE`) so the
92
- // two adapters agree on cacheability for the same snapshot. NOTE: this ETag/304 state lives only in
93
- // `published_snapshots`, read fresh per request — there is no separate in-process cache to invalidate,
94
- // so this is safe across multiple server processes sharing the DB.
95
- const etag = `"${snapshot.fingerprint}"`
96
- const cacheControl = cacheControlFor(htmlKeyForRoute(path))
97
- setHeader(event, 'etag', etag)
98
- if (cacheControl) setHeader(event, 'cache-control', cacheControl)
99
-
100
- if (ifNoneMatchHits(getRequestHeader(event, 'if-none-match'), etag)) {
101
- setResponseStatus(event, 304)
102
- await send(event, undefined)
103
- return true
104
- }
105
-
106
- const body = Buffer.from(snapshot.html, 'utf8')
107
- setHeader(event, 'content-length', body.byteLength)
108
- setResponseStatus(event, 200)
109
- await send(event, method === 'HEAD' ? undefined : snapshot.html, 'text/html; charset=utf-8')
110
- return true
111
- }
@@ -1,7 +0,0 @@
1
- import { serveLiveRoute } from '../delivery-live/serve'
2
-
3
- // Runs on every request; only intercepts when serveLiveRoute's own gate says so (delivery-live/ stays
4
- // the single owner of when/how this adapter answers — see its TSDoc).
5
- export default defineEventHandler(async (event) => {
6
- await serveLiveRoute(event)
7
- })
@@ -1,23 +0,0 @@
1
- import type { PipelineDef } from '@michaelthielemann/kestrel-core'
2
- import {
3
- buildPublishPipelines,
4
- buildPublishRunsPipelines,
5
- buildLinkPipelines,
6
- buildPreviewPipelines,
7
- buildRoutePipelines,
8
- } from '@michaelthielemann/kestrel-publishing'
9
- import { buildDeliveryLivePipelines } from '@michaelthielemann/kestrel-delivery-live'
10
-
11
- /** The public layer's own API surface: the render entry, the preview ticket pair, the publish action and
12
- * its status read, the orchestrator's admin progress read, the editor's link resolver, and the
13
- * delivery-live read API. All collection-less, so each lives at `/api/<name>`. */
14
- export function buildPublicPipelines(): PipelineDef[] {
15
- return [
16
- ...buildRoutePipelines(),
17
- ...buildPreviewPipelines(),
18
- ...buildPublishPipelines(),
19
- ...buildPublishRunsPipelines(),
20
- ...buildLinkPipelines(),
21
- ...buildDeliveryLivePipelines(),
22
- ]
23
- }
@@ -1,14 +0,0 @@
1
- import type Database from 'better-sqlite3'
2
- import { useDb } from '@michaelthielemann/kestrel-core'
3
- import { ensureSnapshotTriggers } from '@michaelthielemann/kestrel-publishing'
4
-
5
- // Layer order puts every core-layer plugin (00.migrate, 02.schema-sync) before this layer's, so
6
- // `published_snapshots` already exists here for either provisioning path (a committed migration inside
7
- // Kestrel's own repo, or the schema engine for a consumer layer) — except a boot before EITHER has run,
8
- // which `ensureSnapshotTriggers` itself tolerates (warns, does not crash). Skipped during prerender, same
9
- // as `02.schema-sync`: no DDL runs in that phase.
10
- export default defineNitroPlugin(() => {
11
- if (import.meta.prerender) return
12
- const client = (useDb() as unknown as { $client: Database.Database }).$client
13
- ensureSnapshotTriggers(client)
14
- })
@@ -1,7 +0,0 @@
1
- import { registerPipeline } from '@michaelthielemann/kestrel-core'
2
- import { buildPublicPipelines } from '../pipelines'
3
-
4
- // Registration only — nothing resolves a pipeline here; the registry is read on the first request.
5
- export default defineNitroPlugin(() => {
6
- for (const def of buildPublicPipelines()) registerPipeline(def)
7
- })
@@ -1,16 +0,0 @@
1
- import { registerFieldPopulator } from '@michaelthielemann/kestrel-core'
2
- import { buildLinkFieldPopulators, resolveInternalHref } from '@michaelthielemann/kestrel-publishing'
3
-
4
- // Resolve internal links to the target record's localized public path at read time (page-like targets;
5
- // external/email/tel pass through). The resolver IS status-gated: a link to a DRAFT resolves to nothing and
6
- // bakes `'#'`, so an unpublished slug never reaches published HTML. The href therefore encodes availability,
7
- // which is why flipping a target's status re-renders its referrers; the editor warns about the resulting
8
- // draft/dead links separately. (See link-resolve.ts.)
9
- //
10
- // Registered as `link` + `richtext` per-type populators; the shared field-tree walker dispatches them over
11
- // top-level fields, block props, slots, and repeater entries.
12
- export default defineNitroPlugin(() => {
13
- const { link, richtext } = buildLinkFieldPopulators(resolveInternalHref)
14
- registerFieldPopulator('link', link)
15
- registerFieldPopulator('richtext', richtext)
16
- })
@@ -1,55 +0,0 @@
1
- import { Effect } from 'effect'
2
- import { collectionOf, registerAfterStep } from '@michaelthielemann/kestrel-core'
3
- import type { StepDef } from '@michaelthielemann/kestrel-core'
4
- import { outputDriver, REDIRECTS_COLLECTION, REDIRECTS_FIELD, writeRedirectsArtifact } from '@michaelthielemann/kestrel-publishing'
5
- import { invalidateLiveRedirects } from '@michaelthielemann/kestrel-delivery-live'
6
-
7
- /**
8
- * Publish `redirects.json` on every save of the redirects singleton — deliberately decoupled from the
9
- * publish cycle, so a redirect goes live without a full republish (and without an editor pressing
10
- * Publish, which after ADR-0008 they otherwise would have to).
11
- *
12
- * A CRITICAL after-step, not a non-critical one: a save that reports success while the edge still serves
13
- * the old rules is exactly the failure this feature cannot have — its failure becomes the save's response.
14
- * Registered in every environment, unlike the publish plugin — that one is dev-gated because a dev publish
15
- * would write Vite-dev HTML with no hashed `_nuxt`, a CONTENT problem this artifact does not have (it is
16
- * `JSON.stringify` of DB rows, byte-identical either way).
17
- *
18
- * Where it lands is `output.dir` / the S3 prefix — the same target the publisher uses. With the classic
19
- * `output.auto: false` + `driver: 'local'` build model that is NOT the deployed tree, so there a
20
- * redirect goes live with the next `nuxt generate` instead; documented under Redirects in
21
- * `docs/guide/redirects.md`.
22
- */
23
- export default defineNitroPlugin(() => {
24
- const writeRedirectsStep: StepDef = {
25
- name: 'writeRedirects',
26
- when: (ctx) => collectionOf(ctx).def.name === REDIRECTS_COLLECTION,
27
- whenLabel: `collection is "${REDIRECTS_COLLECTION}"`,
28
- fn: (ctx) => Effect.gen(function* () {
29
- const row = ctx.output as Record<string, unknown>
30
- // The row is already committed regardless of what the static artifact write below does (see its
31
- // own comment) — the live catch-all reads straight from the row, so its cache drops unconditionally,
32
- // in lockstep with every save.
33
- invalidateLiveRedirects()
34
- // A JS try/catch does NOT observe an Effect failure crossing a `yield*` — only Effect's own
35
- // combinators do — so the survivor rewrap has to be Effect.catchAll, not a wrapping try/catch.
36
- yield* Effect.tryPromise({ try: () => writeRedirectsArtifact(row[REDIRECTS_FIELD], outputDriver()), catch: (error) => error }).pipe(
37
- Effect.catchAll((error) => Effect.sync(() => {
38
- // The row is already committed (CRUD holds no transaction), so the only honest message is
39
- // "saved, but not live". Short and ASCII on purpose: h3 truncates nothing but the reason phrase is
40
- // the wire's, and the cause goes in `data` where nothing can strip it. `savedUpdatedAt` lets the
41
- // editor rebaseline on this failure the same way it would on success — it only rebaselines then.
42
- throw createError({
43
- statusCode: 500,
44
- statusMessage: 'Redirects saved, but publishing redirects.json failed. Save again to retry.',
45
- data: {
46
- cause: (error as Error)?.message ?? String(error),
47
- savedUpdatedAt: new Date(row.updatedAt as string | number).getTime(),
48
- },
49
- })
50
- })),
51
- )
52
- }),
53
- }
54
- registerAfterStep({ step: writeRedirectsStep, critical: true, ops: ['updateOne'] })
55
- })
@@ -1,5 +0,0 @@
1
- import { registerPlanPublish } from '@michaelthielemann/kestrel-publishing'
2
-
3
- export default defineNitroPlugin(() => {
4
- registerPlanPublish()
5
- })