@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,7 +0,0 @@
1
- import { registerPipeline } from '@michaelthielemann/kestrel-core'
2
- import { buildAuthPipelines } from '@michaelthielemann/kestrel-auth'
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 buildAuthPipelines()) registerPipeline(def)
7
- })
@@ -1 +0,0 @@
1
- export default defineNuxtConfig({})
@@ -1,29 +0,0 @@
1
- import collections from '#kestrel/collections'
2
- import blocks from '#kestrel/blocks'
3
- import { registerBlock, type BlockDef } from '@michaelthielemann/kestrel-fields'
4
- import { ensureBuilt, collectionEnabled, registerCollection } from '@michaelthielemann/kestrel-core'
5
- import { resolveServerKestrel, serverRuntimeConfig } from '../../../core/server/utils/server-config'
6
- import type { BuiltCollection, CollectionDef } from '@michaelthielemann/kestrel-core'
7
- // Auto-discovered collection files default-export either a plain CollectionDef (the common consumer
8
- // form, `export default defineCollection(…)`) or an already-built collection (advanced). `ensureBuilt`
9
- // normalizes both so the registry only ever holds BuiltCollections.
10
- //
11
- // NOTE on ordering: this plugin's real execution position is declared data now
12
- // (layers/core/modules/plugin-order/plugin-order.ts), not an accident of filename/layer scan order — it
13
- // runs after every core plugin (00.migrate/02.schema-sync included), identically in both build contexts.
14
- // It is order-free regardless: nothing reads the registry at plugin init — migrate/schema-sync read the
15
- // `#kestrel/collections` virtual directly, and the ref/cleanup/publish plugins register deferred listeners.
16
- // Keep that invariant — do not add an init-time allCollections() read.
17
- export default defineNitroPlugin(() => {
18
- for (const block of blocks as BlockDef[]) registerBlock(block)
19
- // Built-in collections (`pages`, `media`) register by default but can be disabled per consumer via
20
- // `kestrel: { collections: { <name>: false } }`. Prefer the value the kestrel module put on runtimeConfig
21
- // (the consumer's `kestrel: {}`); fall back to Kestrel's own resolved config for non-Nitro / dev callers.
22
- const toggles = (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>
23
- for (const collection of collections as (CollectionDef | BuiltCollection)[]) {
24
- const built = ensureBuilt(collection)
25
- // The shared predicate (schema/bootstrap) — the schema engine gates on exactly the same rule.
26
- if (!collectionEnabled(built.def, toggles)) continue
27
- registerCollection(built)
28
- }
29
- })
@@ -1,18 +0,0 @@
1
- import { getCollection, getOne, registerFieldPopulator, useDb } from '@michaelthielemann/kestrel-core'
2
- import { isPubliclyReadable, publicReadableResources } from '@michaelthielemann/kestrel-access'
3
- import { buildRelationFieldPopulator, skipMissing } from '@michaelthielemann/kestrel-collections'
4
- // Expand relation fields to the fully-populated related record(s) under `$<name>` at read time (depth ≥ 1).
5
- // The related read is published-only (a draft target is skipped, never leaked to a public read), tolerates a
6
- // stale/deleted id (getOne 404 → null via `skipMissing`, other errors propagate — fail-loud), and runs at
7
- // `depth - 1`, so `populateRow` bails at 0 and cycles terminate. Registered per-type; the shared field-tree
8
- // walker (fields layer) dispatches it.
9
- // The reachability predicate is the declared public set, NOT the guard's full decision — the guard also
10
- // folds in `registeredGrants()`, which this omits. So a collection opened to anonymous by a registered
11
- // grant is served on its own route but stays unexpanded here: fail-closed drift, never a widening.
12
- export default defineNitroPlugin(() => {
13
- registerFieldPopulator('relation', buildRelationFieldPopulator((collection, id, depth, locale, publicOnly) => {
14
- const built = getCollection(collection)
15
- if (!built) return null
16
- return skipMissing(() => getOne(useDb(), built, id, depth, locale, true, publicOnly) as Record<string, unknown>)
17
- }, (collection) => isPubliclyReadable(collection, publicReadableResources())))
18
- })
@@ -1,5 +0,0 @@
1
- import { kestrelLayouts } from '#build/kestrel-layouts.mjs'
2
-
3
- export function useOfferableLayouts(): string[] {
4
- return kestrelLayouts
5
- }
@@ -1,129 +0,0 @@
1
- import type { FieldDef } from '@michaelthielemann/kestrel-core'
2
-
3
- /**
4
- * Field factories for single-file (SFC) block authoring, à la Pruvious. A block's schema is declared as its
5
- * component's `defineProps({ heading: textField({ required: true }), … })`; each factory returns a DUAL
6
- * object — a Vue prop descriptor (`type` = a JS-global constructor) that ALSO carries the Kestrel `FieldDef`
7
- * under `[KESTREL_FIELD]`. Vue reads `type` at runtime; the build-time SFC extractor reads `[KESTREL_FIELD]`.
8
- *
9
- * This file imports NO Vue/Nuxt runtime (only JS globals), so it is importable BOTH in the browser (the SFC's
10
- * props declaration) AND by the Node extractor that injects these factories to evaluate a block's schema.
11
- * `Symbol.for` (global registry) keeps the key identical even if the file is instantiated twice.
12
- */
13
- export const KESTREL_FIELD = Symbol.for('kestrel.field')
14
-
15
- export interface FieldFactoryResult<C = unknown> {
16
- /** A Vue prop constructor (String / Number / Boolean / Object / Array) — the runtime prop shape. Typed as
17
- * the specific constructor so Vue's `defineProps` accepts the factory result AND infers the prop's value
18
- * type (`textField()` → `string`, `mediaField()` → `number`, …). */
19
- type: C
20
- /** The Kestrel field definition the extractor lifts into the block's schema. */
21
- [KESTREL_FIELD]: FieldDef
22
- }
23
-
24
- // Keys that belong on the FieldDef itself (not inside `options`); everything else a factory receives is a
25
- // type-specific option. Mirrors `BaseFieldDef` in defineCollection.ts.
26
- const BASE_KEYS = new Set(['required', 'unique', 'label', 'default', 'condition', 'renamedFrom', 'populate'])
27
-
28
- function partition(opts: Record<string, unknown>): { base: Record<string, unknown>; options: Record<string, unknown> } {
29
- const base: Record<string, unknown> = {}
30
- const options: Record<string, unknown> = {}
31
- for (const [k, v] of Object.entries(opts)) (BASE_KEYS.has(k) ? base : options)[k] = v
32
- return { base, options }
33
- }
34
-
35
- function wrap<C>(def: Record<string, unknown>, vueType: C): FieldFactoryResult<C> {
36
- // The factory assembles the FieldDef structurally (open discriminated union); cast through `unknown`.
37
- return { type: vueType, [KESTREL_FIELD]: def as unknown as FieldDef }
38
- }
39
-
40
- /** A leaf field: base props at the top level, everything else under `options`. */
41
- function leaf<C>(type: string, vueType: C) {
42
- return (opts: Record<string, unknown> = {}): FieldFactoryResult<C> => {
43
- const { base, options } = partition(opts)
44
- const def: Record<string, unknown> = { type, ...base }
45
- if (Object.keys(options).length) def.options = options
46
- return wrap(def, vueType)
47
- }
48
- }
49
-
50
- /** Generic escape hatch for a CUSTOM field type (registered via `defineFieldType`) that has no dedicated
51
- * factory — `field('secureGallery', { … })`. Base props split off the top level, the rest into `options`;
52
- * the Vue prop is typed `Object` (unknown value shape). */
53
- export function field(type: string, opts: Record<string, unknown> = {}): FieldFactoryResult<ObjectConstructor> {
54
- const { base, options } = partition(opts)
55
- const def: Record<string, unknown> = { type, ...base }
56
- if (Object.keys(options).length) def.options = options
57
- return wrap(def, Object)
58
- }
59
-
60
- export const textField = leaf('text', String)
61
- export const slugField = leaf('slug', String)
62
- export const richtextField = leaf('richtext', String)
63
- export const numberField = leaf('number', Number)
64
- export const booleanField = leaf('boolean', Boolean)
65
- export const jsonField = leaf('json', Object)
66
- export const linkField = leaf('link', Object)
67
-
68
- /** Media: `multiple` (stored bare, an id array) → Array prop; single (a `<name>Id` FK) → Number prop. */
69
- export function mediaField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | NumberConstructor> {
70
- const { base, options } = partition(opts)
71
- const def: Record<string, unknown> = { type: 'media', ...base }
72
- if (Object.keys(options).length) def.options = options
73
- return wrap(def, options.multiple ? Array : Number)
74
- }
75
-
76
- /** Choice: `multiple` → the value is a `string[]` (Array prop), single → a `string` (String prop). */
77
- export function choiceField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | StringConstructor> {
78
- const { base, options } = partition(opts)
79
- const def: Record<string, unknown> = { type: 'choice', ...base }
80
- if (Object.keys(options).length) def.options = options
81
- return wrap(def, options.multiple ? Array : String)
82
- }
83
-
84
- /** Datetime: `range` → the value is a `{ start, end }` object (Object prop), else a `string` (String prop). */
85
- export function datetimeField(opts: Record<string, unknown> = {}): FieldFactoryResult<ObjectConstructor | StringConstructor> {
86
- const { base, options } = partition(opts)
87
- const def: Record<string, unknown> = { type: 'datetime', ...base }
88
- if (Object.keys(options).length) def.options = options
89
- return wrap(def, options.range ? Object : String)
90
- }
91
-
92
- /** Relation: `collection`/`many`/`labelField` map into `relation`; `many` → Array prop, single → Number. */
93
- export function relationField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor | NumberConstructor> {
94
- const { collection, many, labelField, ...rest } = opts as {
95
- collection?: string
96
- many?: boolean
97
- labelField?: string
98
- } & Record<string, unknown>
99
- if (typeof collection !== 'string' || !collection) {
100
- throw new Error('relationField requires a `collection` (the target collection name)')
101
- }
102
- const { base, options } = partition(rest)
103
- const relation: Record<string, unknown> = { collection }
104
- if (many) relation.many = many
105
- if (labelField) relation.labelField = labelField
106
- const def: Record<string, unknown> = { type: 'relation', ...base, relation }
107
- if (Object.keys(options).length) def.options = options
108
- return wrap(def, many ? Array : Number)
109
- }
110
-
111
- /** Repeater: sub-fields are authored as factory calls too — unwrap each to its FieldDef (recursively).
112
- * `fieldLayout` (the sub-field editor layout) forwards into `options` instead of being dropped. */
113
- export function repeaterField(opts: Record<string, unknown> = {}): FieldFactoryResult<ArrayConstructor> {
114
- const { fields, fieldLayout, ...rest } = opts as { fields?: Record<string, unknown>; fieldLayout?: unknown } & Record<string, unknown>
115
- const { base } = partition(rest)
116
- const options: Record<string, unknown> = { fields: unwrapFields(fields ?? {}) }
117
- if (fieldLayout !== undefined) options.fieldLayout = fieldLayout
118
- return wrap({ type: 'repeater', ...base, options }, Array)
119
- }
120
-
121
- /** Convert a `{ name: <factory-result | FieldDef> }` map into a `{ name: FieldDef }` map. A nested repeater's
122
- * result already carries a fully-unwrapped def, so recursion is implicit. */
123
- function unwrapFields(fields: Record<string, unknown>): Record<string, FieldDef> {
124
- const out: Record<string, FieldDef> = {}
125
- for (const [k, v] of Object.entries(fields)) {
126
- out[k] = v && typeof v === 'object' && KESTREL_FIELD in v ? (v as FieldFactoryResult)[KESTREL_FIELD] : (v as FieldDef)
127
- }
128
- return out
129
- }
@@ -1,38 +0,0 @@
1
- /** The admin shell. Never offerable for a public record — a page rendered inside it would carry the
2
- * admin chrome and its own `useHead`. */
3
- export const ADMIN_LAYOUT = 'admin'
4
-
5
- /** Nuxt's resolved `app.layouts` entry (`nuxt.options.app.layouts`, filled before the `app:resolve` hook). */
6
- export interface ResolvedLayout { name: string, file: string }
7
-
8
- /**
9
- * The layout names a page may be assigned, from Nuxt's own resolved layout map. Nuxt has already done the
10
- * layer-ordered, name-first dedup (a consumer's `default.vue` shadows the engine's), so this only filters
11
- * and sorts — no directory scan of our own.
12
- */
13
- export function offerableLayouts(layouts: Record<string, ResolvedLayout | undefined>): string[] {
14
- return Object.values(layouts)
15
- .filter((l): l is ResolvedLayout => !!l && typeof l.file === 'string' && l.file.endsWith('.vue'))
16
- .map((l) => l.name)
17
- .filter((name) => name !== ADMIN_LAYOUT)
18
- .sort()
19
- }
20
-
21
- export function renderLayoutRegistry(names: string[]): string {
22
- return `export const kestrelLayouts = ${JSON.stringify(names)}\n`
23
- }
24
-
25
- /**
26
- * Options for the page-layout select. The fallback is one entry with an EMPTY value — an unset column
27
- * already renders `default`, so offering `default` as its own value would give the editor two controls for
28
- * one outcome and pin the row to a name the consumer may later rename.
29
- */
30
- export function layoutSelectOptions(names: string[], fallbackLabel: string): { label: string, value: string }[] {
31
- return [
32
- { label: fallbackLabel, value: '' },
33
- ...names.filter((n) => n !== DEFAULT_LAYOUT_NAME).map((n) => ({ label: n, value: n })),
34
- ]
35
- }
36
-
37
- /** Mirrors `DEFAULT_LAYOUT` in the public layer; kept local so this util stays dependency-free. */
38
- const DEFAULT_LAYOUT_NAME = 'default'
@@ -1,179 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { basename } from 'node:path'
3
- import { parse } from '@babel/parser'
4
- import type { FieldDef } from '@michaelthielemann/kestrel-core'
5
- import * as factoryModule from '../../app/utils/field-factories'
6
- import { KESTREL_FIELD } from '../../app/utils/field-factories'
7
-
8
- /** The extracted block definition (shape mirrors `BlockDef`, kept structural so this build-time module
9
- * needs no `layers/fields` runtime import). */
10
- export interface ExtractedBlock {
11
- name: string
12
- fields: Record<string, FieldDef>
13
- slots?: string[]
14
- label?: unknown
15
- icon?: string
16
- image?: string
17
- }
18
-
19
- // Only the callable factories are injected into the schema-eval scope (exclude the KESTREL_FIELD symbol).
20
- const FACTORIES: Record<string, unknown> = Object.fromEntries(
21
- Object.entries(factoryModule).filter(([, v]) => typeof v === 'function'),
22
- )
23
- // The factory functions themselves, to catch an uncalled factory (`heading: textField` vs `textField()`).
24
- const FACTORY_SET = new Set(Object.values(FACTORIES))
25
-
26
- /** `Hero.vue` → `hero`, `BoxedContainer.vue` → `boxedContainer` (camel-case; `BlockRenderer` pascal-cases it
27
- * back to the `Blocks<Name>` component). */
28
- export function blockNameFromFile(fileBase: string): string {
29
- const name = fileBase.replace(/\.vue$/, '')
30
- return name.charAt(0).toLowerCase() + name.slice(1)
31
- }
32
-
33
- /** The raw `<script setup>` source. `setup` is matched in ANY attribute position (`<script setup lang="ts">`
34
- * and `<script lang="ts" setup>` are both valid), so a plain `<script>` block (no `setup` word) is skipped.
35
- * (A real Vite build could use `@vue/compiler-sfc parse` for full robustness — e.g. a `</script>` inside a
36
- * string literal; a scoped match is adequate + dependency-light here.) */
37
- function scriptSetup(sfc: string, where: string): string {
38
- const m = sfc.match(/<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/)
39
- if (!m) throw new Error(`${where}: a Kestrel block SFC must have a <script setup> block`)
40
- return m[1]
41
- }
42
-
43
- interface Node { type: string; start: number; end: number; [k: string]: unknown }
44
-
45
- /** Peel the wrappers a macro call is commonly written behind so the CallExpression underneath is reached:
46
- * a TS as-cast / satisfies (`defineProps({…}) as T`), a non-null assertion (`defineProps({…})!`), and
47
- * parentheses. */
48
- function unwrapExpr(node: unknown): Record<string, unknown> | undefined {
49
- let n = node as Record<string, unknown> | undefined
50
- while (n && (n.type === 'TSAsExpression' || n.type === 'TSSatisfiesExpression' || n.type === 'TSNonNullExpression' || n.type === 'ParenthesizedExpression')) {
51
- n = n.expression as Record<string, unknown> | undefined
52
- }
53
- return n
54
- }
55
-
56
- /** If `call` resolves to `<macro>(…)`, its first arg node + whether it carried a type parameter
57
- * (`defineProps<T>()`, unsupported — no runtime schema). Also looks INSIDE `withDefaults(defineProps(…), …)`
58
- * — the most common props idiom — so that form is not silently extracted as an empty schema. */
59
- function macroInCall(call: unknown, macro: string): { arg?: Node; hasTypeParam: boolean } | undefined {
60
- const n = unwrapExpr(call)
61
- if (n?.type !== 'CallExpression') return undefined
62
- const callee = n.callee as Record<string, unknown> | undefined
63
- if (callee?.type === 'Identifier' && callee.name === macro) {
64
- return { arg: (n.arguments as Node[])[0], hasTypeParam: !!(n.typeParameters || n.typeArguments) }
65
- }
66
- // withDefaults(defineProps(…), { … }) — recurse into its arguments to find the wrapped macro call.
67
- if (callee?.type === 'Identifier' && callee.name === 'withDefaults') {
68
- for (const a of (n.arguments as unknown[]) ?? []) {
69
- const hit = macroInCall(a, macro)
70
- if (hit) return hit
71
- }
72
- }
73
- return undefined
74
- }
75
-
76
- /** The FIRST TOP-LEVEL `<macro>(…)` call — a bare `defineBlock({…})` (ExpressionStatement), a
77
- * `const props = defineProps({…})` (VariableDeclaration init, incl. `export const` and wrapper forms).
78
- * Vue's macros are top-level by contract, so scanning only `program.body` (not a deep DFS) means a call
79
- * nested in a helper function can't shadow it. */
80
- function macroCall(ast: unknown, macro: string): { arg?: Node; hasTypeParam: boolean } | undefined {
81
- const body = (ast as { program?: { body?: unknown[] } }).program?.body
82
- if (!Array.isArray(body)) return undefined
83
- for (const raw of body as Array<Record<string, unknown>>) {
84
- // Unwrap `export const props = …` → the VariableDeclaration it wraps.
85
- const stmt = raw.type === 'ExportNamedDeclaration' && raw.declaration ? (raw.declaration as Record<string, unknown>) : raw
86
- if (stmt.type === 'ExpressionStatement') {
87
- const hit = macroInCall(stmt.expression, macro)
88
- if (hit) return hit
89
- } else if (stmt.type === 'VariableDeclaration') {
90
- for (const d of (stmt.declarations as Array<Record<string, unknown>>) ?? []) {
91
- const hit = macroInCall(d.init, macro)
92
- if (hit) return hit
93
- }
94
- }
95
- }
96
- return undefined
97
- }
98
-
99
- function evalObjectExpr(content: string, node: Node, scope: Record<string, unknown>, where: string): Record<string, unknown> {
100
- const src = content.slice(node.start, node.end)
101
- const names = Object.keys(scope)
102
- try {
103
- // `src` is a slice of the CONSUMER's own block-SFC source, evaluated once here at build/discovery time
104
- // to recover the object literal's runtime shape — never attacker- or request-supplied, so this is not
105
- // an eval-injection surface.
106
- const fn = new Function(...names, `return (${src})`)
107
- const out = fn(...names.map((k) => scope[k]))
108
- if (!out || typeof out !== 'object') throw new Error('expected an object literal')
109
- return out as Record<string, unknown>
110
- } catch (e) {
111
- throw new Error(
112
- `${where}: could not evaluate the block declaration. Field/block args must be self-contained literals + ` +
113
- `field-factory calls (no imported constants, computed values, or TS type-args). Cause: ${(e as Error).message}`,
114
- { cause: e },
115
- )
116
- }
117
- }
118
-
119
- /**
120
- * Extract a `BlockDef` from a block SFC's source: `defineProps({ … field factories … })` → `fields`,
121
- * `defineBlock({ label, slots, icon })` → metadata, `name` ← filename. Static (no component execution): the
122
- * `defineProps`/`defineBlock` argument object-literals are sliced from source and evaluated with the field
123
- * factories in scope; each field's `[KESTREL_FIELD]` is lifted into the schema.
124
- */
125
- export function extractBlockDef(sfcSource: string, fileBase: string): ExtractedBlock {
126
- const name = blockNameFromFile(fileBase)
127
- const content = scriptSetup(sfcSource, fileBase)
128
- let ast: ReturnType<typeof parse>
129
- try {
130
- ast = parse(content, { sourceType: 'module', plugins: ['typescript'] })
131
- } catch (e) {
132
- throw new Error(`${fileBase}: could not parse <script setup> — ${(e as Error).message}`, { cause: e })
133
- }
134
-
135
- const props = macroCall(ast, 'defineProps')
136
- if (props?.hasTypeParam && !props.arg) {
137
- throw new Error(`${fileBase}: use the runtime form defineProps({ … }) with field factories, not defineProps<T>() — the type-only form carries no schema`)
138
- }
139
- const propsObj = props?.arg ? evalObjectExpr(content, props.arg, FACTORIES, fileBase) : {}
140
-
141
- const block = macroCall(ast, 'defineBlock')
142
- const meta = block?.arg ? evalObjectExpr(content, block.arg, {}, fileBase) : {}
143
-
144
- const fields: Record<string, FieldDef> = {}
145
- for (const [key, value] of Object.entries(propsObj)) {
146
- const carried = value && typeof value === 'object' ? (value as Record<symbol, unknown>)[KESTREL_FIELD] : undefined
147
- if (carried === undefined) {
148
- // An UNCALLED field factory (`heading: textField` instead of `textField()`) evaluates to the factory
149
- // function itself — catch that common slip loudly rather than silently dropping the field.
150
- if (typeof value === 'function' && FACTORY_SET.has(value)) {
151
- throw new Error(`${fileBase}: prop "${key}" is a field factory that was not called — write ${(value as { name?: string }).name ?? 'xField'}({ … })`)
152
- }
153
- // Otherwise it's a plain (display-only) Vue prop (e.g. `media: Object`, the server-resolved $media bag
154
- // BlockRenderer passes) — not part of the block schema. A misspelled factory throws at eval time above.
155
- continue
156
- }
157
- const def = carried as FieldDef & { default?: unknown }
158
- if (typeof def.default === 'function') {
159
- // The registry is inlined as JSON (renderBlockRegistry) — a function default would be silently dropped.
160
- throw new Error(`${fileBase}: prop "${key}" has a function \`default\` — block field defaults must be JSON-serializable literals`)
161
- }
162
- fields[key] = carried as FieldDef
163
- }
164
-
165
- const out: ExtractedBlock = { name, fields }
166
- if (meta.label !== undefined) out.label = meta.label
167
- if (Array.isArray(meta.slots) && meta.slots.length) out.slots = meta.slots as string[]
168
- if (typeof meta.icon === 'string') out.icon = meta.icon
169
- if (typeof meta.image === 'string') out.image = meta.image
170
- return out
171
- }
172
-
173
- /** The `#kestrel/blocks` virtual body: extract each block SFC and inline the definitions as JSON data
174
- * (the block registry is plain, JSON-safe data — no imports needed beyond the field-type side effect the
175
- * caller prepends). */
176
- export function renderBlockRegistry(vuePaths: string[]): string {
177
- const defs = vuePaths.map((p) => extractBlockDef(readFileSync(p, 'utf8'), basename(p)))
178
- return `export default ${JSON.stringify(defs)}`
179
- }
@@ -1,112 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { addComponentsDir, addTemplate, addTypeTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
4
- import { collectBlockSfcs, collectDefinitions, collectManifestFiles, renderPackageConcatRegistry, renderPackageMergedRegistry, renderRegistry, resolvePackageEntry } from './scan'
5
- import { renderBlockRegistry } from './extract-block'
6
- import { offerableLayouts, renderLayoutRegistry } from '../../app/utils/layouts'
7
- import { PACKAGE_COLLECTIONS, PACKAGE_MANIFESTS, PACKAGE_SCHEMA_TABLES } from './package-registry'
8
-
9
- export default defineNuxtModule({
10
- meta: { name: 'kestrel-auto-discovery' },
11
- setup(_options, nuxt) {
12
- const roots = nuxt.options._layers.map((layer) => layer.cwd)
13
- // The `#kestrel/*` registries are Nitro VIRTUAL modules (runtime only) — tsc can't see their shape, so
14
- // server code importing them would be `Cannot find module`. Register the ambient declarations into BOTH
15
- // the Nitro and Nuxt tsconfigs so the typecheck resolves them.
16
- const { resolve } = createResolver(import.meta.url)
17
- addTypeTemplate({ filename: 'types/kestrel-virtual.d.ts', src: resolve('./virtual.d.ts') }, { nitro: true, nuxt: true })
18
-
19
- // Blocks are authored as single `.vue` files under each layer's `app/blocks/` — the SFC is BOTH the
20
- // schema source (extracted at build into `#kestrel/blocks`) AND the display component. Register the dir
21
- // so `app/blocks/Hero.vue` resolves as the global `BlocksHero` that the public BlockRenderer renders.
22
- // No Kestrel package ships a block today, so this stays a pure consumer-dir scan (nothing to bridge).
23
- for (const root of roots) {
24
- const dir = join(root, 'app/blocks')
25
- if (existsSync(dir)) addComponentsDir({ path: dir, prefix: 'Blocks', global: true, pathPrefix: false })
26
- }
27
-
28
- // Layouts need no scan of our own: Nuxt already resolves `app/layouts/*.vue` across the layers with the
29
- // same name-first, consumer-wins dedup, and fills `app.layouts` just before `app:resolve` — which runs
30
- // inside `generateApp`, ahead of the templates being written, so the closure below is filled in time.
31
- let layoutNames: string[] = []
32
- nuxt.hook('app:resolve', (app) => { layoutNames = offerableLayouts(app.layouts ?? {}) })
33
- // `write` so the resolved list is inspectable in `.nuxt/` — a virtual-only template makes "which layouts
34
- // did the build actually find" unanswerable without a debugger.
35
- addTemplate({ filename: 'kestrel-layouts.mjs', write: true, getContents: () => renderLayoutRegistry(layoutNames) })
36
-
37
- nuxt.hook('nitro:config', (nitro) => {
38
- nitro.virtual ||= {}
39
- // `@michaelthielemann/kestrel-fields`'s built-in descriptors (text/richtext/number/…) seed `core`'s field-type registry
40
- // as a side effect of importing the package itself — core cannot import fields, so this is the one
41
- // place that import happens. Resolved to a real file path (`resolvePackageEntry`), not left as the
42
- // bare specifier: a Nitro virtual module has no real file of its own for a bundler to resolve a bare
43
- // import FROM, so it falls back to the project root — where, under a real consumer's pnpm install,
44
- // this package isn't visible (only the engine directly depends on it). Prepended so it always runs, even with zero consumer
45
- // field types (an empty `collectDefinitions` result would otherwise generate `export default []`,
46
- // importing nothing). Consumer field types register as a side effect on import too, and the schema
47
- // engine builds a table the moment a collection/block module loads — so the collections/blocks/
48
- // schema-tables/module-manifests virtuals ALL import the field-type seed FIRST (every one of them can
49
- // now reach a package's `buildCollection()` call transitively via `kestrelDiscovery`, per ADR-0029 —
50
- // an ESM barrel is an eager, whole-module-graph load — so all four need the same guard, not only the
51
- // two that directly build collections themselves).
52
- const importFieldTypesSeed = `import { fieldTypes as __kestrelSeed } from ${JSON.stringify(resolvePackageEntry('@michaelthielemann/kestrel-fields'))}\n`
53
- + `if (!__kestrelSeed || typeof __kestrelSeed !== 'object') throw new Error('[kestrel] built-in field types failed to seed')\n`
54
- // A real runtime check on the imported binding, not a bare `import "path"` or a discarded `void x`
55
- // — Nitro's dev build tree-shakes an import whose binding has no provable effect, silently dropping
56
- // the seed and breaking every collection. A `throw` Rollup can't prove unreachable survives.
57
- // MUST be a static `import` (not `import()`): a dynamic import runs as an ordinary statement AFTER
58
- // every static import of the same module has already evaluated (ESM linking order), which would run
59
- // it too late — after a collection's own static import has already tried to build its table. A static
60
- // import evaluates before any sibling static import declared after it, which is what orders this
61
- // correctly. Every virtual below imports `@michaelthielemann/kestrel-fields` directly for this reason (on top of, not
62
- // instead of, `#kestrel/field-types` for collections/blocks — chaining virtual-imports-virtual was
63
- // unreliable in Nitro dev, so the real package import stays even where the virtual ALSO gets imported).
64
- const importFieldTypesVirtual = `${importFieldTypesSeed}import { default as __kestrelFieldTypes } from '#kestrel/field-types'\n`
65
- + `if (!__kestrelFieldTypes) throw new Error('[kestrel] #kestrel/field-types failed to load')\n`
66
-
67
- nitro.virtual['#kestrel/field-types'] = () =>
68
- `${importFieldTypesSeed}${renderRegistry(collectDefinitions(roots, 'server/field-types'))}`
69
-
70
- nitro.virtual['#kestrel/collections'] = () =>
71
- renderPackageMergedRegistry({
72
- packages: PACKAGE_COLLECTIONS.map(resolvePackageEntry),
73
- property: 'collections',
74
- consumerFiles: collectDefinitions(roots, 'server/collections'),
75
- nameOfExpr: '(x) => x.name',
76
- preamble: importFieldTypesVirtual,
77
- })
78
-
79
- // Extract each `app/blocks/*.vue` into a plain BlockDef inlined as JSON (no per-file imports needed).
80
- // No package ships a block today (see above), so this stays the plain consumer-dir scan, prefixed
81
- // with the same seed a block's field-typed props may need.
82
- nitro.virtual['#kestrel/blocks'] = () =>
83
- `${importFieldTypesVirtual}${renderBlockRegistry(collectBlockSfcs(roots))}`
84
-
85
- // Standalone (non-collection) tables: each package's `kestrelDiscovery.schemaTables`, merged with
86
- // each layer's `server/schema-tables/*.ts` default export (media → folders, public → publish_deps/
87
- // publish_status/…; consumers can add their own). The schema engine consumes this instead of core
88
- // hardcoding upper-layer tables.
89
- nitro.virtual['#kestrel/schema-tables'] = () =>
90
- renderPackageMergedRegistry({
91
- packages: PACKAGE_SCHEMA_TABLES.map(resolvePackageEntry),
92
- property: 'schemaTables',
93
- consumerFiles: collectDefinitions(roots, 'server/schema-tables'),
94
- nameOfExpr: '(x) => __kestrelTableName(x)',
95
- extraImports: `import { getTableName as __kestrelTableName } from 'drizzle-orm'`,
96
- preamble: importFieldTypesVirtual,
97
- })
98
-
99
- // Per-module ownership manifests (ADR-0012): each package's `kestrelDiscovery.manifest` plus each
100
- // layer's `server/db/manifest.ts` default export, for the `db:migrate-module` task to enumerate
101
- // without core importing an upper layer (or a package) directly. No dedup — every contribution is a
102
- // distinct module (see `collectManifestFiles`'s own TSDoc).
103
- nitro.virtual['#kestrel/module-manifests'] = () =>
104
- renderPackageConcatRegistry({
105
- packages: PACKAGE_MANIFESTS.map(resolvePackageEntry),
106
- property: 'manifest',
107
- consumerFiles: collectManifestFiles(roots),
108
- preamble: importFieldTypesVirtual,
109
- })
110
- })
111
- },
112
- })
@@ -1,19 +0,0 @@
1
- /**
2
- * Kestrel's own packages that ship discoverable content, by category — a package declares WHAT it
3
- * contributes via its own `kestrelDiscovery` named export (see `@michaelthielemann/kestrel-core`'s `KestrelPackageDiscovery`
4
- * TSDoc); this module still needs an explicit list of WHICH packages to check, since a Nitro virtual's
5
- * generated source is plain text written before anything runs — there is no filesystem/`node_modules` scan
6
- * to discover the list itself. Deliberately per-category, not one flat package list: a virtual only ever
7
- * imports a package here if that package actually contributes to ITS category, so (for example) building
8
- * the schema-tables virtual never eagerly touches `@michaelthielemann/kestrel-collections`, which has none.
9
- *
10
- * A package with content in a new category is added to the matching list below when that lands — an
11
- * explicit, reviewable registration, not a guess. `test/architecture/kestrel-discovery.test.ts`'s
12
- * "bidirectional consistency" suite computes, against every real workspace package's actual
13
- * `kestrelDiscovery` export, that these three lists are exactly right (nothing missing, nothing stale) —
14
- * so a package that starts or stops contributing a category and forgets to update the matching list here
15
- * fails a fast unit test, not a runtime 404 discovered later.
16
- */
17
- export const PACKAGE_COLLECTIONS = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-collections', '@michaelthielemann/kestrel-publishing']
18
- export const PACKAGE_SCHEMA_TABLES = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-publishing']
19
- export const PACKAGE_MANIFESTS = ['@michaelthielemann/kestrel-media', '@michaelthielemann/kestrel-publishing']